hurx il y a 9 heures
Parent
commit
45feb7cc05

+ 17 - 0
src/api/pc/enterprise/order.ts

@@ -321,3 +321,20 @@ export function customerOrderTradeData() {
     method: 'get'
   });
 }
+
+// 商品采购明细
+export function purchaseDetail() {
+  return request({
+    url: '/order/pcOrder/purchaseDetail',
+    method: 'get'
+  });
+}
+
+// 部门采购金额
+export function deptPurchase(query?: any) {
+  return request({
+    url: '/order/pcOrder/deptPurchase',
+    method: 'get',
+    params: query
+  });
+}

+ 2 - 2
src/layout/components/workbench.vue

@@ -6,10 +6,10 @@
         <img src="@/assets/images/layout/workbench.png" alt="" />
         <div>收起菜单</div>
       </div>
-      <div class="workbench-expand1 flex-row-start" @click="onPath('/indexDataDiy')">
+      <!-- <div class="workbench-expand1 flex-row-start" @click="onPath('/indexDataDiy')">
         <img src="@/assets/images/layout/workbench.png" alt="" />
         <div>首页</div>
-      </div>
+      </div> -->
       <!-- 修改点1: 增加 v-if="item1.show" 过滤无权限菜单 -->
       <template v-for="(item1, index1) in menuList" :key="index1">
         <div class="menu-list1" v-if="item1.show" @click="toggleMenu(item1.path)">

+ 251 - 41
src/views/analysis/deptPurchase/index.vue

@@ -2,13 +2,28 @@
   <div class="page-container">
     <PageTitle title="部门采购金额" />
 
-    <SearchBar :form="searchForm" :filters="filters" @search="handleSearch" @reset="handleReset">
-      <template #buttons>
-        <el-button type="danger">导出</el-button>
-      </template>
-    </SearchBar>
+    <div class="filter-bar">
+      <el-form :model="searchForm" inline>
+        <el-form-item label="下单人">
+          <el-select v-model="searchForm.contactId" placeholder="请选择下单人" clearable filterable @change="handleSearch">
+            <el-option v-for="item in contactOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch">搜索</el-button>
+          <el-button @click="handleReset">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
 
-    <el-table :data="tableData" border style="width: 100%">
+    <div class="chart-section">
+      <div class="chart-header">
+        <span class="chart-title">部门采购趋势</span>
+      </div>
+      <div class="chart-container" ref="chartRef"></div>
+    </div>
+
+    <el-table v-loading="loading" :data="tableData" border style="width: 100%">
       <el-table-column type="index" label="序号" width="60" align="center" />
       <el-table-column prop="deptName" label="部门名称" width="120" align="center" />
       <el-table-column prop="orderPerson" label="下单人" width="100" align="center" />
@@ -23,54 +38,249 @@
       </el-table-column>
     </el-table>
 
-    <TablePagination v-model:page="pagination.page" v-model:pageSize="pagination.pageSize" :total="pagination.total" />
+    <TablePagination v-model:page="pagination.page" v-model:pageSize="pagination.pageSize" :total="pagination.total" @change="fetchData" />
   </div>
 </template>
 
 <script setup lang="ts">
-import { reactive, ref } from 'vue';
-import { PageTitle, SearchBar, TablePagination } from '@/components';
+import { reactive, ref, onMounted, computed, watch, nextTick } from 'vue';
+import { PageTitle, TablePagination } from '@/components';
+import { deptPurchase } from '@/api/pc/enterprise/order';
+import { getContactList } from '@/api/pc/organization';
+import * as echarts from 'echarts';
+
+const loading = ref(false);
+const contactList = ref<any[]>([]);
 
 const searchForm = reactive({
-  keyword: '',
-  dateRange: [],
-  deptId: ''
+  contactId: ''
+});
+
+const contactOptions = computed(() =>
+  contactList.value.map((item: any) => ({
+    label: item.contactName || item.name || item.label || '',
+    value: String(item.contactId || item.id || item.value || '')
+  }))
+);
+
+const loadContactList = async () => {
+  try {
+    const res = await getContactList({ pageSize: 999 });
+    if (res.code === 200) {
+      contactList.value = res.rows || res.data || [];
+    }
+  } catch (e) {
+    console.error('获取联系人列表失败:', e);
+  }
+};
+
+const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
+
+const tableData = ref<any[]>([]);
+
+const chartRef = ref<HTMLElement>();
+let chartInstance: echarts.ECharts | null = null;
+
+const chartData = computed(() => {
+  if (!tableData.value.length) return { depts: [], completed: [], pending: [] };
+
+  const deptMap: Record<string, { completed: number; pending: number }> = {};
+  tableData.value.forEach((row: any) => {
+    const dept = row.deptName || '未知部门';
+    if (!deptMap[dept]) {
+      deptMap[dept] = { completed: 0, pending: 0 };
+    }
+    deptMap[dept].completed += Number(row.completedAmount) || 0;
+    deptMap[dept].pending += Number(row.pendingAmount) || 0;
+  });
+
+  const entries = Object.entries(deptMap);
+  return {
+    depts: entries.map(([name]) => name),
+    completed: entries.map(([, v]) => v.completed),
+    pending: entries.map(([, v]) => v.pending)
+  };
 });
 
-const filters = [
-  {
-    field: 'deptId',
-    label: '下单部门',
-    options: [
-      { label: '全部', value: '' },
-      { label: '人事部', value: '1' },
-      { label: '行政部', value: '2' },
-      { label: '设计部', value: '3' },
-      { label: '技术部', value: '4' }
-    ]
+const initLineChart = () => {
+  if (!chartRef.value) return;
+  if (!chartInstance) chartInstance = echarts.init(chartRef.value);
+
+  const { depts, completed, pending } = chartData.value;
+  const hasData = depts.length > 0;
+  const maxVal = hasData ? Math.max(...completed, ...pending) : 0;
+
+  chartInstance.setOption(
+    {
+      grid: { left: 60, right: 30, top: 30, bottom: 30 },
+      legend: {
+        data: ['已完成金额', '待完成金额'],
+        top: 0,
+        textStyle: { color: '#666', fontSize: 12 }
+      },
+      xAxis: {
+        type: 'category',
+        data: depts,
+        axisLine: { lineStyle: { color: '#eee' } },
+        axisLabel: { color: '#999', fontSize: 11, rotate: depts.length > 6 ? 45 : 0 },
+        axisTick: { alignWithLabel: true }
+      },
+      yAxis: {
+        type: 'value',
+        name: '金额(元)',
+        nameTextStyle: { color: '#999', fontSize: 12 },
+        axisLine: { show: false },
+        splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } },
+        axisLabel: {
+          color: '#999',
+          formatter: (v: number) => (v >= 10000 ? (v / 10000).toFixed(1) + '万' : v)
+        },
+        max: maxVal > 0 ? Math.ceil(maxVal * 1.15) : undefined
+      },
+      series: [
+        {
+          name: '已完成金额',
+          type: 'line',
+          data: completed,
+          smooth: true,
+          lineStyle: { color: '#2ecc71', width: 2.5 },
+          itemStyle: { color: '#2ecc71', borderWidth: 2, borderColor: '#fff' },
+          symbol: 'circle',
+          symbolSize: 6,
+          areaStyle: {
+            color: {
+              type: 'linear',
+              x: 0,
+              y: 0,
+              x2: 0,
+              y2: 1,
+              colorStops: [
+                { offset: 0, color: 'rgba(46, 204, 113, 0.15)' },
+                { offset: 1, color: 'rgba(46, 204, 113, 0.01)' }
+              ]
+            }
+          },
+          emphasis: { symbolSize: 10, itemStyle: { borderWidth: 3 } }
+        },
+        {
+          name: '待完成金额',
+          type: 'line',
+          data: pending,
+          smooth: true,
+          lineStyle: { color: '#e67e22', width: 2.5 },
+          itemStyle: { color: '#e67e22', borderWidth: 2, borderColor: '#fff' },
+          symbol: 'diamond',
+          symbolSize: 7,
+          areaStyle: {
+            color: {
+              type: 'linear',
+              x: 0,
+              y: 0,
+              x2: 0,
+              y2: 1,
+              colorStops: [
+                { offset: 0, color: 'rgba(230, 126, 34, 0.12)' },
+                { offset: 1, color: 'rgba(230, 126, 34, 0.01)' }
+              ]
+            }
+          },
+          emphasis: { symbolSize: 11, itemStyle: { borderWidth: 3 } }
+        }
+      ],
+      tooltip: {
+        trigger: 'axis',
+        backgroundColor: '#fff',
+        borderColor: '#eee',
+        borderWidth: 1,
+        textStyle: { color: '#333', fontSize: 12 },
+        formatter: (params: any) => {
+          const p0 = params[0];
+          const p1 = params[1];
+          return `<div style="font-weight:bold;margin-bottom:4px">${p0.name}</div>
+            已完成: <span style="color:#2ecc71;font-weight:bold">¥${p0.value.toLocaleString('en-US', { minimumFractionDigits: 2 })}</span><br/>
+            待完成: <span style="color:#e67e22;font-weight:bold">¥${p1.value.toLocaleString('en-US', { minimumFractionDigits: 2 })}</span>`;
+        }
+      },
+      animationDuration: 800,
+      animationEasing: 'cubicOut'
+    },
+    { notMerge: true }
+  );
+};
+
+watch(
+  () => tableData.value,
+  () => nextTick(() => initLineChart()),
+  { deep: true }
+);
+
+const fetchData = async () => {
+  loading.value = true;
+  try {
+    const params: any = {
+      pageNum: pagination.page,
+      pageSize: pagination.pageSize
+    };
+    if (searchForm.contactId) params.contactId = searchForm.contactId;
+
+    const res = await deptPurchase(params);
+    if (res.code === 200 || res.code === 0) {
+      tableData.value = res.rows || res.data || [];
+      pagination.total = res.total || 0;
+    }
+  } catch (e) {
+    console.error('获取部门采购数据失败:', e);
+  } finally {
+    loading.value = false;
   }
-];
-
-const pagination = reactive({ page: 1, pageSize: 10, total: 100 });
-
-const tableData = ref([
-  { deptName: '人事部', orderPerson: '李世海', orderAmount: 2115.97, completedAmount: 2115.97, pendingAmount: 2115.97 },
-  { deptName: '行政部', orderPerson: '李书萍', orderAmount: 4476.12, completedAmount: 4476.12, pendingAmount: 4476.12 },
-  { deptName: '人事部', orderPerson: '邓文锦', orderAmount: 7751.34, completedAmount: 7751.34, pendingAmount: 7751.34 },
-  { deptName: '设计部', orderPerson: '王凡宏', orderAmount: 7936.37, completedAmount: 7936.37, pendingAmount: 7936.37 },
-  { deptName: '技术部', orderPerson: '赵昊艳', orderAmount: 3931.45, completedAmount: 3931.45, pendingAmount: 3931.45 },
-  { deptName: '人事部', orderPerson: '钱君霞', orderAmount: 6132.75, completedAmount: 6132.75, pendingAmount: 6132.75 },
-  { deptName: '行政部', orderPerson: '周静', orderAmount: 3189.56, completedAmount: 3189.56, pendingAmount: 3189.56 },
-  { deptName: '行政部', orderPerson: '吴彦琛', orderAmount: 993.66, completedAmount: 993.66, pendingAmount: 993.66 },
-  { deptName: '设计部', orderPerson: '钱慧倩', orderAmount: 2763.35, completedAmount: 2763.35, pendingAmount: 2763.35 },
-  { deptName: '技术部', orderPerson: '孙银茹', orderAmount: 9286.45, completedAmount: 9286.45, pendingAmount: 9286.45 }
-]);
+};
 
 const handleSearch = () => {
-  // 搜索逻辑
+  pagination.page = 1;
+  fetchData();
 };
 
 const handleReset = () => {
-  // 重置逻辑
+  searchForm.contactId = '';
+  pagination.page = 1;
+  fetchData();
 };
+
+onMounted(() => {
+  loadContactList();
+  fetchData();
+});
 </script>
+
+<style scoped lang="scss">
+.filter-bar {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-bottom: 16px;
+}
+.chart-section {
+  background: #fff;
+  border: 1px solid #eee;
+  border-radius: 4px;
+  padding: 16px;
+  margin-bottom: 16px;
+
+  .chart-header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+
+    .chart-title {
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+    }
+  }
+
+  .chart-container {
+    width: 100%;
+    height: 280px;
+  }
+}
+</style>

+ 175 - 2
src/views/analysis/orderStatus/index.vue

@@ -28,7 +28,13 @@
         </el-form-item>
       </el-form>
     </div>
-
+    <!-- 折线图分析 -->
+    <div class="chart-section">
+      <div class="chart-header">
+        <span class="chart-title">订单金额趋势</span>
+      </div>
+      <div class="chart-container" ref="chartRef"></div>
+    </div>
     <el-table v-loading="loading" :data="tableData" border style="width: 100%">
       <el-table-column prop="orderDate" label="下单日期" width="110" align="center" />
       <el-table-column prop="orderNo" label="订单编号" width="140" align="center" />
@@ -58,13 +64,14 @@
 </template>
 
 <script setup lang="ts">
-import { reactive, ref, computed } from 'vue';
+import { reactive, ref, computed, watch, nextTick } from 'vue';
 import { PageTitle, TablePagination } from '@/components';
 import { getOrderList, getOrderProducts } from '@/api/pc/enterprise/order';
 import { getOrderStatusClass } from '@/utils/status';
 import { formatDate } from '@/utils';
 import type { ComponentInternalInstance } from 'vue';
 import { getCurrentInstance, toRefs } from 'vue';
+import * as echarts from 'echarts';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { order_status } = toRefs<any>(proxy?.useDict('order_status'));
@@ -162,6 +169,148 @@ const fetchOrderList = async () => {
   }
 };
 
+const chartRef = ref<HTMLElement>();
+let chartInstance: echarts.ECharts | null = null;
+
+const chartData = computed(() => {
+  if (!tableData.value.length) return { dates: [], amounts: [], quantities: [] };
+
+  const dailyMap: Record<string, { amount: number; quantity: number }> = {};
+  tableData.value.forEach((row: any) => {
+    const date = (row.orderDate || '').split(' ')[0];
+    if (!date) return;
+    if (!dailyMap[date]) {
+      dailyMap[date] = { amount: 0, quantity: 0 };
+    }
+    dailyMap[date].amount += Number(row.amount) || 0;
+    dailyMap[date].quantity += Number(row.quantity) || 0;
+  });
+
+  const sorted = Object.entries(dailyMap).sort(([a], [b]) => a.localeCompare(b));
+  return {
+    dates: sorted.map(([date]) => date),
+    amounts: sorted.map(([, v]) => v.amount),
+    quantities: sorted.map(([, v]) => v.quantity)
+  };
+});
+
+const initLineChart = () => {
+  if (!chartRef.value) return;
+  if (!chartInstance) chartInstance = echarts.init(chartRef.value);
+
+  const { dates, amounts, quantities } = chartData.value;
+
+  const hasData = dates.length > 0;
+  const maxAmount = hasData ? Math.max(...amounts) : 0;
+  const maxQty = hasData ? Math.max(...quantities) : 0;
+
+  chartInstance.setOption(
+    {
+      grid: { left: 60, right: 60, top: 30, bottom: 30 },
+      legend: {
+        data: ['金额', '数量'],
+        top: 0,
+        textStyle: { color: '#666', fontSize: 12 }
+      },
+      xAxis: {
+        type: 'category',
+        data: dates,
+        axisLine: { lineStyle: { color: '#eee' } },
+        axisLabel: { color: '#999', fontSize: 11, rotate: dates.length > 10 ? 45 : 0 },
+        axisTick: { alignWithLabel: true }
+      },
+      yAxis: [
+        {
+          type: 'value',
+          name: '金额(元)',
+          nameTextStyle: { color: '#e60012', fontSize: 12 },
+          axisLine: { show: false },
+          splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } },
+          axisLabel: {
+            color: '#e60012',
+            formatter: (v: number) => (v >= 10000 ? (v / 10000).toFixed(1) + '万' : v)
+          },
+          max: maxAmount > 0 ? Math.ceil(maxAmount * 1.15) : undefined
+        },
+        {
+          type: 'value',
+          name: '数量(件)',
+          nameTextStyle: { color: '#3498db', fontSize: 12 },
+          axisLine: { show: false },
+          splitLine: { show: false },
+          axisLabel: { color: '#3498db' },
+          max: maxQty > 0 ? Math.ceil(maxQty * 1.15) : undefined,
+          min: 0
+        }
+      ],
+      series: [
+        {
+          name: '金额',
+          type: 'line',
+          yAxisIndex: 0,
+          data: amounts,
+          smooth: true,
+          lineStyle: { color: '#e60012', width: 2.5 },
+          itemStyle: { color: '#e60012', borderWidth: 2, borderColor: '#fff' },
+          symbol: 'circle',
+          symbolSize: 6,
+          areaStyle: {
+            color: {
+              type: 'linear',
+              x: 0,
+              y: 0,
+              x2: 0,
+              y2: 1,
+              colorStops: [
+                { offset: 0, color: 'rgba(230, 0, 18, 0.15)' },
+                { offset: 1, color: 'rgba(230, 0, 18, 0.01)' }
+              ]
+            }
+          },
+          emphasis: { symbolSize: 10, itemStyle: { borderWidth: 3 } }
+        },
+        {
+          name: '数量',
+          type: 'line',
+          yAxisIndex: 1,
+          data: quantities,
+          smooth: true,
+          lineStyle: { color: '#3498db', width: 2.5 },
+          itemStyle: { color: '#3498db', borderWidth: 2, borderColor: '#fff' },
+          symbol: 'diamond',
+          symbolSize: 7,
+          emphasis: { symbolSize: 11, itemStyle: { borderWidth: 3 } }
+        }
+      ],
+      tooltip: {
+        trigger: 'axis',
+        backgroundColor: '#fff',
+        borderColor: '#eee',
+        borderWidth: 1,
+        textStyle: { color: '#333', fontSize: 12 },
+        formatter: (params: any) => {
+          const p0 = params[0];
+          const p1 = params[1];
+          return `<div style="font-weight:bold;margin-bottom:4px">${p0.name}</div>
+            金额: <span style="color:#e60012;font-weight:bold">¥${p0.value.toLocaleString('en-US', { minimumFractionDigits: 2 })}</span><br/>
+            数量: <span style="color:#3498db;font-weight:bold">${p1.value}</span> 件`;
+        }
+      },
+      animationDuration: 800,
+      animationEasing: 'cubicOut'
+    },
+    { notMerge: true }
+  );
+};
+
+watch(
+  () => tableData.value,
+  () => {
+    nextTick(() => initLineChart());
+  },
+  { deep: true }
+);
+
 const handleSearch = () => {
   pagination.page = 1;
   fetchOrderList();
@@ -188,4 +337,28 @@ fetchOrderList();
   word-break: break-all;
   line-height: 1.5;
 }
+.chart-section {
+  background: #fff;
+  border: 1px solid #eee;
+  border-radius: 4px;
+  padding: 16px;
+  margin-bottom: 16px;
+
+  .chart-header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+
+    .chart-title {
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+    }
+  }
+
+  .chart-container {
+    width: 100%;
+    height: 280px;
+  }
+}
 </style>

+ 26 - 39
src/views/analysis/purchaseDetail/index.vue

@@ -4,19 +4,7 @@
 
     <StatCards :data="statData" />
 
-    <!-- 采购效率分析 -->
-    <div class="analysis-section">
-      <div class="section-title">采购效率分析</div>
-      <div class="analysis-cards">
-        <div class="analysis-card" v-for="(item, index) in efficiencyData" :key="index">
-          <div class="card-label">{{ item.label }}</div>
-          <div class="card-content">
-            <span class="card-value">{{ item.value }}</span>
-            <div class="mini-chart" :ref="(el) => setChartRef(el, index)"></div>
-          </div>
-        </div>
-      </div>
-    </div>
+    <!--  -->
 
     <!-- 售后服务分析 -->
     <div class="analysis-section">
@@ -35,9 +23,10 @@
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted } from 'vue';
+import { ref, onMounted, nextTick } from 'vue';
 import * as echarts from 'echarts';
 import { PageTitle, StatCards } from '@/components';
+import { purchaseDetail } from '@/api/pc/enterprise/order';
 
 const chartRefs = ref<(HTMLElement | null)[]>([]);
 
@@ -45,40 +34,24 @@ const setChartRef = (el: any, index: number) => {
   if (el) chartRefs.value[index] = el as HTMLElement;
 };
 
-const statData = [
-  { value: '12', label: '购买数量(件)' },
-  { value: '3', label: '商品数量(件)' },
-  { value: '45', label: '品牌数量(件)' },
-  { value: '78', label: '三类品类数量(件)' }
-];
-
-const efficiencyData = [
-  { label: '订单平均确认时间(小时)', value: '0.50', data: [30, 40, 35, 50, 45, 60, 55, 70, 65, 80], color: '#e60012', type: 'line' },
-  { label: '商品平均出库时间(小时)', value: '7', data: [3, 5, 4, 6, 5, 7, 6, 8, 7, 9, 8, 7], color: '#3498db', type: 'bar' },
-  { label: '商品平均妥投时间(小时)', value: '16.5', data: [40, 50, 45, 60, 55, 70, 65, 80, 75, 90], color: '#f4c542', type: 'line' }
-];
+const statData = ref<any[]>([]);
+const afterSaleData = ref<any[]>([]);
 
-const afterSaleData = [
-  { label: '售后商品数量(件)', value: '23', data: [20, 30, 25, 40, 35, 50, 45, 60, 55, 70], color: '#e60012', type: 'line' },
-  { label: '平均完成时效(天)', value: '7', data: [2, 4, 3, 5, 4, 6, 5, 7, 6, 8, 7, 6], color: '#3498db', type: 'bar' },
-  { label: '售后商品占比', value: '16.5%', data: [10, 15, 12, 18, 14, 20, 16, 22, 18, 25], color: '#f4c542', type: 'line' }
-];
-
-onMounted(() => {
-  const allData = [...efficiencyData, ...afterSaleData];
-  allData.forEach((item, index) => {
+const initMiniCharts = () => {
+  afterSaleData.value.forEach((item, index) => {
     const el = chartRefs.value[index];
     if (!el) return;
     const chart = echarts.init(el);
+    const data = item.data && item.data.length > 0 ? item.data : [0];
     if (item.type === 'line') {
       chart.setOption({
         grid: { left: 0, right: 0, top: 5, bottom: 5 },
-        xAxis: { type: 'category', show: false, data: item.data.map((_, i) => i) },
+        xAxis: { type: 'category', show: false, data: data.map((_: any, i: number) => i) },
         yAxis: { type: 'value', show: false },
         series: [
           {
             type: 'line',
-            data: item.data,
+            data: data,
             lineStyle: { color: item.color, width: 2 },
             itemStyle: { color: item.color },
             symbol: 'none',
@@ -95,12 +68,26 @@ onMounted(() => {
     } else {
       chart.setOption({
         grid: { left: 0, right: 0, top: 5, bottom: 5 },
-        xAxis: { type: 'category', show: false, data: item.data.map((_, i) => i) },
+        xAxis: { type: 'category', show: false, data: data.map((_: any, i: number) => i) },
         yAxis: { type: 'value', show: false },
-        series: [{ type: 'bar', data: item.data, itemStyle: { color: item.color }, barWidth: 4 }]
+        series: [{ type: 'bar', data: data, itemStyle: { color: item.color }, barWidth: 4 }]
       });
     }
   });
+};
+
+onMounted(async () => {
+  try {
+    const res = await purchaseDetail();
+    if (res.code === 200 || res.code === 0) {
+      statData.value = res.data?.statData || [];
+      afterSaleData.value = res.data?.afterSaleData || [];
+    }
+  } catch (e) {
+    console.error('获取采购明细失败:', e);
+  }
+  await nextTick();
+  initMiniCharts();
 });
 </script>
 

+ 247 - 140
src/views/analysis/settlementStatus/index.vue

@@ -2,167 +2,274 @@
   <div class="page-container">
     <PageTitle title="对账结算状况" />
 
-    <SearchBar :form="searchForm" :filters="filters">
-      <template #buttons>
-        <el-button type="danger">导出</el-button>
-      </template>
-    </SearchBar>
-
-    <el-table :data="tableData" border style="width: 100%">
-      <el-table-column prop="orderNo" label="订单编号" min-width="110" align="center" />
-      <el-table-column prop="orderType" label="订单类型" min-width="90" align="center">
-        <template #default="{ row }">{{ row.orderType || '-' }}</template>
+    <div class="filter-bar">
+      <el-form :model="searchForm" inline>
+        <el-form-item label="对账状态">
+          <el-select v-model="searchForm.billStatus" placeholder="请选择对账状态" clearable @change="handleSearch">
+            <el-option v-for="item in billStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="开票状态">
+          <el-select v-model="searchForm.invoiceStatus" placeholder="请选择开票状态" clearable @change="handleSearch">
+            <el-option v-for="item in invoiceStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch">搜索</el-button>
+          <el-button @click="handleReset">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <div class="chart-section">
+      <div class="chart-header">
+        <span class="chart-title">对账金额趋势</span>
+      </div>
+      <div class="chart-container" ref="chartRef"></div>
+    </div>
+
+    <el-table v-loading="loading" :data="tableData" border style="width: 100%">
+      <el-table-column prop="billNo" label="对账编号" min-width="130" align="center" />
+      <el-table-column prop="billDate" label="对账日期" min-width="110" align="center">
+        <template #default="{ row }">{{ parseTime(row.billDate, '{y}-{m}-{d}') }}</template>
       </el-table-column>
-      <el-table-column prop="amount" label="金额" min-width="100" align="center">
-        <template #default="{ row }">¥{{ row.amount.toLocaleString() }}</template>
+      <el-table-column prop="amount" label="对账单金额" min-width="110" align="center">
+        <template #default="{ row }">¥{{ row.amount.toFixed(2) }}</template>
       </el-table-column>
-      <el-table-column prop="orderDate" label="下单日期" min-width="100" align="center" />
-      <el-table-column prop="orderStatus" label="订单状态" min-width="90" align="center">
+      <el-table-column prop="billStatus" label="对账状态" min-width="90" align="center">
         <template #default="{ row }">
-          <span :class="['status-tag', getOrderStatusClass(row.orderStatus)]">{{ row.orderStatus }}</span>
+          {{ getDictLabel(statement_status, row.billStatus) }}
         </template>
       </el-table-column>
-      <el-table-column prop="billStatus" label="对账状态" min-width="90" align="center">
-        <template #default="{ row }">{{ row.billStatus || '-' }}</template>
-      </el-table-column>
       <el-table-column prop="invoiceStatus" label="开票状态" min-width="90" align="center">
         <template #default="{ row }">
-          <span :class="['status-tag', getInvoiceStatusClass(row.invoiceStatus)]">{{ row.invoiceStatus }}</span>
+          <span :style="{ color: row.invoiceStatus === '0' ? '#e60012' : '' }">
+            {{ getDictLabel(invoice_issuance_status, row.invoiceStatus) }}
+          </span>
         </template>
       </el-table-column>
-      <el-table-column prop="settleStatus" label="结算状态" min-width="90" align="center">
+      <!-- <el-table-column prop="payStatus" label="支付状态" min-width="90" align="center">
         <template #default="{ row }">
-          <span :style="{ color: row.settleStatus === '已结算' ? '#67c23a' : '' }">{{ row.settleStatus }}</span>
+          {{ getDictLabel(payment_status, row.payStatus) }}
         </template>
-      </el-table-column>
+      </el-table-column> -->
     </el-table>
 
-    <TablePagination v-model:page="pagination.page" v-model:pageSize="pagination.pageSize" :total="pagination.total" />
+    <TablePagination v-model:page="pagination.page" v-model:pageSize="pagination.pageSize" :total="pagination.total" @change="loadStatementList" />
   </div>
 </template>
 
 <script setup lang="ts">
-import { reactive, ref } from 'vue';
-import { PageTitle, SearchBar, TablePagination } from '@/components';
-import { BILL_STATUS_OPTIONS, INVOICE_STATUS_OPTIONS, SETTLE_STATUS_OPTIONS } from '@/constants/status';
-import { getOrderStatusClass, getInvoiceStatusClass } from '@/utils/status';
+import { reactive, ref, onMounted, computed, watch, nextTick, getCurrentInstance, toRefs } from 'vue';
+import { ElMessage } from 'element-plus';
+import { PageTitle, TablePagination } from '@/components';
+import { getStatementList } from '@/api/pc/enterprise/statement';
+import type { StatementOrder } from '@/api/pc/enterprise/statementTypes';
+import type { ComponentInternalInstance } from 'vue';
+import * as echarts from 'echarts';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { invoice_issuance_status, statement_status, payment_status } = toRefs<any>(
+  proxy?.useDict('invoice_issuance_status', 'statement_status', 'payment_status')
+);
+
+const billStatusOptions = computed(() => [{ label: '全部', value: '' }, ...(statement_status.value || [])]);
+const invoiceStatusOptions = computed(() => [{ label: '全部', value: '' }, ...(invoice_issuance_status.value || [])]);
 
+const loading = ref(false);
 const searchForm = reactive({
-  keyword: '',
-  dateRange: [],
   billStatus: '',
-  invoiceStatus: '',
-  settleStatus: ''
+  invoiceStatus: ''
 });
 
-const filters = [
-  { field: 'billStatus', label: '对账状态', options: BILL_STATUS_OPTIONS },
-  { field: 'invoiceStatus', label: '开票状态', options: INVOICE_STATUS_OPTIONS },
-  { field: 'settleStatus', label: '结算状态', options: SETTLE_STATUS_OPTIONS }
-];
-
-const pagination = reactive({ page: 1, pageSize: 10, total: 100 });
-
-const tableData = ref([
-  {
-    orderNo: '232323232',
-    orderType: '普通订单',
-    amount: 2115.97,
-    orderDate: '2025-11-22',
-    orderStatus: '运输中',
-    billStatus: '已对账',
-    invoiceStatus: '已付款',
-    settleStatus: '已结算'
-  },
-  {
-    orderNo: '2323412123',
-    orderType: '普通订单',
-    amount: 4476.12,
-    orderDate: '2025-12-09',
-    orderStatus: '待发货',
-    billStatus: '未对账',
-    invoiceStatus: '待开票',
-    settleStatus: '未结算'
-  },
-  {
-    orderNo: '4566784543',
-    orderType: '协议订单',
-    amount: 7751.34,
-    orderDate: '2025-11-21',
-    orderStatus: '已收货',
-    billStatus: '已对账',
-    invoiceStatus: '已开票',
-    settleStatus: '已结算'
-  },
-  {
-    orderNo: '2356565654',
-    orderType: '普通订单',
-    amount: 7936.37,
-    orderDate: '2025-11-27',
-    orderStatus: '运输中',
-    billStatus: '未对账',
-    invoiceStatus: '已付款',
-    settleStatus: '未结算'
-  },
-  {
-    orderNo: '2323412123',
-    orderType: '协议订单',
-    amount: 3931.45,
-    orderDate: '2025-12-11',
-    orderStatus: '待发货',
-    billStatus: '未对账',
-    invoiceStatus: '待开票',
-    settleStatus: '未结算'
-  },
-  {
-    orderNo: '2323232323',
-    orderType: '普通订单',
-    amount: 6132.75,
-    orderDate: '2025-11-27',
-    orderStatus: '已收货',
-    billStatus: '已对账',
-    invoiceStatus: '已开票',
-    settleStatus: '已结算'
-  },
-  {
-    orderNo: '2356565654',
-    orderType: '普通订单',
-    amount: 3189.56,
-    orderDate: '2025-11-25',
-    orderStatus: '已收货',
-    billStatus: '已对账',
-    invoiceStatus: '已开票',
-    settleStatus: '已结算'
-  },
-  {
-    orderNo: '2323232323',
-    orderType: '协议订单',
-    amount: 993.66,
-    orderDate: '2025-11-20',
-    orderStatus: '运输中',
-    billStatus: '未对账',
-    invoiceStatus: '已付款',
-    settleStatus: '未结算'
-  },
-  {
-    orderNo: '2323412123',
-    orderType: '普通订单',
-    amount: 2763.35,
-    orderDate: '2025-11-17',
-    orderStatus: '待发货',
-    billStatus: '未对账',
-    invoiceStatus: '待开票',
-    settleStatus: '未结算'
-  },
-  {
-    orderNo: '2323232323',
-    orderType: '协议订单',
-    amount: 9286.45,
-    orderDate: '2025-12-05',
-    orderStatus: '已收货',
-    billStatus: '已对账',
-    invoiceStatus: '已开票',
-    settleStatus: '已结算'
+const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
+const tableData = ref<any[]>([]);
+
+const chartRef = ref<HTMLElement>();
+let chartInstance: echarts.ECharts | null = null;
+
+const chartData = computed(() => {
+  if (!tableData.value.length) return { dates: [], amounts: [] };
+
+  const dailyMap: Record<string, number> = {};
+  tableData.value.forEach((row: any) => {
+    const date = (row.billDate || '').split(' ')[0];
+    if (!date) return;
+    if (!dailyMap[date]) dailyMap[date] = 0;
+    dailyMap[date] += Number(row.amount) || 0;
+  });
+
+  const sorted = Object.entries(dailyMap).sort(([a], [b]) => a.localeCompare(b));
+  return {
+    dates: sorted.map(([date]) => date),
+    amounts: sorted.map(([, v]) => v)
+  };
+});
+
+const initLineChart = () => {
+  if (!chartRef.value) return;
+  if (!chartInstance) chartInstance = echarts.init(chartRef.value);
+
+  const { dates, amounts } = chartData.value;
+  const hasData = dates.length > 0;
+  const maxVal = hasData ? Math.max(...amounts) : 0;
+
+  chartInstance.setOption(
+    {
+      grid: { left: 60, right: 20, top: 30, bottom: 30 },
+      xAxis: {
+        type: 'category',
+        data: dates,
+        axisLine: { lineStyle: { color: '#eee' } },
+        axisLabel: { color: '#999', fontSize: 11, rotate: dates.length > 10 ? 45 : 0 },
+        axisTick: { alignWithLabel: true }
+      },
+      yAxis: {
+        type: 'value',
+        name: '金额(元)',
+        nameTextStyle: { color: '#999', fontSize: 12 },
+        axisLine: { show: false },
+        splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } },
+        axisLabel: {
+          color: '#999',
+          formatter: (v: number) => (v >= 10000 ? (v / 10000).toFixed(1) + '万' : v)
+        },
+        max: maxVal > 0 ? Math.ceil(maxVal * 1.15) : undefined
+      },
+      series: [
+        {
+          name: '对账单金额',
+          type: 'line',
+          data: amounts,
+          smooth: true,
+          lineStyle: { color: '#e60012', width: 2.5 },
+          itemStyle: { color: '#e60012', borderWidth: 2, borderColor: '#fff' },
+          symbol: 'circle',
+          symbolSize: 6,
+          areaStyle: {
+            color: {
+              type: 'linear',
+              x: 0,
+              y: 0,
+              x2: 0,
+              y2: 1,
+              colorStops: [
+                { offset: 0, color: 'rgba(230, 0, 18, 0.15)' },
+                { offset: 1, color: 'rgba(230, 0, 18, 0.01)' }
+              ]
+            }
+          },
+          emphasis: { symbolSize: 10, itemStyle: { borderWidth: 3 } }
+        }
+      ],
+      tooltip: {
+        trigger: 'axis',
+        backgroundColor: '#fff',
+        borderColor: '#eee',
+        borderWidth: 1,
+        textStyle: { color: '#333', fontSize: 12 },
+        formatter: (params: any) => {
+          const p = params[0];
+          return `<div style="font-weight:bold;margin-bottom:4px">${p.name}</div>金额: <span style="color:#e60012;font-weight:bold">¥${p.value.toLocaleString('en-US', { minimumFractionDigits: 2 })}</span>`;
+        }
+      },
+      animationDuration: 800,
+      animationEasing: 'cubicOut'
+    },
+    { notMerge: true }
+  );
+};
+
+watch(
+  () => tableData.value,
+  () => nextTick(() => initLineChart()),
+  { deep: true }
+);
+
+const getDictLabel = (dictOptions: any[], value: string) => {
+  if (!dictOptions || !value) return value;
+  const dict = dictOptions.find((item) => item.value === value);
+  return dict ? dict.label : value;
+};
+
+const loadStatementList = async () => {
+  try {
+    loading.value = true;
+    const queryParams = {
+      pageNum: pagination.page,
+      pageSize: pagination.pageSize,
+      statementStatus: searchForm.billStatus,
+      isInvoiceStatus: searchForm.invoiceStatus
+    };
+
+    const res = await getStatementList(queryParams);
+
+    if (res.code === 200 && res.rows) {
+      tableData.value = res.rows.map((item: StatementOrder) => ({
+        id: item.id,
+        billNo: item.statementOrderNo,
+        billDate: item.statementDate,
+        amount: parseFloat(item.amount as any) || 0,
+        billStatus: item.statementStatus,
+        invoiceStatus: item.isInvoiceStatus,
+        payStatus: item.isPaymentStatus
+      }));
+      pagination.total = res.total || 0;
+    }
+  } catch (error) {
+    console.error('加载对账单列表失败:', error);
+    ElMessage.error('加载对账单列表失败');
+  } finally {
+    loading.value = false;
   }
-]);
+};
+
+const handleSearch = () => {
+  pagination.page = 1;
+  loadStatementList();
+};
+
+const handleReset = () => {
+  searchForm.billStatus = '';
+  searchForm.invoiceStatus = '';
+  pagination.page = 1;
+  loadStatementList();
+};
+
+onMounted(() => {
+  loadStatementList();
+});
 </script>
+
+<style scoped lang="scss">
+.filter-bar {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-bottom: 16px;
+}
+.chart-section {
+  background: #fff;
+  border: 1px solid #eee;
+  border-radius: 4px;
+  padding: 16px;
+  margin-bottom: 16px;
+
+  .chart-header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+
+    .chart-title {
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+    }
+  }
+
+  .chart-container {
+    width: 100%;
+    height: 280px;
+  }
+}
+</style>

+ 1 - 1
src/views/home/datacomponents/JDHeader.vue

@@ -119,7 +119,7 @@
         <!-- 双 Logo 设计:优易慧采 + 中国银行 (锁定物理尺寸为 127*40 与 144*40) -->
         <div class="logo-box flex">
           <!-- 优易慧采大红Logo (矢量渲染,锁定宽度 127px,高度 40px) -->
-          <div class="logo-hucai">{{ dataInfo.mainTitle || '' }}</div>
+          <div class="logo-hucai" @click="onPath('/indexDataDiy')">{{ dataInfo.mainTitle || '' }}</div>
 
           <!-- 中国银行真实图片 Logo (物理尺寸三层死锁为 144px * 40px) -->
           <img

+ 1 - 1
src/views/home/jdcomponents/JDHeader.vue

@@ -38,7 +38,7 @@
     <div :class="['header-mid-wrap', { 'is-fixed': isFixed }]">
       <div class="header-mid flex" :class="{ 'w': headerClass == 'header-indexEnterprise' }">
         <div class="logo-box">
-          <div class="logo-text" :style="{ color: config.themeColor }">{{ config.mainTitle }}</div>
+          <div class="logo-text" :style="{ color: config.themeColor }" @click="onPath('/indexEnterprise')">{{ config.mainTitle }}</div>
           <p class="logo-desc">{{ config.subTitle }}</p>
         </div>