|
|
@@ -1,24 +1,357 @@
|
|
|
<template>
|
|
|
<div class="app-container home">
|
|
|
- <!-- <el-divider /> -->
|
|
|
- <div class="index-style">
|
|
|
- <div class="typewriter-container">
|
|
|
- <span v-for="(char, index) in 'welcome!'" :key="index" :style="{ animationDelay: `${index * 0.5}s` }" class="typewriter-char">{{
|
|
|
- char
|
|
|
- }}</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <el-row :gutter="12">
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="kpi-stack">
|
|
|
+ <div class="kpi-card kpi-blue">
|
|
|
+ <div class="kpi-title">供应商总数</div>
|
|
|
+ <div class="kpi-value">{{ stats.totalSuppliers }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="kpi-card kpi-red">
|
|
|
+ <div class="kpi-title">不合作供应商</div>
|
|
|
+ <div class="kpi-value">{{ stats.nonCooperativeSuppliers }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="kpi-card kpi-orange">
|
|
|
+ <div class="kpi-title">待审核供应商</div>
|
|
|
+ <div class="kpi-value">{{ stats.pendingSuppliers }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="kpi-card kpi-purple">
|
|
|
+ <div class="kpi-title">协议供货单数</div>
|
|
|
+ <div class="kpi-value">{{ stats.contractSupplies }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="kpi-card kpi-teal">
|
|
|
+ <div class="kpi-title">协议供货商品数</div>
|
|
|
+ <div class="kpi-value">{{ stats.contractSupplyProducts }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="kpi-card kpi-green">
|
|
|
+ <div class="kpi-title">合同总数</div>
|
|
|
+ <div class="kpi-value">{{ stats.contractTotal }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="kpi-card kpi-indigo">
|
|
|
+ <div class="kpi-title">商品总数</div>
|
|
|
+ <div class="kpi-value">{{ stats.productTotal }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="kpi-card kpi-gray">
|
|
|
+ <div class="kpi-title">品牌数量</div>
|
|
|
+ <div class="kpi-value">{{ stats.brandTotal }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <el-col :span="18">
|
|
|
+ <el-row :gutter="12">
|
|
|
+ <el-col :span="10">
|
|
|
+ <el-card shadow="hover" class="chart-card">
|
|
|
+ <div class="chart-title">供应商结构</div>
|
|
|
+ <div ref="pieRef" class="chart-box"></div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="14">
|
|
|
+ <el-card shadow="hover" class="chart-card">
|
|
|
+ <div class="chart-title">核心指标</div>
|
|
|
+ <div ref="barRef" class="chart-box"></div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-row :gutter="12" style="margin-top: 12px;">
|
|
|
+ <el-col :span="24">
|
|
|
+ <el-card shadow="hover" class="chart-card">
|
|
|
+ <div class="chart-title">近七日新增趋势</div>
|
|
|
+ <div ref="lineRef" class="chart-box"></div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup name="Index" lang="ts">
|
|
|
-const goTarget = (url: string) => {
|
|
|
- window.open(url, '__blank');
|
|
|
+import { onMounted, onBeforeUnmount, reactive, ref, nextTick } from 'vue';
|
|
|
+import * as echarts from 'echarts';
|
|
|
+import { getInfoList, getApproveList } from '@/api/customer/info';
|
|
|
+import { listContractsupply } from '@/api/supplier/contractsupply';
|
|
|
+import { scmListContracproduct } from '@/api/product/contracproduct';
|
|
|
+import { listContact } from '@/api/supplier/contact';
|
|
|
+import { listContract } from '@/api/supplier/contract';
|
|
|
+import { listBase } from '@/api/product/base';
|
|
|
+import { listBrand } from '@/api/product/brand';
|
|
|
+
|
|
|
+const pieRef = ref();
|
|
|
+const barRef = ref();
|
|
|
+const lineRef = ref();
|
|
|
+let pieInstance: echarts.ECharts | null = null;
|
|
|
+let barInstance: echarts.ECharts | null = null;
|
|
|
+let lineInstance: echarts.ECharts | null = null;
|
|
|
+
|
|
|
+const stats = reactive({
|
|
|
+ totalSuppliers: 0,
|
|
|
+ nonCooperativeSuppliers: 0,
|
|
|
+ pendingSuppliers: 0,
|
|
|
+ contractSupplies: 0,
|
|
|
+ contractSupplyProducts: 0,
|
|
|
+ contractTotal: 0,
|
|
|
+ productTotal: 0,
|
|
|
+ brandTotal: 0
|
|
|
+});
|
|
|
+
|
|
|
+const trend = reactive({
|
|
|
+ days: [] as string[],
|
|
|
+ contactAdds: [] as number[],
|
|
|
+ contractAdds: [] as number[]
|
|
|
+});
|
|
|
+
|
|
|
+const formatDay = (d: Date) => {
|
|
|
+ const y = d.getFullYear();
|
|
|
+ const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
|
+ const day = String(d.getDate()).padStart(2, '0');
|
|
|
+ return `${y}-${m}-${day}`;
|
|
|
+};
|
|
|
+
|
|
|
+const buildLast7Days = () => {
|
|
|
+ const days: string[] = [];
|
|
|
+ const today = new Date();
|
|
|
+ for (let i = 6; i >= 0; i--) {
|
|
|
+ const d = new Date(today);
|
|
|
+ d.setDate(today.getDate() - i);
|
|
|
+ days.push(formatDay(d));
|
|
|
+ }
|
|
|
+ return days;
|
|
|
+};
|
|
|
+
|
|
|
+const fetchTrend = async () => {
|
|
|
+ const days = buildLast7Days();
|
|
|
+ trend.days = days;
|
|
|
+
|
|
|
+ const contactReqs = days.map((day) => {
|
|
|
+ return listContact({
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 1,
|
|
|
+ params: {
|
|
|
+ beginTime: `${day} 00:00:00`,
|
|
|
+ endTime: `${day} 23:59:59`
|
|
|
+ }
|
|
|
+ } as any);
|
|
|
+ });
|
|
|
+
|
|
|
+ const contractReqs = days.map((day) => {
|
|
|
+ return listContract({
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 1,
|
|
|
+ params: {
|
|
|
+ beginTime: `${day} 00:00:00`,
|
|
|
+ endTime: `${day} 23:59:59`
|
|
|
+ }
|
|
|
+ } as any);
|
|
|
+ });
|
|
|
+
|
|
|
+ const [contactRes, contractRes] = await Promise.all([
|
|
|
+ Promise.all(contactReqs),
|
|
|
+ Promise.all(contractReqs)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ trend.contactAdds = contactRes.map((r: any) => r?.total ?? 0);
|
|
|
+ trend.contractAdds = contractRes.map((r: any) => r?.total ?? 0);
|
|
|
+};
|
|
|
+
|
|
|
+const fetchStats = async () => {
|
|
|
+ const [supplierRes, nonCoopRes, pendingRes, contractRes, productRes, contractTotalRes, productTotalRes, brandTotalRes] = await Promise.all([
|
|
|
+ getInfoList({ pageNum: 1, pageSize: 1 } as any),
|
|
|
+ getInfoList({ pageNum: 1, pageSize: 1, cooperative: 0 } as any),
|
|
|
+ getApproveList({ pageNum: 1, pageSize: 1 } as any),
|
|
|
+ listContractsupply({ pageNum: 1, pageSize: 1 } as any),
|
|
|
+ scmListContracproduct({ pageNum: 1, pageSize: 1 } as any),
|
|
|
+ listContract({ pageNum: 1, pageSize: 1 } as any),
|
|
|
+ listBase({ pageNum: 1, pageSize: 1 } as any),
|
|
|
+ listBrand({ pageNum: 1, pageSize: 1 } as any)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ stats.totalSuppliers = supplierRes?.total ?? 0;
|
|
|
+ stats.nonCooperativeSuppliers = nonCoopRes?.total ?? 0;
|
|
|
+ stats.pendingSuppliers = pendingRes?.total ?? 0;
|
|
|
+ stats.contractSupplies = contractRes?.total ?? 0;
|
|
|
+ stats.contractSupplyProducts = productRes?.total ?? 0;
|
|
|
+ stats.contractTotal = contractTotalRes?.total ?? 0;
|
|
|
+ stats.productTotal = productTotalRes?.total ?? 0;
|
|
|
+ stats.brandTotal = brandTotalRes?.total ?? 0;
|
|
|
};
|
|
|
+
|
|
|
+const renderCharts = () => {
|
|
|
+ if (pieRef.value) {
|
|
|
+ pieInstance?.dispose();
|
|
|
+ pieInstance = echarts.init(pieRef.value, 'macarons');
|
|
|
+ pieInstance.setOption({
|
|
|
+ tooltip: { trigger: 'item' },
|
|
|
+ legend: { bottom: 0, left: 'center' },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ type: 'pie',
|
|
|
+ radius: ['55%', '75%'],
|
|
|
+ center: ['50%', '45%'],
|
|
|
+ avoidLabelOverlap: true,
|
|
|
+ label: { show: false },
|
|
|
+ labelLine: { show: false },
|
|
|
+ data: [
|
|
|
+ { name: '不合作', value: stats.nonCooperativeSuppliers },
|
|
|
+ { name: '待审核', value: stats.pendingSuppliers },
|
|
|
+ {
|
|
|
+ name: '其他',
|
|
|
+ value: Math.max(0, stats.totalSuppliers - stats.nonCooperativeSuppliers - stats.pendingSuppliers)
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (barRef.value) {
|
|
|
+ barInstance?.dispose();
|
|
|
+ barInstance = echarts.init(barRef.value, 'macarons');
|
|
|
+ barInstance.setOption({
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
+ grid: { left: 40, right: 20, top: 20, bottom: 30 },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: ['供应商', '不合作', '待审核', '供货单', '供货商品', '合同', '商品', '品牌'],
|
|
|
+ axisTick: { show: false }
|
|
|
+ },
|
|
|
+ yAxis: { type: 'value', splitLine: { lineStyle: { type: 'dashed' } } },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ type: 'bar',
|
|
|
+ data: [
|
|
|
+ stats.totalSuppliers,
|
|
|
+ stats.nonCooperativeSuppliers,
|
|
|
+ stats.pendingSuppliers,
|
|
|
+ stats.contractSupplies,
|
|
|
+ stats.contractSupplyProducts,
|
|
|
+ stats.contractTotal,
|
|
|
+ stats.productTotal,
|
|
|
+ stats.brandTotal
|
|
|
+ ],
|
|
|
+ barWidth: 22,
|
|
|
+ itemStyle: {
|
|
|
+ borderRadius: [6, 6, 0, 0],
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
+ { offset: 0, color: '#3b82f6' },
|
|
|
+ { offset: 1, color: '#60a5fa' }
|
|
|
+ ])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (lineRef.value) {
|
|
|
+ lineInstance?.dispose();
|
|
|
+ lineInstance = echarts.init(lineRef.value, 'macarons');
|
|
|
+ lineInstance.setOption({
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
+ legend: { top: 0, right: 10 },
|
|
|
+ grid: { left: 40, right: 20, top: 40, bottom: 30 },
|
|
|
+ xAxis: { type: 'category', data: trend.days },
|
|
|
+ yAxis: { type: 'value', splitLine: { lineStyle: { type: 'dashed' } } },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '联系人新增',
|
|
|
+ type: 'line',
|
|
|
+ data: trend.contactAdds,
|
|
|
+ smooth: true,
|
|
|
+ symbol: 'circle',
|
|
|
+ symbolSize: 6
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '合同新增',
|
|
|
+ type: 'line',
|
|
|
+ data: trend.contractAdds,
|
|
|
+ smooth: true,
|
|
|
+ symbol: 'circle',
|
|
|
+ symbolSize: 6
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const resizeChart = () => {
|
|
|
+ pieInstance?.resize();
|
|
|
+ barInstance?.resize();
|
|
|
+ lineInstance?.resize();
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ await Promise.all([fetchStats(), fetchTrend()]);
|
|
|
+ await nextTick();
|
|
|
+ renderCharts();
|
|
|
+ window.addEventListener('resize', resizeChart);
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ window.removeEventListener('resize', resizeChart);
|
|
|
+ pieInstance?.dispose();
|
|
|
+ pieInstance = null;
|
|
|
+ barInstance?.dispose();
|
|
|
+ barInstance = null;
|
|
|
+ lineInstance?.dispose();
|
|
|
+ lineInstance = null;
|
|
|
+});
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
.home {
|
|
|
+ .kpi-stack {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .kpi-card {
|
|
|
+ padding: 14px 16px;
|
|
|
+ border-radius: 12px;
|
|
|
+ color: #fff;
|
|
|
+ box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.25);
|
|
|
+ }
|
|
|
+
|
|
|
+ .kpi-title {
|
|
|
+ font-size: 13px;
|
|
|
+ opacity: 0.9;
|
|
|
+ }
|
|
|
+
|
|
|
+ .kpi-value {
|
|
|
+ margin-top: 8px;
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: 800;
|
|
|
+ line-height: 1;
|
|
|
+ letter-spacing: 0.5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .kpi-blue { background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%); }
|
|
|
+ .kpi-red { background: linear-gradient(135deg, #ef4444 0%, #fb7185 100%); }
|
|
|
+ .kpi-orange { background: linear-gradient(135deg, #f97316 0%, #fdba74 100%); }
|
|
|
+ .kpi-purple { background: linear-gradient(135deg, #7c3aed 0%, #c4b5fd 100%); }
|
|
|
+ .kpi-teal { background: linear-gradient(135deg, #0ea5e9 0%, #67e8f9 100%); }
|
|
|
+ .kpi-green { background: linear-gradient(135deg, #16a34a 0%, #86efac 100%); }
|
|
|
+ .kpi-indigo { background: linear-gradient(135deg, #4f46e5 0%, #a5b4fc 100%); }
|
|
|
+ .kpi-gray { background: linear-gradient(135deg, #334155 0%, #94a3b8 100%); }
|
|
|
+
|
|
|
+ .chart-card {
|
|
|
+ border-radius: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #303133;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-box {
|
|
|
+ width: 100%;
|
|
|
+ height: 320px;
|
|
|
+ }
|
|
|
+
|
|
|
blockquote {
|
|
|
padding: 10px 20px;
|
|
|
margin: 0 0 20px;
|