feat(wms): 新增合同挂接钢卷统计功能
- 在IWmsCoilContractRelService接口中添加queryContractCoilStatistics方法 - 在WmsCoilContractRelController控制器中添加统计查询和导出接口 - 在WmsCoilContractRelMapper中添加selectContractCoilStatistics查询方法 - 实现合同挂接钢卷统计的SQL查询逻辑,支持多条件筛选 - 创建ContractCoilStatisticsBo查询对象和ContractCoilStatisticsVo视图对象 - 开发前端统计页面,包含搜索筛选、统计卡片、图表展示和数据表格 - 集成ECharts实现合同状态分布饼图和销售员挂接钢卷柱状图 - 实现统计数据的Excel导出功能 - 添加响应式设计适配移动端显示
This commit is contained in:
10
klp-ui/src/api/wms/contractCoilStatistics.js
Normal file
10
klp-ui/src/api/wms/contractCoilStatistics.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询合同挂接情况统计
|
||||||
|
export function getContractCoilStatistics(query) {
|
||||||
|
return request({
|
||||||
|
url: '/wms/coilContractRel/statistics',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
613
klp-ui/src/views/wms/contractCoilStatistics/index.vue
Normal file
613
klp-ui/src/views/wms/contractCoilStatistics/index.vue
Normal file
@@ -0,0 +1,613 @@
|
|||||||
|
<template>
|
||||||
|
<div class="statistics-container" v-loading="loading">
|
||||||
|
<!-- 搜索筛选区 -->
|
||||||
|
<div class="filter-panel">
|
||||||
|
<el-form :model="queryParams" ref="queryForm" :inline="true" size="small">
|
||||||
|
<el-form-item label="合同号" prop="contractCode">
|
||||||
|
<el-input v-model="queryParams.contractCode" placeholder="请输入合同号" clearable @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="合同名称" prop="contractName">
|
||||||
|
<el-input v-model="queryParams.contractName" placeholder="请输入合同名称" clearable @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="订单编号" prop="orderCode">
|
||||||
|
<el-input v-model="queryParams.orderCode" placeholder="请输入订单编号" clearable @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="需方" prop="customer">
|
||||||
|
<el-input v-model="queryParams.customer" placeholder="请输入需方" clearable @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="销售员" prop="salesman">
|
||||||
|
<el-input v-model="queryParams.salesman" placeholder="请输入销售员" clearable @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="合同状态" prop="status">
|
||||||
|
<el-select v-model="queryParams.status" placeholder="请选择" clearable style="width:120px">
|
||||||
|
<el-option label="草稿" :value="0" />
|
||||||
|
<el-option label="生效" :value="1" />
|
||||||
|
<el-option label="作废" :value="2" />
|
||||||
|
<el-option label="已完成" :value="3" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
|
||||||
|
<el-button type="warning" icon="el-icon-download" @click="handleExport">导出</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 四个统计卡片 -->
|
||||||
|
<el-row :gutter="16" class="stat-cards-row">
|
||||||
|
<el-col :xs="24" :sm="12" :md="6">
|
||||||
|
<div class="stat-card card-blue">
|
||||||
|
<div class="card-inner">
|
||||||
|
<div class="card-icon"><i class="el-icon-document"></i></div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-value">{{ summary.totalContracts }}</div>
|
||||||
|
<div class="card-label">合同总数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<span>全部合同</span>
|
||||||
|
<i class="el-icon-arrow-right"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="6">
|
||||||
|
<div class="stat-card card-green">
|
||||||
|
<div class="card-inner">
|
||||||
|
<div class="card-icon"><i class="el-icon-link"></i></div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-value">{{ summary.attachedContracts }}</div>
|
||||||
|
<div class="card-label">已挂接合同</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<span>占比 {{ attachmentRate }}%</span>
|
||||||
|
<i class="el-icon-arrow-right"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="6">
|
||||||
|
<div class="stat-card card-orange">
|
||||||
|
<div class="card-inner">
|
||||||
|
<div class="card-icon"><i class="el-icon-warning-outline"></i></div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-value">{{ summary.unattachedContracts }}</div>
|
||||||
|
<div class="card-label">未挂接合同</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<span>需关注</span>
|
||||||
|
<i class="el-icon-arrow-right"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="6">
|
||||||
|
<div class="stat-card card-red">
|
||||||
|
<div class="card-inner">
|
||||||
|
<div class="card-icon"><i class="el-icon-s-data"></i></div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-value">{{ summary.totalCoils }}</div>
|
||||||
|
<div class="card-label">挂接钢卷总数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<span>总净重 {{ summary.totalWeight }}t</span>
|
||||||
|
<i class="el-icon-arrow-right"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 图表区域 -->
|
||||||
|
<el-row :gutter="16" class="charts-row">
|
||||||
|
<el-col :xs="24" :md="12">
|
||||||
|
<div class="chart-card">
|
||||||
|
<div class="chart-header">
|
||||||
|
<span class="chart-title"><i class="el-icon-pie-chart"></i> 合同状态分布</span>
|
||||||
|
</div>
|
||||||
|
<div ref="statusPieChart" class="chart-box"></div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :md="12">
|
||||||
|
<div class="chart-card">
|
||||||
|
<div class="chart-header">
|
||||||
|
<span class="chart-title"><i class="el-icon-s-marketing"></i> 销售员挂接钢卷TOP10</span>
|
||||||
|
</div>
|
||||||
|
<div ref="salesmanBarChart" class="chart-box"></div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="16" class="charts-row">
|
||||||
|
<el-col :span="24">
|
||||||
|
<div class="chart-card">
|
||||||
|
<div class="chart-header">
|
||||||
|
<span class="chart-title"><i class="el-icon-data-line"></i> 合同挂接钢卷数排行 TOP10</span>
|
||||||
|
</div>
|
||||||
|
<div ref="topContractChart" class="chart-box chart-box-bar"></div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<div class="table-card">
|
||||||
|
<div class="table-header">
|
||||||
|
<span class="table-title"><i class="el-icon-tickets"></i> 详细数据</span>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" style="float:right"></right-toolbar>
|
||||||
|
</div>
|
||||||
|
<el-table v-loading="tableLoading" :data="tableData" border style="width: 100%"
|
||||||
|
:row-class-name="tableRowClassName">
|
||||||
|
<el-table-column type="index" label="序号" width="55" align="center" />
|
||||||
|
<el-table-column label="合同号" align="center" prop="contractCode" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column label="合同名称" align="center" prop="contractName" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="订单编号" align="center" prop="orderCode" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column label="需方" align="center" prop="customer" min-width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="销售员" align="center" prop="salesman" width="100" />
|
||||||
|
<el-table-column label="合同状态" align="center" prop="status" width="90">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag v-if="scope.row.status === 0" type="info" size="small">草稿</el-tag>
|
||||||
|
<el-tag v-else-if="scope.row.status === 1" type="success" size="small">生效</el-tag>
|
||||||
|
<el-tag v-else-if="scope.row.status === 2" type="danger" size="small">作废</el-tag>
|
||||||
|
<el-tag v-else-if="scope.row.status === 3" type="warning" size="small">已完成</el-tag>
|
||||||
|
<span v-else>{{ scope.row.status }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="签订时间" align="center" prop="signTime" width="110">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.signTime ? parseTime(scope.row.signTime, '{y}-{m}-{d}') : '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="订单总额" align="center" prop="orderAmount" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.orderAmount ? formatMoney(scope.row.orderAmount) : '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="挂接钢卷数" align="center" prop="coilCount" width="110" sortable>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.coilCount > 0 ? 'success' : 'info'" size="small">
|
||||||
|
{{ scope.row.coilCount }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="总净重(t)" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.totalNetWeight != null ? (Number(scope.row.totalNetWeight) / 1000).toFixed(2) : '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="总毛重(t)" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.totalGrossWeight != null ? (Number(scope.row.totalGrossWeight) / 1000).toFixed(2) : '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||||
|
@pagination="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getContractCoilStatistics } from "@/api/wms/contractCoilStatistics";
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ContractCoilStatistics",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
tableLoading: false,
|
||||||
|
showSearch: true,
|
||||||
|
total: 0,
|
||||||
|
tableData: [],
|
||||||
|
allData: [],
|
||||||
|
summary: {
|
||||||
|
totalContracts: 0,
|
||||||
|
attachedContracts: 0,
|
||||||
|
unattachedContracts: 0,
|
||||||
|
totalCoils: 0,
|
||||||
|
totalWeight: 0
|
||||||
|
},
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
contractCode: undefined,
|
||||||
|
contractName: undefined,
|
||||||
|
orderCode: undefined,
|
||||||
|
customer: undefined,
|
||||||
|
salesman: undefined,
|
||||||
|
status: undefined
|
||||||
|
},
|
||||||
|
// ECharts 实例
|
||||||
|
statusPieChartInst: null,
|
||||||
|
salesmanBarChartInst: null,
|
||||||
|
topContractChartInst: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
attachmentRate() {
|
||||||
|
if (this.summary.totalContracts === 0) return 0;
|
||||||
|
return ((this.summary.attachedContracts / this.summary.totalContracts) * 100).toFixed(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.initCharts();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.disposeCharts();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 查询数据 */
|
||||||
|
getList() {
|
||||||
|
this.loading = true;
|
||||||
|
this.tableLoading = true;
|
||||||
|
getContractCoilStatistics(this.queryParams).then(response => {
|
||||||
|
const list = response.data || [];
|
||||||
|
this.allData = list;
|
||||||
|
this.total = list.length;
|
||||||
|
// 汇总
|
||||||
|
this.summary.totalContracts = list.length;
|
||||||
|
this.summary.attachedContracts = list.filter(item => item.coilCount > 0).length;
|
||||||
|
this.summary.unattachedContracts = list.filter(item => item.coilCount === 0).length;
|
||||||
|
this.summary.totalCoils = list.reduce((sum, item) => sum + (Number(item.coilCount) || 0), 0);
|
||||||
|
const totalNetKg = list.reduce((sum, item) => sum + (Number(item.totalNetWeight) || 0), 0);
|
||||||
|
this.summary.totalWeight = (totalNetKg / 1000).toFixed(2);
|
||||||
|
// 切片
|
||||||
|
this.sliceTableData();
|
||||||
|
this.loading = false;
|
||||||
|
this.tableLoading = false;
|
||||||
|
// 渲染图表
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.renderCharts();
|
||||||
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
this.loading = false;
|
||||||
|
this.tableLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
sliceTableData() {
|
||||||
|
const { pageNum, pageSize } = this.queryParams;
|
||||||
|
const start = (pageNum - 1) * pageSize;
|
||||||
|
this.tableData = this.allData.slice(start, start + pageSize);
|
||||||
|
},
|
||||||
|
handlePageChange() {
|
||||||
|
this.sliceTableData();
|
||||||
|
},
|
||||||
|
handleQuery() {
|
||||||
|
this.queryParams.pageNum = 1;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.resetForm("queryForm");
|
||||||
|
this.handleQuery();
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
this.download('wms/coilContractRel/statistics/export', {
|
||||||
|
...this.queryParams
|
||||||
|
}, `合同挂接情况统计_${new Date().getTime()}.xlsx`)
|
||||||
|
},
|
||||||
|
formatMoney(val) {
|
||||||
|
if (!val) return '-';
|
||||||
|
return (val / 10000).toFixed(2) + '万';
|
||||||
|
},
|
||||||
|
tableRowClassName({ row }) {
|
||||||
|
if (row.coilCount === 0) return 'row-unattached';
|
||||||
|
if (row.coilCount >= 10) return 'row-high';
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== ECharts 图表 ==========
|
||||||
|
initCharts() {
|
||||||
|
if (this.$refs.statusPieChart) {
|
||||||
|
this.statusPieChartInst = echarts.init(this.$refs.statusPieChart);
|
||||||
|
}
|
||||||
|
if (this.$refs.salesmanBarChart) {
|
||||||
|
this.salesmanBarChartInst = echarts.init(this.$refs.salesmanBarChart);
|
||||||
|
}
|
||||||
|
if (this.$refs.topContractChart) {
|
||||||
|
this.topContractChartInst = echarts.init(this.$refs.topContractChart);
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', this.resizeCharts);
|
||||||
|
this.renderCharts();
|
||||||
|
},
|
||||||
|
disposeCharts() {
|
||||||
|
window.removeEventListener('resize', this.resizeCharts);
|
||||||
|
if (this.statusPieChartInst) { this.statusPieChartInst.dispose(); }
|
||||||
|
if (this.salesmanBarChartInst) { this.salesmanBarChartInst.dispose(); }
|
||||||
|
if (this.topContractChartInst) { this.topContractChartInst.dispose(); }
|
||||||
|
},
|
||||||
|
resizeCharts() {
|
||||||
|
if (this.statusPieChartInst) this.statusPieChartInst.resize();
|
||||||
|
if (this.salesmanBarChartInst) this.salesmanBarChartInst.resize();
|
||||||
|
if (this.topContractChartInst) this.topContractChartInst.resize();
|
||||||
|
},
|
||||||
|
renderCharts() {
|
||||||
|
this.renderStatusPie();
|
||||||
|
this.renderSalesmanBar();
|
||||||
|
this.renderTopContractBar();
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 合同状态分布饼图 */
|
||||||
|
renderStatusPie() {
|
||||||
|
if (!this.statusPieChartInst || !this.allData.length) return;
|
||||||
|
const statusMap = { 0: '草稿', 1: '生效', 2: '作废', 3: '已完成' };
|
||||||
|
const statusColorMap = { 0: '#909399', 1: '#67c23a', 2: '#f56c6c', 3: '#e6a23c' };
|
||||||
|
const countMap = {};
|
||||||
|
this.allData.forEach(item => {
|
||||||
|
const k = item.status != null ? item.status : -1;
|
||||||
|
countMap[k] = (countMap[k] || 0) + 1;
|
||||||
|
});
|
||||||
|
const data = Object.keys(countMap).map(k => ({
|
||||||
|
name: statusMap[k] || ('未知(' + k + ')'),
|
||||||
|
value: countMap[k],
|
||||||
|
itemStyle: { color: statusColorMap[k] || '#c0c4cc' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.statusPieChartInst.setOption({
|
||||||
|
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||||||
|
legend: { bottom: 0, textStyle: { color: '#606266' } },
|
||||||
|
series: [{
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['45%', '72%'],
|
||||||
|
center: ['50%', '48%'],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
itemStyle: { borderRadius: 4, borderColor: '#fff', borderWidth: 2 },
|
||||||
|
label: { show: true, formatter: '{b}\n{d}%' },
|
||||||
|
emphasis: {
|
||||||
|
label: { fontSize: 16, fontWeight: 'bold' },
|
||||||
|
scaleSize: 8
|
||||||
|
},
|
||||||
|
data
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 销售员挂接钢卷 TOP10 横向柱状图 */
|
||||||
|
renderSalesmanBar() {
|
||||||
|
if (!this.salesmanBarChartInst || !this.allData.length) return;
|
||||||
|
// 按销售员聚合
|
||||||
|
const map = {};
|
||||||
|
this.allData.forEach(item => {
|
||||||
|
const name = item.salesman || '未知';
|
||||||
|
map[name] = (map[name] || 0) + (item.coilCount || 0);
|
||||||
|
});
|
||||||
|
let arr = Object.entries(map).map(([name, count]) => ({ name, count }));
|
||||||
|
arr.sort((a, b) => b.count - a.count);
|
||||||
|
arr = arr.slice(0, 10);
|
||||||
|
|
||||||
|
this.salesmanBarChartInst.setOption({
|
||||||
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||||
|
grid: { left: '3%', right: '10%', bottom: '3%', top: '5%', containLabel: true },
|
||||||
|
xAxis: { type: 'value', name: '钢卷数' },
|
||||||
|
yAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: arr.map(i => i.name).reverse(),
|
||||||
|
axisLabel: { width: 60, overflow: 'truncate' }
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
type: 'bar',
|
||||||
|
data: arr.map(i => ({
|
||||||
|
value: i.count,
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||||
|
{ offset: 0, color: '#6dd5ed' },
|
||||||
|
{ offset: 1, color: '#2193b0' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})).reverse(),
|
||||||
|
barWidth: 16,
|
||||||
|
itemStyle: { borderRadius: [0, 8, 8, 0] },
|
||||||
|
label: { show: true, position: 'right', color: '#303133' }
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 合同挂接钢卷排行 TOP10 */
|
||||||
|
renderTopContractBar() {
|
||||||
|
if (!this.topContractChartInst || !this.allData.length) return;
|
||||||
|
let arr = this.allData
|
||||||
|
.filter(item => item.coilCount > 0)
|
||||||
|
.sort((a, b) => b.coilCount - a.coilCount)
|
||||||
|
.slice(0, 10);
|
||||||
|
|
||||||
|
const xData = arr.map(i => i.contractCode || i.orderCode || '未知');
|
||||||
|
const yData = arr.map(i => i.coilCount);
|
||||||
|
|
||||||
|
this.topContractChartInst.setOption({
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: { type: 'shadow' },
|
||||||
|
formatter: function(params) {
|
||||||
|
const d = arr[params[0].dataIndex];
|
||||||
|
return `<b>${d.contractCode || d.orderCode}</b><br/>
|
||||||
|
合同名称: ${d.contractName || '-'}<br/>
|
||||||
|
需方: ${d.customer || '-'}<br/>
|
||||||
|
挂接钢卷: <b>${d.coilCount}</b> 卷<br/>
|
||||||
|
总净重: ${(Number(d.totalNetWeight) / 1000).toFixed(2)}t`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: { left: '3%', right: '4%', bottom: '12%', top: '8%', containLabel: true },
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: xData,
|
||||||
|
axisLabel: { rotate: 30, fontSize: 11, width: 80, overflow: 'truncate' }
|
||||||
|
},
|
||||||
|
yAxis: { type: 'value', name: '钢卷数' },
|
||||||
|
series: [{
|
||||||
|
type: 'bar',
|
||||||
|
data: yData.map((v, idx) => ({
|
||||||
|
value: v,
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#f093fb' },
|
||||||
|
{ offset: 1, color: '#f5576c' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
barWidth: 28,
|
||||||
|
itemStyle: { borderRadius: [8, 8, 0, 0] },
|
||||||
|
label: { show: true, position: 'top', color: '#303133', fontWeight: 'bold' }
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.statistics-container {
|
||||||
|
padding: 16px;
|
||||||
|
background: #f0f2f5;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 筛选面板 */
|
||||||
|
.filter-panel {
|
||||||
|
background: #fff;
|
||||||
|
padding: 16px 20px 4px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计卡片 */
|
||||||
|
.stat-cards-row {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 20px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
width: 54px;
|
||||||
|
height: 54px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.card-icon i {
|
||||||
|
font-size: 26px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-blue .card-icon { background: linear-gradient(135deg, #409eff, #337ecc); }
|
||||||
|
.card-green .card-icon { background: linear-gradient(135deg, #67c23a, #529b2e); }
|
||||||
|
.card-orange .card-icon { background: linear-gradient(135deg, #e6a23c, #cf8b2b); }
|
||||||
|
.card-red .card-icon { background: linear-gradient(135deg, #f56c6c, #da4a4a); }
|
||||||
|
|
||||||
|
.card-info { flex: 1; min-width: 0; }
|
||||||
|
.card-value { font-size: 30px; font-weight: 700; color: #303133; line-height: 1.2; }
|
||||||
|
.card-label { font-size: 13px; color: #909399; margin-top: 4px; }
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: #fafafa;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图表卡片 */
|
||||||
|
.charts-row {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header {
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.chart-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
.chart-title i {
|
||||||
|
margin-right: 4px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 320px;
|
||||||
|
}
|
||||||
|
.chart-box-bar {
|
||||||
|
height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格卡片 */
|
||||||
|
.table-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.table-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
.table-title i {
|
||||||
|
margin-right: 4px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格行样式 */
|
||||||
|
::v-deep .el-table .row-unattached {
|
||||||
|
background-color: #fef0f0 !important;
|
||||||
|
}
|
||||||
|
::v-deep .el-table .row-high {
|
||||||
|
background-color: #f0f9eb !important;
|
||||||
|
}
|
||||||
|
::v-deep .el-table .row-unattached:hover > td {
|
||||||
|
background-color: #fde2e2 !important;
|
||||||
|
}
|
||||||
|
::v-deep .el-table .row-high:hover > td {
|
||||||
|
background-color: #e1f3d8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chart-box { height: 260px; }
|
||||||
|
.chart-box-bar { height: 300px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -18,7 +18,9 @@ import com.klp.common.core.validate.EditGroup;
|
|||||||
import com.klp.common.enums.BusinessType;
|
import com.klp.common.enums.BusinessType;
|
||||||
import com.klp.common.utils.poi.ExcelUtil;
|
import com.klp.common.utils.poi.ExcelUtil;
|
||||||
import com.klp.domain.vo.WmsCoilContractRelVo;
|
import com.klp.domain.vo.WmsCoilContractRelVo;
|
||||||
|
import com.klp.domain.vo.ContractCoilStatisticsVo;
|
||||||
import com.klp.domain.bo.WmsCoilContractRelBo;
|
import com.klp.domain.bo.WmsCoilContractRelBo;
|
||||||
|
import com.klp.domain.bo.ContractCoilStatisticsBo;
|
||||||
import com.klp.service.IWmsCoilContractRelService;
|
import com.klp.service.IWmsCoilContractRelService;
|
||||||
import com.klp.common.core.page.TableDataInfo;
|
import com.klp.common.core.page.TableDataInfo;
|
||||||
|
|
||||||
@@ -97,6 +99,25 @@ public class WmsCoilContractRelController extends BaseController {
|
|||||||
return R.ok(updated);
|
return R.ok(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询合同挂接情况统计
|
||||||
|
*/
|
||||||
|
@GetMapping("/statistics")
|
||||||
|
public R<List<ContractCoilStatisticsVo>> statistics(ContractCoilStatisticsBo bo) {
|
||||||
|
List<ContractCoilStatisticsVo> list = iWmsCoilContractRelService.queryContractCoilStatistics(bo);
|
||||||
|
return R.ok(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出合同挂接情况统计
|
||||||
|
*/
|
||||||
|
@Log(title = "合同挂接情况统计", businessType = BusinessType.EXPORT)
|
||||||
|
@PostMapping("/statistics/export")
|
||||||
|
public void statisticsExport(ContractCoilStatisticsBo bo, HttpServletResponse response) {
|
||||||
|
List<ContractCoilStatisticsVo> list = iWmsCoilContractRelService.queryContractCoilStatistics(bo);
|
||||||
|
ExcelUtil.exportExcel(list, "合同挂接情况统计", ContractCoilStatisticsVo.class, response);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除钢卷与合同关联关系
|
* 删除钢卷与合同关联关系
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.klp.domain.bo;
|
||||||
|
|
||||||
|
import com.klp.common.core.domain.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合同挂接情况统计查询对象
|
||||||
|
*
|
||||||
|
* @author klp
|
||||||
|
* @date 2026-06-30
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ContractCoilStatisticsBo extends BaseEntity {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 合同号 */
|
||||||
|
private String contractCode;
|
||||||
|
|
||||||
|
/** 合同名称 */
|
||||||
|
private String contractName;
|
||||||
|
|
||||||
|
/** 需方(客户) */
|
||||||
|
private String customer;
|
||||||
|
|
||||||
|
/** 销售员 */
|
||||||
|
private String salesman;
|
||||||
|
|
||||||
|
/** 合同状态 0=草稿 1=生效 2=作废 3=已完成 */
|
||||||
|
private Long status;
|
||||||
|
|
||||||
|
/** 订单编号 */
|
||||||
|
private String orderCode;
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package com.klp.domain.vo;
|
||||||
|
|
||||||
|
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||||
|
import com.alibaba.excel.annotation.ExcelProperty;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合同挂接情况统计视图对象
|
||||||
|
* 基于 crm_order + wms_coil_contract_rel + wms_material_coil 三表联合统计
|
||||||
|
*
|
||||||
|
* @author klp
|
||||||
|
* @date 2026-06-30
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@ExcelIgnoreUnannotated
|
||||||
|
public class ContractCoilStatisticsVo {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
// ========== 合同信息(来自 crm_order)==========
|
||||||
|
|
||||||
|
/** 订单ID */
|
||||||
|
@ExcelProperty(value = "订单ID")
|
||||||
|
private Long orderId;
|
||||||
|
|
||||||
|
/** 订单编号 */
|
||||||
|
@ExcelProperty(value = "订单编号")
|
||||||
|
private String orderCode;
|
||||||
|
|
||||||
|
/** 合同号 */
|
||||||
|
@ExcelProperty(value = "合同号")
|
||||||
|
private String contractCode;
|
||||||
|
|
||||||
|
/** 合同名称 */
|
||||||
|
@ExcelProperty(value = "合同名称")
|
||||||
|
private String contractName;
|
||||||
|
|
||||||
|
/** 需方(客户) */
|
||||||
|
@ExcelProperty(value = "需方")
|
||||||
|
private String customer;
|
||||||
|
|
||||||
|
/** 供方 */
|
||||||
|
@ExcelProperty(value = "供方")
|
||||||
|
private String supplier;
|
||||||
|
|
||||||
|
/** 销售员 */
|
||||||
|
@ExcelProperty(value = "销售员")
|
||||||
|
private String salesman;
|
||||||
|
|
||||||
|
/** 合同状态 0=草稿 1=生效 2=作废 3=已完成 */
|
||||||
|
@ExcelProperty(value = "合同状态")
|
||||||
|
private Long status;
|
||||||
|
|
||||||
|
/** 订单总额 */
|
||||||
|
@ExcelProperty(value = "订单总额")
|
||||||
|
private BigDecimal orderAmount;
|
||||||
|
|
||||||
|
/** 签订时间 */
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@ExcelProperty(value = "签订时间")
|
||||||
|
private Date signTime;
|
||||||
|
|
||||||
|
/** 交货日期 */
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@ExcelProperty(value = "交货日期")
|
||||||
|
private Date deliveryDate;
|
||||||
|
|
||||||
|
// ========== 统计字段 ==========
|
||||||
|
|
||||||
|
/** 挂接钢卷数量 */
|
||||||
|
@ExcelProperty(value = "挂接钢卷数")
|
||||||
|
private Long coilCount;
|
||||||
|
|
||||||
|
/** 钢卷总净重(kg) */
|
||||||
|
@ExcelProperty(value = "钢卷总净重(kg)")
|
||||||
|
private BigDecimal totalNetWeight;
|
||||||
|
|
||||||
|
/** 钢卷总毛重(kg) */
|
||||||
|
@ExcelProperty(value = "钢卷总毛重(kg)")
|
||||||
|
private BigDecimal totalGrossWeight;
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ package com.klp.mapper;
|
|||||||
|
|
||||||
import com.klp.domain.WmsCoilContractRel;
|
import com.klp.domain.WmsCoilContractRel;
|
||||||
import com.klp.domain.vo.WmsCoilContractRelVo;
|
import com.klp.domain.vo.WmsCoilContractRelVo;
|
||||||
|
import com.klp.domain.vo.ContractCoilStatisticsVo;
|
||||||
|
import com.klp.domain.bo.ContractCoilStatisticsBo;
|
||||||
import com.klp.common.core.mapper.BaseMapperPlus;
|
import com.klp.common.core.mapper.BaseMapperPlus;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
@@ -34,4 +36,9 @@ public interface WmsCoilContractRelMapper extends BaseMapperPlus<WmsCoilContract
|
|||||||
* 根据订单ID查询合同信息(业务员姓名 + 合同编号)
|
* 根据订单ID查询合同信息(业务员姓名 + 合同编号)
|
||||||
*/
|
*/
|
||||||
java.util.Map<String, String> selectContractInfoByOrderId(@Param("orderId") Long orderId);
|
java.util.Map<String, String> selectContractInfoByOrderId(@Param("orderId") Long orderId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询合同挂接钢卷统计(按合同分组统计钢卷数量和重量)
|
||||||
|
*/
|
||||||
|
List<ContractCoilStatisticsVo> selectContractCoilStatistics(@Param("bo") ContractCoilStatisticsBo bo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package com.klp.service;
|
|||||||
|
|
||||||
import com.klp.domain.WmsCoilContractRel;
|
import com.klp.domain.WmsCoilContractRel;
|
||||||
import com.klp.domain.vo.WmsCoilContractRelVo;
|
import com.klp.domain.vo.WmsCoilContractRelVo;
|
||||||
|
import com.klp.domain.vo.ContractCoilStatisticsVo;
|
||||||
import com.klp.domain.bo.WmsCoilContractRelBo;
|
import com.klp.domain.bo.WmsCoilContractRelBo;
|
||||||
|
import com.klp.domain.bo.ContractCoilStatisticsBo;
|
||||||
import com.klp.common.core.page.TableDataInfo;
|
import com.klp.common.core.page.TableDataInfo;
|
||||||
import com.klp.common.core.domain.PageQuery;
|
import com.klp.common.core.domain.PageQuery;
|
||||||
|
|
||||||
@@ -55,4 +57,12 @@ public interface IWmsCoilContractRelService {
|
|||||||
* @return 实际更新的记录数
|
* @return 实际更新的记录数
|
||||||
*/
|
*/
|
||||||
int batchUpdateContractId(Long contractId, List<Long> coilIds);
|
int batchUpdateContractId(Long contractId, List<Long> coilIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询合同挂接钢卷统计
|
||||||
|
*
|
||||||
|
* @param bo 查询参数
|
||||||
|
* @return 统计列表
|
||||||
|
*/
|
||||||
|
List<ContractCoilStatisticsVo> queryContractCoilStatistics(ContractCoilStatisticsBo bo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import com.klp.common.utils.StringUtils;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import com.klp.domain.bo.WmsCoilContractRelBo;
|
import com.klp.domain.bo.WmsCoilContractRelBo;
|
||||||
|
import com.klp.domain.bo.ContractCoilStatisticsBo;
|
||||||
import com.klp.domain.vo.WmsCoilContractRelVo;
|
import com.klp.domain.vo.WmsCoilContractRelVo;
|
||||||
|
import com.klp.domain.vo.ContractCoilStatisticsVo;
|
||||||
import com.klp.domain.WmsCoilContractRel;
|
import com.klp.domain.WmsCoilContractRel;
|
||||||
import com.klp.domain.WmsMaterialCoil;
|
import com.klp.domain.WmsMaterialCoil;
|
||||||
import com.klp.mapper.WmsCoilContractRelMapper;
|
import com.klp.mapper.WmsCoilContractRelMapper;
|
||||||
@@ -195,4 +197,12 @@ public class WmsCoilContractRelServiceImpl implements IWmsCoilContractRelService
|
|||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询合同挂接钢卷统计
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<ContractCoilStatisticsVo> queryContractCoilStatistics(ContractCoilStatisticsBo bo) {
|
||||||
|
return baseMapper.selectContractCoilStatistics(bo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,4 +154,49 @@
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 合同挂接钢卷统计:LEFT JOIN 合同 + 关联表 + 钢卷,按合同分组统计 -->
|
||||||
|
<select id="selectContractCoilStatistics" resultType="com.klp.domain.vo.ContractCoilStatisticsVo">
|
||||||
|
SELECT
|
||||||
|
co.order_id AS orderId,
|
||||||
|
co.order_code AS orderCode,
|
||||||
|
co.contract_code AS contractCode,
|
||||||
|
co.contract_name AS contractName,
|
||||||
|
co.customer,
|
||||||
|
co.supplier,
|
||||||
|
co.salesman,
|
||||||
|
co.status,
|
||||||
|
co.order_amount AS orderAmount,
|
||||||
|
co.sign_time AS signTime,
|
||||||
|
co.delivery_date AS deliveryDate,
|
||||||
|
COUNT(rel.coil_id) AS coilCount,
|
||||||
|
IFNULL(SUM(mc.net_weight), 0) AS totalNetWeight,
|
||||||
|
IFNULL(SUM(mc.gross_weight), 0) AS totalGrossWeight
|
||||||
|
FROM crm_order co
|
||||||
|
LEFT JOIN wms_coil_contract_rel rel ON co.order_id = rel.contract_id AND rel.del_flag = 0
|
||||||
|
LEFT JOIN wms_material_coil mc ON rel.coil_id = mc.coil_id AND mc.del_flag = 0
|
||||||
|
WHERE co.del_flag = 0
|
||||||
|
<if test="bo.contractCode != null and bo.contractCode != ''">
|
||||||
|
AND co.contract_code LIKE CONCAT('%', #{bo.contractCode}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="bo.contractName != null and bo.contractName != ''">
|
||||||
|
AND co.contract_name LIKE CONCAT('%', #{bo.contractName}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="bo.customer != null and bo.customer != ''">
|
||||||
|
AND co.customer LIKE CONCAT('%', #{bo.customer}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="bo.salesman != null and bo.salesman != ''">
|
||||||
|
AND co.salesman LIKE CONCAT('%', #{bo.salesman}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="bo.status != null">
|
||||||
|
AND co.status = #{bo.status}
|
||||||
|
</if>
|
||||||
|
<if test="bo.orderCode != null and bo.orderCode != ''">
|
||||||
|
AND co.order_code LIKE CONCAT('%', #{bo.orderCode}, '%')
|
||||||
|
</if>
|
||||||
|
GROUP BY co.order_id, co.order_code, co.contract_code, co.contract_name,
|
||||||
|
co.customer, co.supplier, co.salesman, co.status,
|
||||||
|
co.order_amount, co.sign_time, co.delivery_date
|
||||||
|
ORDER BY coilCount DESC, co.order_id ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
Reference in New Issue
Block a user