src/assets/icons/png/circleBlue@2x.png
src/assets/icons/png/circleGreen@2x.png
src/assets/icons/png/circleOrange@2x.png
src/assets/icons/png/circleRed@2x.png
src/assets/icons/png/circleYellow@2x.png
src/assets/icons/png/walletBlue@2x.png
src/assets/icons/png/walletGreen@2x.png
src/assets/icons/png/walletOrange@2x.png
src/assets/icons/png/walletRed@2x.png
src/assets/icons/png/walletYellow@2x.png
src/views/collaborativeApproval/approvalProcess/index.vue
@@ -8,7 +8,7 @@ <el-tab-pane label="报销管理" name="4"></el-tab-pane> <el-tab-pane label="采购审批" name="5"></el-tab-pane> <el-tab-pane label="报价审批" name="6"></el-tab-pane> <el-tab-pane label="出库审批" name="7"></el-tab-pane> <el-tab-pane label="发货审批" name="7"></el-tab-pane> </el-tabs> <div class="search_form"> @@ -35,9 +35,18 @@ > </div> <div> <el-button type="primary" @click="openForm('add')" v-if="currentApproveType !== 6">新增</el-button> <el-button type="primary" @click="openForm('add')" v-if="currentApproveType !== 6 && currentApproveType !== 7" >新增</el-button> <el-button @click="handleOut">导出</el-button> <el-button type="danger" plain @click="handleDelete">删除</el-button> <el-button type="danger" plain @click="handleDelete" v-if="currentApproveType !== 7" >删除</el-button> </div> </div> <div class="table_list"> @@ -205,7 +214,12 @@ clickFun: (row) => { openForm("edit", row); }, disabled: (row) => currentApproveType.value === 6 || row.approveStatus == 2 || row.approveStatus == 1 || row.approveStatus == 4 disabled: (row) => currentApproveType.value === 6 || currentApproveType.value === 7 || row.approveStatus == 2 || row.approveStatus == 1 || row.approveStatus == 4 }, { name: "审核", @@ -294,7 +308,7 @@ 4: "报销管理审批表", 5: "采购申请审批表", 6: "报价审批表", 7: "出库审批表", 7: "发货审批表", } const fileName = nameMap[type] || nameMap[0] proxy.download(url, {}, `${fileName}.xlsx`) src/views/financialManagement/financialStatements/index.vue
@@ -28,103 +28,135 @@ <main class="container mx-auto px-4 pb-10"> <!-- 财务指标卡片 --> <div class="grid-container"> <!-- 总收入 --> <el-card class="bg1"> <p>总收入</p> <h3> ¥{{ pageInfo.totalIncome }} </h3> </el-card> <!-- 收入笔数 --> <el-card class="bg2"> <p>收入笔数</p> <h3> {{ pageInfo.incomeNumber }} </h3> </el-card> <div class="stats-cards"> <!-- 总营收 --> <div class="stat-card stat-card-blue"> <div class="stat-icon"> <img src="@/assets/icons/png/walletBlue@2x.png" alt="总营收" /> </div> <div class="stat-content"> <div class="stat-label">总营收</div> <div class="stat-value">{{ formatMoney(pageInfo.totalIncome || 0) }} 元</div> </div> </div> <!-- 总支出 --> <el-card class="bg3"> <p>总支出</p> <h3> ¥{{ pageInfo.totalExpense }} </h3> </el-card> <div class="stat-card stat-card-orange"> <div class="stat-icon"> <img src="@/assets/icons/png/walletOrange@2x.png" alt="总支出" /> </div> <div class="stat-content"> <div class="stat-label">总支出</div> <div class="stat-value">{{ formatMoney(pageInfo.totalExpense || 0) }} 元</div> </div> </div> <!-- 支出笔数 --> <el-card class="bg4"> <p>支出笔数</p> <h3> {{ pageInfo.expenseNumber }} </h3> </el-card> <!-- 总收入笔数 --> <div class="stat-card stat-card-green"> <div class="stat-icon"> <img src="@/assets/icons/png/walletGreen@2x.png" alt="总收入笔数" /> </div> <div class="stat-content"> <div class="stat-label">总收入笔数</div> <div class="stat-value">{{ pageInfo.incomeNumber || 0 }} 笔</div> </div> </div> <!-- 总支出笔数 --> <div class="stat-card stat-card-red"> <div class="stat-icon"> <img src="@/assets/icons/png/walletRed@2x.png" alt="总支出笔数" /> </div> <div class="stat-content"> <div class="stat-label">总支出笔数</div> <div class="stat-value">{{ pageInfo.expenseNumber || 0 }} 笔</div> </div> </div> <!-- 净收入 --> <el-card class="bg5"> <p>净收入</p> <h3> ¥{{ pageInfo.netRevenue }} </h3> <div class="stat-card stat-card-yellow"> <div class="stat-icon"> <img src="@/assets/icons/png/walletYellow@2x.png" alt="净收入" /> </div> <div class="stat-content"> <div class="stat-label">净收入</div> <div class="stat-value">{{ formatMoney(pageInfo.netRevenue || 0) }} 元</div> </div> </div> </div> <!-- 中间图表区域 --> <div class="charts-row"> <!-- 左侧:收入支出分析 --> <el-card class="chart-card"> <h2 class="section-title">收入支出分析</h2> <div class="pie-chart-container"> <Echarts :legend="pieLegendIncomeExpense" :chartStyle="chartStylePie" :series="pieSeriesIncomeExpense" :tooltip="pieTooltipIncomeExpense" style="height: 320px; width: 100%;"> </Echarts> <div class="pie-stats"> <div class="bar-stat-item"> <span class="bar-stat-label">收入数量</span> <span class="bar-stat-value">{{ pageInfo.incomeNumber || 0 }}</span> </div> <div class="bar-stat-item"> <span class="bar-stat-label">支出数量</span> <span class="bar-stat-value">{{ pageInfo.expenseNumber || 0 }}</span> </div> </div> </div> </el-card> <!-- 右侧:行项盈利分析 --> <el-card class="chart-card"> <h2 class="section-title">行项盈利分析</h2> <div class="bar-chart-header"> <div class="bar-stat-item"> <span class="bar-stat-label">当前总个数</span> <span class="bar-stat-value">{{ allBarTypes.value?.length || 0 }}</span> </div> <div class="bar-stat-item"> <span class="bar-stat-label">支出金额</span> <span class="bar-stat-value">{{ formatMoney(pageInfo.totalExpense || 0) }}</span> </div> <div class="bar-stat-item"> <span class="bar-stat-label">收入金额</span> <span class="bar-stat-value">{{ formatMoney(pageInfo.totalIncome || 0) }}</span> </div> </div> <Echarts ref="barChart" :chartStyle="chartStyle" :grid="barGrid" :legend="barLegend" :series="barSeries" :tooltip="barTooltip" :xAxis="barXAxis" :yAxis="barYAxis" style="height: 300px; width: 100%;"> </Echarts> </el-card> </div> <!-- 收入统计图表 --> <div class="grid-layout"> <el-card style="margin-bottom: 20px;"> <h2 class="section-title">收入统计(元)</h2> <div class="echarts"> <Echarts :legend="pieLegend0" :chartStyle="chartStylePie" :series="materialPieSeries0" :tooltip="pieTooltip" style="height: 260px;width: 35%;"> <div class="chart-num"> <span style="font-size: 22px;">收入</span> <span style="font-size: 36px; font-weight: 500; font-family: 'MyCustomFont', sans-serif;">{{ pageInfo.totalIncome }}</span> </div> </Echarts> <Echarts ref="chart" <!-- 底部:营收趋势分析 --> <el-card class="trend-chart-card"> <h2 class="section-title">营收趋势分析</h2> <Echarts ref="trendChart" :chartStyle="chartStyle" :grid="grid" :legend="lineLegend" :series="lineSeries0" :legend="trendLegend" :series="trendSeries" :tooltip="tooltip" :xAxis="xAxis0" :yAxis="yAxis0" style="height: 260px;width: 64%;"></Echarts> </div> :yAxis="trendYAxis" style="height: 350px; width: 100%;"> </Echarts> </el-card> <!-- 支出统计图表 --> <el-card> <h2 class="section-title">支出统计(元)</h2> <div class="echarts"> <Echarts ref="chart" :legend="pieLegend1" :chartStyle="chartStylePie" :series="materialPieSeries1" :tooltip="pieTooltip" style="height: 260px;width: 35%;"> <div class="chart-num"> <span style="font-size: 22px;">支出</span> <span style="font-size: 36px; font-weight: 500; font-family: 'MyCustomFont', sans-serif;">{{ pageInfo.totalExpense }}</span> </div></Echarts> <Echarts ref="chart" :chartStyle="chartStyle" :grid="grid" :legend="lineLegend" :series="lineSeries1" :tooltip="tooltip" :xAxis="xAxis1" :yAxis="yAxis1" style="height: 260px;width: 64%;"></Echarts> </div> </el-card> </div> </main> </div> </template> @@ -334,6 +366,262 @@ const pageInfo = ref({ }) // 格式化金额 const formatMoney = (value) => { if (!value && value !== 0) return '0'; return Number(value).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }; // 收入支出分析饼图 const pieDataIncomeExpense = computed(() => { const totalIncome = Number(pageInfo.value.totalIncome) || 0; const totalExpense = Number(pageInfo.value.totalExpense) || 0; const total = totalIncome + totalExpense; if (total === 0) { return [ { name: '收入', value: 0, percent: '0%' }, { name: '支出', value: 0, percent: '0%' } ]; } const incomePercent = ((totalIncome / total) * 100).toFixed(0); const expensePercent = ((totalExpense / total) * 100).toFixed(0); return [ { name: '收入', value: totalIncome, percent: `${incomePercent}%` }, { name: '支出', value: totalExpense, percent: `${expensePercent}%` } ]; }); const pieLegendIncomeExpense = computed(() => ({ show: false })); const pieTooltipIncomeExpense = reactive({ trigger: 'item', formatter: function(params) { if (!params.data) return params.name; return `${params.name}占比 ${params.percent}%`; } }); const pieSeriesIncomeExpense = computed(() => [ { type: 'pie', radius: ['0%', '70%'], center: ['50%', '50%'], avoidLabelOverlap: true, itemStyle: { borderColor: '#fff', borderWidth: 2 }, label: { show: true, position: 'outside', formatter: function(params) { return `${params.name}占比 ${params.percent}%`; }, fontSize: 14, color: '#333' }, labelLine: { show: true, length: 15, length2: 10, lineStyle: { color: '#333' } }, emphasis: { label: { show: true, fontSize: 16, fontWeight: 'bold' } }, data: pieDataIncomeExpense.value, color: ['#1890FF', '#FACC14'] } ]); // 行项盈利分析柱状图 const barXAxis = computed(() => { return [{ type: 'category', data: (allBarTypes.value && allBarTypes.value.length > 0) ? allBarTypes.value : ['项目1', '项目2', '项目3', '项目4', '项目5', '项目6', '项目7'], axisTick: { show: true, alignWithLabel: true }, }]; }); const barYAxis = [{ type: 'value', name: '单位: 元', position: 'left', min: 0, nameTextStyle: { color: '#000', fontSize: 14, }, }]; const barGrid = { left: '3%', right: '4%', bottom: '3%', containLabel: true }; const barLegend = { show: true, top: 10, right: 10, }; // 获取所有类型名称 const allBarTypes = computed(() => { const incomeTypes = (lineSeries0.value || []).map(item => item.name || item.typeName).filter(Boolean); const expenseTypes = (lineSeries1.value || []).map(item => item.name || item.typeName).filter(Boolean); return [...new Set([...incomeTypes, ...expenseTypes])]; }); const barSeries = computed(() => { if (allBarTypes.value.length === 0) { return [ { name: '支出', type: 'bar', data: [], itemStyle: { color: '#1890FF' } }, { name: '收入', type: 'bar', data: [], itemStyle: { color: '#13C2C2' } } ]; } // 计算每个项目的总收入(汇总所有月份) const incomeData = allBarTypes.value.map(typeName => { const incomeItem = (lineSeries0.value || []).find(item => (item.name || item.typeName) === typeName); if (incomeItem && incomeItem.data && Array.isArray(incomeItem.data)) { return incomeItem.data.reduce((sum, val) => sum + (Number(val) || 0), 0); } return 0; }); // 计算每个项目的总支出(汇总所有月份) const expenseData = allBarTypes.value.map(typeName => { const expenseItem = (lineSeries1.value || []).find(item => (item.name || item.typeName) === typeName); if (expenseItem && expenseItem.data && Array.isArray(expenseItem.data)) { return expenseItem.data.reduce((sum, val) => sum + (Number(val) || 0), 0); } return 0; }); return [ { name: '支出', type: 'bar', data: expenseData, itemStyle: { color: '#1890FF' } }, { name: '收入', type: 'bar', data: incomeData, itemStyle: { color: '#13C2C2' } } ]; }); const barTooltip = reactive({ trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: function (params) { if (!params || !params.length) return ''; const axisLabel = params[0].axisValueLabel || params[0].axisValue || ''; const rows = params .map(p => { const colorDot = `<span style="display:inline-block;margin-right:6px;width:8px;height:8px;border-radius:50%;background:${p.color}"></span>`; const value = typeof p.value === 'number' ? p.value.toFixed(2) : p.value; return `${colorDot}${p.seriesName} ${value}`; }) .join('<br/>'); return `<div>${axisLabel}</div><div>${rows}</div>`; } }); // 营收趋势分析 const trendLegend = { show: true, top: 10, right: 10, }; const trendYAxis = [{ type: 'value', name: '单位: 元', position: 'left', min: 0, nameTextStyle: { color: '#000', fontSize: 14, }, }]; const trendSeries = computed(() => { // 汇总所有支出类型的数据 let expenseTrend = []; if (lineSeries1.value.length > 0) { const monthCount = Math.max(...lineSeries1.value.map(item => item.data?.length || 0)); expenseTrend = Array(monthCount).fill(0); lineSeries1.value.forEach(item => { if (item.data && Array.isArray(item.data)) { item.data.forEach((val, index) => { if (index < monthCount) { expenseTrend[index] += Number(val) || 0; } }); } }); } // 汇总所有收入类型的数据 let incomeTrend = []; if (lineSeries0.value.length > 0) { const monthCount = Math.max(...lineSeries0.value.map(item => item.data?.length || 0)); incomeTrend = Array(monthCount).fill(0); lineSeries0.value.forEach(item => { if (item.data && Array.isArray(item.data)) { item.data.forEach((val, index) => { if (index < monthCount) { incomeTrend[index] += Number(val) || 0; } }); } }); } return [ { name: '支出', type: 'line', data: expenseTrend, itemStyle: { color: '#1890FF' }, smooth: true }, { name: '收入', type: 'line', data: incomeTrend, itemStyle: { color: '#13C2C2' }, smooth: true } ]; }); // 获取最近六个月的范围 const getLastSixMonths = () => { const endMonth = dayjs().format('YYYY-MM'); @@ -487,111 +775,220 @@ :root { --el-color-primary: #4f46e5; } .el-card{ position: relative; border-radius: 12px; padding: 14px 10px 10px 10px; box-shadow: 0 2px 8px #eee; :deep(.el-card__body){ padding: 10px 20px !important; } &.bg1{ background: url(@/assets/icons/png/1.png) no-repeat 100% 100% !important; } &.bg2{ background: url(@/assets/icons/png/2.png) no-repeat 100% 100% !important; } &.bg3{ background: url(@/assets/icons/png/3.png) no-repeat 100% 100% !important; } &.bg4{ background: url(@/assets/icons/png/4.png) no-repeat 100% 100% !important; } &.bg5{ background: url(@/assets/icons/png/5.png) no-repeat 100% 100% !important; } } .grid-container { /* grid 容器基础样式 */ /* 统计卡片样式 */ .stats-cards { display: grid; gap: 1rem; /* gap-4 对应 1rem (16px) */ margin-bottom: 2rem; /* mb-8 对应 2rem (32px) */ p{ font-size: 22px; margin-top: 0px; color: #fff; } h3{ font-size: 36px; font-weight: 500; font-family: 'MyCustomFont', sans-serif; margin: 10px 0; color: #fff; grid-template-columns: repeat(5, 1fr); gap: 20px; margin-bottom: 20px; } } .stat-card { background: #fff; border: 1px solid #e4e7ed; border-radius: 8px; padding: 20px; display: flex; align-items: center; gap: 15px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); transition: all 0.3s; /* 移动端默认样式 (grid-cols-1) */ .grid-container { grid-template-columns: repeat(1, minmax(0, 1fr)); } /* 小屏幕及以上 (sm:grid-cols-2) */ @media (min-width: 640px) { .grid-container { grid-template-columns: repeat(2, minmax(0, 1fr)); } } /* 大屏幕及以上 (lg:grid-cols-5) */ @media (min-width: 1024px) { .grid-container { grid-template-columns: repeat(5, minmax(0, 1fr)); } } /* 卡片悬停效果增强 */ .el-card:hover { &:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); transform: translateY(-2px); } .echarts{ display: flex; justify-content: space-between; } /* 图表容器样式 */ .el-chart { .stat-icon { width: 48px; height: 48px; flex-shrink: 0; img { width: 100%; height: 100%; object-fit: contain; } } .stat-content { flex: 1; display: flex; flex-direction: column; gap: 8px; } .stat-label { font-size: 14px; color: #666; line-height: 1.2; } .stat-value { font-size: 24px; font-weight: 600; color: #333; line-height: 1.2; } .stat-trend { font-size: 12px; line-height: 1.2; &.trend-up { color: #f56c6c; } &.trend-down { color: #67c23a; } } } /* 图表行布局 */ .charts-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; } .chart-card { border: 1px solid #e4e7ed; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); :deep(.el-card__body) { padding: 20px !important; } } .trend-chart-card { border: 1px solid #e4e7ed; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); :deep(.el-card__body) { padding: 20px !important; } } /* 饼图容器 */ .pie-chart-container { position: relative; .pie-stats { display: flex; justify-content: space-between; gap: 20px; margin-top: 20px; .bar-stat-item { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 15px; background: #f5f7fa; border-radius: 6px; flex: 1; .bar-stat-label { font-size: 14px; color: #666; } .bar-stat-value { font-size: 18px; font-weight: 600; color: #333; } } } } /* 柱状图头部统计 */ .bar-chart-header { display: flex; justify-content: space-between; gap: 20px; margin-bottom: 20px; .bar-stat-item { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 15px; background: #f5f7fa; border-radius: 6px; flex: 1; .bar-stat-label { font-size: 14px; color: #666; } .bar-stat-value { font-size: 18px; font-weight: 600; color: #333; } } } /* 标题样式 */ .section-title { position: relative; font-size: 18px; color: #333; padding-left: 10px; margin-bottom: 10px; padding-left: 12px; margin-bottom: 20px; font-weight: 700; } .section-title::before { &::before { position: absolute; left: 0; top: 0px; top: 2px; content: ''; width: 4px; height: 18px; background-color: #002FA7; border-radius: 2px; } .chart-num{ position: absolute; z-index: 3; top: 92px; left: 92px; display: flex; flex-direction: column; justify-content: center; } /* 响应式设计 */ @media (max-width: 1400px) { .stats-cards { grid-template-columns: repeat(3, 1fr); } } @media (max-width: 1024px) { .stats-cards { grid-template-columns: repeat(2, 1fr); } .charts-row { grid-template-columns: 1fr; } } @media (max-width: 640px) { .stats-cards { grid-template-columns: 1fr; } } </style> src/views/salesManagement/deliveryLedger/index.vue
@@ -3,11 +3,15 @@ <div class="search_form"> <el-form :model="searchForm" :inline="true"> <el-form-item label="销售订单号:"> <el-input v-model="searchForm.salesContractNo" placeholder="请输入" clearable prefix-icon="Search" <el-input v-model="searchForm.salesContractNo" placeholder="请输入" clearable prefix-icon="Search" style="width: 200px" @change="handleQuery" /> </el-form-item> <el-form-item label="车牌号:"> <el-input v-model="searchForm.shippingCarNumber" placeholder="请输入" clearable prefix-icon="Search" <el-input v-model="searchForm.shippingCarNumber" placeholder="请输入" clearable prefix-icon="Search" style="width: 200px" @change="handleQuery" /> </el-form-item> <el-form-item label="快递单号:"> <el-input v-model="searchForm.expressNumber" placeholder="请输入" clearable prefix-icon="Search" style="width: 200px" @change="handleQuery" /> </el-form-item> <el-form-item> @@ -24,13 +28,15 @@ </div> </div> <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange" :row-key="(row) => row.id" style="width: 100%" height="calc(100vh - 18.5em)"> :row-key="(row) => row.id" style="width: 100%" height="calc(100vh - 21.5em)"> <el-table-column align="center" type="selection" width="55" /> <el-table-column align="center" label="序号" type="index" width="60" /> <el-table-column label="销售订单" prop="salesContractNo" show-overflow-tooltip /> <el-table-column label="客户名称" prop="customerName" show-overflow-tooltip /> <el-table-column label="发货时间" prop="shippingDate" show-overflow-tooltip /> <el-table-column label="发货车牌号" prop="shippingCarNumber" show-overflow-tooltip /> <el-table-column label="快递公司" prop="expressCompany" show-overflow-tooltip /> <el-table-column label="快递单号" prop="expressNumber" show-overflow-tooltip /> <el-table-column fixed="right" label="操作" width="150" align="center"> <template #default="scope"> <el-button link type="primary" size="small" @click="openForm('edit', scope.row)">编辑</el-button> @@ -41,61 +47,123 @@ <pagination v-show="total > 0" :total="total" layout="total, sizes, prev, pager, next, jumper" :page="page.current" :limit="page.size" @pagination="paginationChange" /> </div> <FormDialog v-model="dialogFormVisible" :title="operationType === 'add' ? '新增发货台账' : '编辑发货台账'" :width="'50%'" :operation-type="operationType" @close="closeDia" @confirm="submitForm" @cancel="closeDia"> <el-dialog v-model="dialogFormVisible" :title="operationType === 'add' ? '新增发货台账' : '编辑发货台账'" width="40%" @close="closeDia"> <el-form :model="form" label-width="120px" label-position="top" :rules="rules" ref="formRef"> <el-row :gutter="30"> <el-col :span="24"> <el-form-item label="销售订单:" prop="salesContractNo"> <el-select v-model="form.salesContractNo" placeholder="请选择" clearable filterable @change="handleSalesOrderChange" style="width: 100%" :disabled="operationType === 'edit'"> <el-option v-for="item in salesOrderOptions" :key="item.salesContractNo" :label="item.salesContractNo" :value="item.salesContractNo"> {{ item.salesContractNo + ' - ' + item.customerName }} </el-option> <el-form-item label="发货类型:" prop="type"> <el-select v-model="form.type" placeholder="请选择发货类型" style="width: 100%" @change="handleShippingTypeChange" > <el-option label="货车" value="货车" /> <el-option label="快递" value="快递" /> </el-select> </el-form-item> </el-col> </el-row> <el-row :gutter="30"> <el-col :span="24"> <el-form-item label="客户名称:" prop="customerName"> <el-input v-model="form.customerName" placeholder="请输入" clearable :disabled="operationType === 'edit'" /> <el-form-item label="发货日期:" prop="shippingDate"> <el-date-picker style="width: 100%" v-model="form.shippingDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="date" placeholder="请选择发货日期" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="30"> <el-col :span="24"> <el-form-item label="发货时间:" prop="shippingDate"> <el-date-picker style="width: 100%" v-model="form.shippingDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="date" placeholder="请选择" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="30"> <el-col :span="24"> <el-col :span="24" v-if="form.type === '货车'"> <el-form-item label="发货车牌号:" prop="shippingCarNumber"> <el-input v-model="form.shippingCarNumber" placeholder="请输入" clearable /> <el-input v-model="form.shippingCarNumber" placeholder="请输入发货车牌号" clearable /> </el-form-item> </el-col> <el-col :span="24" v-else> <el-form-item label="快递公司:" prop="expressCompany"> <el-input v-model="form.expressCompany" placeholder="请输入快递公司" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="30" v-if="form.type === '快递'"> <el-col :span="24"> <el-form-item label="快递单号:" prop="expressNumber"> <el-input v-model="form.expressNumber" placeholder="请输入快递单号" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="30"> <el-col :span="24"> <el-form-item label="发货图片:"> <el-upload v-model:file-list="deliveryFileList" :action="upload.url" multiple ref="deliveryFileUpload" auto-upload :headers="upload.headers" :data="{ type: 9 }" :before-upload="handleDeliveryBeforeUpload" :on-error="handleDeliveryUploadError" :on-success="handleDeliveryUploadSuccess" :on-remove="handleDeliveryRemove" list-type="picture-card" :limit="9" accept="image/png,image/jpeg,image/jpg" > <el-icon class="avatar-uploader-icon"><Plus /></el-icon> <template #tip> <div class="el-upload__tip"> 支持 jpg、jpeg、png 格式,最多上传 9 张,单张大小不超过 10MB </div> </template> </el-upload> </el-form-item> </el-col> </el-row> </el-form> </FormDialog> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="submitForm">确认</el-button> <el-button @click="closeDia">取消</el-button> </div> </template> </el-dialog> </div> </template> <script setup> import pagination from "@/components/PIMTable/Pagination.vue"; import FormDialog from '@/components/Dialog/FormDialog.vue'; import { onMounted, ref, reactive, toRefs, getCurrentInstance } from "vue"; import { ElMessageBox } from "element-plus"; import { Plus } from "@element-plus/icons-vue"; import { getToken } from "@/utils/auth"; import { getCurrentDate } from "@/utils/index.js"; import { deliveryLedgerListPage, addOrUpdateDeliveryLedger, delDeliveryLedger, } from "@/api/salesManagement/deliveryLedger.js"; import { delLedgerFile } from "@/api/salesManagement/salesLedger.js"; const { proxy } = getCurrentInstance(); @@ -108,6 +176,16 @@ size: 100, }); const total = ref(0); const deliveryFileList = ref([]); const javaApi = proxy.javaApi; // 上传配置 const upload = reactive({ // 上传的地址 url: import.meta.env.VITE_APP_BASE_API + "/file/upload", // 设置上传的请求头部 headers: { Authorization: "Bearer " + getToken() }, }); // 用户信息表单弹框数据 const operationType = ref(""); @@ -116,19 +194,31 @@ searchForm: { salesContractNo: "", // 销售订单号 shippingCarNumber: "", // 车牌号 expressNumber: "", // 快递单号 }, form: { id: null, salesContractNo: "", customerName: "", type: "货车", // 货车, 快递 shippingDate: "", shippingCarNumber: "", expressCompany: "", expressNumber: "", // 快递单号 }, rules: { salesContractNo: [{ required: true, message: "请选择销售订单", trigger: "change" }], customerName: [{ required: true, message: "请输入客户名称", trigger: "blur" }], type: [ { required: true, message: "请选择发货类型", trigger: "change" } ], shippingDate: [{ required: true, message: "请选择发货时间", trigger: "change" }], shippingCarNumber: [{ required: true, message: "请输入发货车牌号", trigger: "blur" }], shippingCarNumber: [ { validator: (_, value, callback) => validateShippingCarNumber(value, callback), trigger: "blur" } ], expressCompany: [ { validator: (_, value, callback) => validateExpressCompany(value, callback), trigger: "blur" } ], }, }); const { form, rules } = toRefs(data); @@ -177,22 +267,85 @@ // 打开弹框 const openForm = async (type, row) => { operationType.value = type; const baseUrl = import.meta.env.VITE_APP_BASE_API; if (type === 'edit' && row) { form.value = { id: row.id ?? null, salesContractNo: row.salesContractNo ?? "", customerName: row.customerName ?? "", type: row.type || "货车", shippingDate: row.shippingDate || getCurrentDate(), shippingCarNumber: row.shippingCarNumber ?? "", expressCompany: row.expressCompany ?? "", expressNumber: row.expressNumber ?? "", }; // 如果有图片,将 commonFileList 转换为文件列表格式 if (row.commonFileList && Array.isArray(row.commonFileList) && row.commonFileList.length > 0) { deliveryFileList.value = row.commonFileList.map((file, index) => { // 处理 URL:将 Windows 路径转换为可访问的 URL let fileUrl = file.url || ''; console.log('原始 URL:', fileUrl); // 如果 URL 是 Windows 路径格式(包含反斜杠),需要转换 if (fileUrl && fileUrl.indexOf('\\') > -1) { // 查找 uploads 关键字的位置,从那里开始提取相对路径 const uploadsIndex = fileUrl.toLowerCase().indexOf('uploads'); if (uploadsIndex > -1) { // 从 uploads 开始提取路径,并将反斜杠替换为正斜杠 const relativePath = fileUrl.substring(uploadsIndex).replace(/\\/g, '/'); fileUrl = '/' + relativePath; console.log('转换后的相对路径:', fileUrl); } else { // 如果没有找到 uploads,提取最后一个目录和文件名 const parts = fileUrl.split('\\'); const fileName = parts[parts.length - 1]; fileUrl = '/uploads/' + fileName; console.log('未找到 uploads,使用文件名:', fileUrl); } } // 确保所有非 http 开头的 URL 都拼接 baseUrl if (fileUrl && !fileUrl.startsWith('http')) { // 确保路径以 / 开头 if (!fileUrl.startsWith('/')) { fileUrl = '/' + fileUrl; } // 拼接 baseUrl fileUrl = javaApi + fileUrl; console.log('最终拼接的 URL:', fileUrl); } return { uid: file.id || Date.now() + index, name: file.name || `image_${index + 1}.jpg`, url: fileUrl, status: 'success', response: { code: 200, data: { tempId: file.id, url: fileUrl } }, tempId: file.id // 保存文件ID,用于提交时使用 }; }); } else { deliveryFileList.value = []; } } else { form.value = { id: null, salesContractNo: "", customerName: "", type: "货车", shippingDate: getCurrentDate(), shippingCarNumber: "", expressCompany: "", expressNumber: "", }; deliveryFileList.value = []; } dialogFormVisible.value = true; @@ -202,10 +355,18 @@ const submitForm = () => { proxy.$refs["formRef"].validate((valid) => { if (valid) { let tempFileIds = []; if (deliveryFileList.value !== null && deliveryFileList.value.length > 0) { tempFileIds = deliveryFileList.value.map((item) => item.tempId); } const payload = { id: form.value.id, type: form.value.type, shippingDate: form.value.shippingDate, shippingCarNumber: form.value.shippingCarNumber, shippingCarNumber: form.value.type === "货车" ? form.value.shippingCarNumber : "", expressCompany: form.value.type === "快递" ? form.value.expressCompany : "", expressNumber: form.value.type === "快递" ? form.value.expressNumber : "", tempFileIds: tempFileIds, }; addOrUpdateDeliveryLedger(payload).then((res) => { proxy.$modal.msgSuccess("操作成功"); @@ -219,6 +380,7 @@ // 关闭弹框 const closeDia = () => { proxy.resetForm("formRef"); deliveryFileList.value = []; // 清空文件列表 dialogFormVisible.value = false; }; @@ -280,6 +442,89 @@ }); }; // 发货类型校验:货车时要求车牌,快递时要求快递公司 const validateShippingCarNumber = (value, callback) => { if (form.value.type === "货车") { if (!value) return callback(new Error("请输入发货车牌号")); } callback(); }; const validateExpressCompany = (value, callback) => { if (form.value.type === "快递") { if (!value) return callback(new Error("请输入快递公司")); } callback(); }; // 发货图片上传前校检 function handleDeliveryBeforeUpload(file) { // 校检文件类型 const isImage = file.type === 'image/png' || file.type === 'image/jpeg' || file.type === 'image/jpg'; if (!isImage) { proxy.$modal.msgError("只能上传 jpg、jpeg、png 格式的图片!"); return false; } // 校检文件大小 const isLt10M = file.size / 1024 / 1024 < 10; if (!isLt10M) { proxy.$modal.msgError("上传图片大小不能超过 10MB!"); return false; } proxy.$modal.loading("正在上传图片,请稍候..."); return true; } // 发货图片上传失败 function handleDeliveryUploadError(err) { proxy.$modal.msgError("上传图片失败"); proxy.$modal.closeLoading(); } // 发货图片上传成功回调 function handleDeliveryUploadSuccess(res, file, uploadFiles) { proxy.$modal.closeLoading(); if (res.code === 200) { file.tempId = res.data.tempId; proxy.$modal.msgSuccess("上传成功"); } else { proxy.$modal.msgError(res.msg); proxy.$refs.deliveryFileUpload.handleRemove(file); } } // 移除发货图片 function handleDeliveryRemove(file) { console.log('file--', file) // 如果是编辑模式且文件有 id,需要调用接口删除 if (operationType.value === "edit") { let ids = []; ids.push(file.uid); delLedgerFile(ids).then((res) => { proxy.$modal.msgSuccess("删除成功"); // 从文件列表中移除 const index = deliveryFileList.value.findIndex(item => item.uid === file.uid); if (index > -1) { deliveryFileList.value.splice(index, 1); } }).catch(() => { proxy.$modal.msgError("删除失败"); }); } else { // 新增模式或没有 id 的文件,直接从列表中移除 const index = deliveryFileList.value.findIndex(item => item.uid === file.uid); if (index > -1) { deliveryFileList.value.splice(index, 1); } } } // 发货类型切换时清空对应字段 const handleShippingTypeChange = (val) => { if (val === "货车") { form.value.expressCompany = ""; form.value.expressNumber = ""; } else { form.value.shippingCarNumber = ""; } }; onMounted(() => { getList(); }); @@ -295,5 +540,12 @@ justify-content: space-between; margin-bottom: 10px; } // 隐藏图片上传组件的预览按钮(放大镜) :deep(.el-upload-list--picture-card .el-upload-list__item-actions) { .el-upload-list__item-preview { display: none; } } </style> src/views/salesManagement/invoiceLedger/index.vue
@@ -32,7 +32,6 @@ <el-table-column align="center" label="序号" type="index" width="60" /> <el-table-column label="销售合同号" prop="salesContractNo" show-overflow-tooltip width="180" /> <el-table-column label="客户名称" prop="customerName" show-overflow-tooltip width="240" /> <!-- <el-table-column label="项目" prop="projectName" width="320" />--> <el-table-column label="产品大类" prop="productCategory" width="200" /> <el-table-column label="规格型号" prop="specificationModel" width="160" show-overflow-tooltip /> <el-table-column label="发票号" prop="invoiceNo" width="200" show-overflow-tooltip /> @@ -42,17 +41,6 @@ <el-table-column label="录入人" prop="invoicePerson" show-overflow-tooltip /> <el-table-column label="录入日期" prop="createTime" show-overflow-tooltip :formatter="formatDate" width="180" /> <el-table-column label="开票日期" prop="invoiceDate" show-overflow-tooltip width="120" /> <!-- <el-table-column label="发票" prop="invoiceFileName" width="120" align="center" show-overflow-tooltip fixed="right"> <template #default="scope"> <el-button v-if="scope.row.invoiceFileName" text bg type="primary" @click="handleFile(scope.row.commonFiles)"> 查看附件 </el-button> <el-button v-else link type="primary" @click="handleDownload(scope.row)"> 上传 </el-button> </template> </el-table-column> --> <el-table-column fixed="right" label="操作" width="150" align="center"> <template #default="scope"> <el-button link type="primary" size="small" @click="openForm(scope.row)">编辑</el-button> @@ -439,12 +427,6 @@ } onMounted(() => { // 设置开票日期范围默认值为当天 const today = dayjs().format('YYYY-MM-DD'); searchForm.invoiceDate = [today, today]; // 设置范围日期字段的起始和结束时间 searchForm.invoiceDateStart = today; searchForm.invoiceDateEnd = today; getList(); }); </script> src/views/salesManagement/salesLedger/index.vue
@@ -6,16 +6,8 @@ <el-input v-model="searchForm.customerName" placeholder="请输入" clearable prefix-icon="Search" @change="handleQuery" /> </el-form-item> <el-form-item label="客户合同号:"> <el-input v-model="searchForm.customerContractNo" placeholder="请输入" clearable prefix-icon="Search" @change="handleQuery" /> </el-form-item> <el-form-item label="销售合同号:"> <el-input v-model="searchForm.salesContractNo" placeholder="请输入" clearable prefix-icon="Search" @change="handleQuery" /> </el-form-item> <el-form-item label="项目名称:"> <el-input v-model="searchForm.projectName" placeholder="请输入" clearable prefix-icon="Search" @change="handleQuery" /> </el-form-item> <el-form-item label="录入日期:"> @@ -34,6 +26,7 @@ <el-button type="primary" @click="openForm('add')"> 新增台账 </el-button> <el-button @click="handleImport">导入</el-button> <el-button @click="handleOut">导出</el-button> <el-button type="danger" plain @click="handleDelete">删除</el-button> <el-button type="primary" plain @click="handlePrint">打印</el-button> @@ -41,7 +34,7 @@ </div> <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange" :expand-row-keys="expandedRowKeys" :row-key="(row) => row.id" show-summary style="width: 100%" :summary-method="summarizeMainTable" @expand-change="expandChange" height="calc(100vh - 18.5em)"> :summary-method="summarizeMainTable" @expand-change="expandChange" height="calc(100vh - 21em)"> <el-table-column align="center" type="selection" width="55" /> <el-table-column type="expand"> <template #default="props"> @@ -50,72 +43,80 @@ <el-table-column label="产品大类" prop="productCategory" /> <el-table-column label="规格型号" prop="specificationModel" /> <el-table-column label="单位" prop="unit" /> <el-table-column label="产品状态" width="100px" align="center"> <template #default="scope"> <el-tag v-if="scope.row.approveStatus === 0" type="info">未出库</el-tag> <el-tag v-if="scope.row.approveStatus === 1" type="success">已出库</el-tag> <el-tag v-if="scope.row.approveStatus === 2" type="warning">审核中</el-tag> <el-tag v-if="scope.row.approveStatus === 3" type="success">审核成功</el-tag> <el-tag v-if="scope.row.approveStatus === 4" type="danger">审核失败</el-tag> </template> </el-table-column> <el-table-column label="发货车牌" minWidth="100px" align="center"> <template #default="scope"> <div> <el-tag type="success" v-if="scope.row.shippingCarNumber">{{ scope.row.shippingCarNumber }}</el-tag> <el-tag v-else type="info">未发货</el-tag> </div> </template> </el-table-column> <el-table-column label="发货日期" minWidth="100px" align="center"> <template #default="scope"> <div> <div v-if="scope.row.shippingDate">{{ scope.row.shippingDate }}</div> <el-tag v-else type="info">未发货</el-tag> </div> </template> </el-table-column> <el-table-column label="数量" prop="quantity" /> <el-table-column label="税率(%)" prop="taxRate" /> <el-table-column label="含税单价(元)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" /> <el-table-column label="含税总价(元)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" /> <el-table-column label="不含税总价(元)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" /> <!--操作--> <el-table-column Width="60px" label="操作" align="center"> <el-table-column label="产品状态" width="100px" align="center"> <template #default="scope"> <el-button :disabled="scope.row.approveStatus!==2 || scope.row.approveStatus!==5" link type="primary" size="small" @click="openDeliveryForm(scope.row)">发货</el-button> <el-tag v-if="scope.row.approveStatus === 1" type="success">充足</el-tag> <el-tag v-else type="danger">不足</el-tag> </template> </el-table-column> <el-table-column label="发货状态" prop="shippingStatus" width="140" align="center" show-overflow-tooltip /> <el-table-column label="发货日期" minWidth="100px" align="center"> <template #default="scope"> <div> <div v-if="scope.row.shippingDate">{{ scope.row.shippingDate }}</div> <el-tag v-else type="info">-</el-tag> </div> </template> </el-table-column> <el-table-column Width="60px" label="操作" align="center"> <template #default="scope"> <el-button link type="primary" size="small" @click="openDeliveryForm(scope.row)">发货</el-button> </template> </el-table-column> </el-table> </template> </el-table-column> <el-table-column align="center" label="序号" type="index" width="60" /> <el-table-column label="销售合同号" prop="salesContractNo" width="180" show-overflow-tooltip /> <el-table-column label="客户合同号" prop="customerContractNo" width="180" show-overflow-tooltip /> <el-table-column label="客户名称" prop="customerName" width="300" show-overflow-tooltip /> <el-table-column label="销售合同号" prop="salesContractNo" show-overflow-tooltip /> <el-table-column label="客户名称" prop="customerName" show-overflow-tooltip /> <el-table-column label="业务员" prop="salesman" width="100" show-overflow-tooltip /> <el-table-column label="项目名称" prop="projectName" width="180" show-overflow-tooltip /> <el-table-column label="付款方式" prop="paymentMethod" show-overflow-tooltip /> <el-table-column label="合同金额(元)" prop="contractAmount" width="220" show-overflow-tooltip :formatter="formattedNumber" /> <el-table-column label="录入人" prop="entryPersonName" width="100" show-overflow-tooltip /> <el-table-column label="录入日期" prop="entryDate" width="120" show-overflow-tooltip /> <el-table-column label="签订日期" prop="executionDate" width="120" show-overflow-tooltip /> <el-table-column fixed="right" label="操作" min-width="100" align="center"> <el-table-column label="录入日期" prop="entryDate" width="120" show-overflow-tooltip /> <el-table-column fixed="right" label="操作" width="120" align="center"> <template #default="scope"> <el-button link type="primary" size="small" @click="openForm('edit', scope.row)">编辑</el-button> <!-- <el-button link type="primary" size="small" @click="openForm('view', scope.row)">详情</el-button>--> <el-button link type="primary" size="small" @click="downLoadFile(scope.row)">附件</el-button> <!-- <el-button link type="primary" size="small" @click="openDeliveryForm(scope.row)">发货</el-button>--> </template> </el-table-column> </el-table> <pagination v-show="total > 0" :total="total" layout="total, sizes, prev, pager, next, jumper" :page="page.current" :limit="page.size" @pagination="paginationChange" /> </div> <el-dialog v-model="dialogFormVisible" :title="operationType === 'add' ? '新增销售台账页面' : '编辑销售台账页面'" width="70%" @close="closeDia"> <FormDialog v-model="dialogFormVisible" :title="operationType === 'add' ? '新增销售台账页面' : '编辑销售台账页面'" :width="'70%'" :operation-type="operationType" @close="closeDia" @confirm="submitForm" @cancel="closeDia"> <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef"> <el-row v-if="operationType !== 'view'"> <el-col :span="24" style="display:flex; justify-content:flex-end; gap:10px; margin-bottom: 6px;"> <el-button type="primary" plain @click="openQuotationDialog">从审批通过的报价单导入</el-button> </el-col> </el-row> <el-row :gutter="30"> <el-col :span="12"> <el-form-item label="销售合同号:" prop="salesContractNo"> @@ -124,7 +125,9 @@ </el-col> <el-col :span="12"> <el-form-item label="业务员:" prop="salesman"> <el-select v-model="form.salesman" placeholder="请选择" clearable :disabled="operationType === 'view'"> <el-select v-model="form.salesman" filterable :reserve-keyword="false" placeholder="请选择" clearable :disabled="operationType === 'view'"> <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName" /> </el-select> @@ -133,26 +136,14 @@ </el-row> <el-row :gutter="30"> <el-col :span="12"> <el-form-item label="客户合同号:" prop="customerContractNo"> <el-input v-model="form.customerContractNo" placeholder="请输入" clearable :disabled="operationType === 'view'"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="客户名称:" prop="customerId"> <el-select v-model="form.customerId" placeholder="请选择" clearable :disabled="operationType === 'view'"> <el-select v-model="form.customerId" placeholder="请选择" clearable :disabled="operationType === 'view'" filterable> <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.id"> {{ item.customerName + "——" + item.taxpayerIdentificationNumber }} </el-option> </el-select> </el-form-item> </el-col> </el-row> <el-row :gutter="30"> <el-col :span="12"> <el-form-item label="项目名称:" prop="projectName"> <el-input v-model="form.projectName" placeholder="请输入" clearable :disabled="operationType === 'view'" /> </el-form-item> </el-col> <el-col :span="12"> @@ -165,7 +156,10 @@ <el-row :gutter="30"> <el-col :span="12"> <el-form-item label="录入人:" prop="entryPerson"> <el-select v-model="form.entryPerson" placeholder="请选择" clearable @change="changs" disabled> <el-select v-model="form.entryPerson" filterable default-first-option :reserve-keyword="false" placeholder="请选择" clearable @change="changs"> <el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.userId" /> </el-select> </el-form-item> @@ -177,13 +171,7 @@ </el-form-item> </el-col> </el-row> <el-row :gutter="30"> <el-col :span="12"> <el-form-item label="付款方式"> <el-input v-model="form.paymentMethod" placeholder="请输入" clearable :disabled="operationType === 'view'" /> </el-form-item> </el-col> </el-row> <el-row> <el-form-item label="产品信息:" prop="entryDate"> <el-button v-if="operationType !== 'view'" type="primary" @click="openProductForm('add')">添加</el-button> @@ -233,14 +221,71 @@ </el-col> </el-row> </el-form> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="submitForm">确认</el-button> <el-button @click="closeDia">取消</el-button> </FormDialog> <!-- 从报价单导入(仅审批通过) --> <el-dialog v-model="quotationDialogVisible" title="选择审批通过的销售报价单" width="80%" :close-on-click-modal="false" > <div style="margin-bottom: 12px; display:flex; gap: 12px; align-items:center;"> <el-input v-model="quotationSearchForm.quotationNo" placeholder="请输入报价单号" clearable style="max-width: 260px;" @change="fetchQuotationList" /> <el-input v-model="quotationSearchForm.customer" placeholder="请输入客户名称" clearable style="max-width: 260px;" @change="fetchQuotationList" /> <el-button type="primary" @click="fetchQuotationList">搜索</el-button> <el-button @click="resetQuotationSearch">重置</el-button> </div> <el-table :data="quotationList" border stripe v-loading="quotationLoading" height="420px" > <el-table-column align="center" label="序号" type="index" width="60" /> <el-table-column prop="quotationNo" label="报价单号" width="180" show-overflow-tooltip /> <el-table-column prop="customer" label="客户名称" min-width="220" show-overflow-tooltip /> <el-table-column prop="salesperson" label="业务员" width="120" show-overflow-tooltip /> <el-table-column prop="quotationDate" label="报价日期" width="140" /> <el-table-column prop="status" label="审批状态" width="120" align="center" /> <el-table-column prop="totalAmount" label="报价金额(元)" width="160" align="right"> <template #default="scope"> {{ Number(scope.row.totalAmount ?? 0).toFixed(2) }} </template> </el-table-column> <el-table-column fixed="right" label="操作" width="120" align="center"> <template #default="scope"> <el-button type="primary" link @click="applyQuotation(scope.row)">选择</el-button> </template> </el-table-column> </el-table> <template #footer> <el-button @click="quotationDialogVisible = false">关闭</el-button> </template> </el-dialog> <el-dialog v-model="productFormVisible" :title="productOperationType === 'add' ? '新增产品' : '编辑产品'" width="40%" @close="closeProductDia"> <FormDialog v-model="productFormVisible" :title="productOperationType === 'add' ? '新增产品' : '编辑产品'" :width="'40%'" :operation-type="productOperationType" @close="closeProductDia" @confirm="submitProduct" @cancel="closeProductDia"> <el-form :model="productForm" label-width="140px" label-position="top" :rules="productRules" ref="productFormRef"> <el-row :gutter="30"> <el-col :span="24"> @@ -256,7 +301,7 @@ <el-row :gutter="30"> <el-col :span="24"> <el-form-item label="规格型号:" prop="productModelId"> <el-select v-model="productForm.productModelId" placeholder="请选择" clearable @change="getProductModel"> <el-select v-model="productForm.productModelId" placeholder="请选择" clearable @change="getProductModel" filterable> <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" /> </el-select> </el-form-item> @@ -317,13 +362,7 @@ </el-col> </el-row> </el-form> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="submitProduct">确认</el-button> <el-button @click="closeProductDia">取消</el-button> </div> </template> </el-dialog> </FormDialog> <!-- 打印预览弹窗 --> <el-dialog v-model="printPreviewVisible" @@ -358,12 +397,15 @@ <span class="value">{{ formatDate(item.createTime) }}</span> </div> <div> <span class="label">客户名称:</span> <span class="value">{{ item.customerName || '张爱有' }}</span> <span class="label">发货车牌号:</span> <span class="value">{{ item.shippingCarNumber }}</span> </div> </div> <div class="info-row"> <div> <span class="label">客户名称:</span> <span class="value">{{ item.customerName || '张爱有' }}</span> </div> <span class="label">单号:</span> <span class="value">{{ item.salesContractNo }}</span> </div> @@ -448,36 +490,60 @@ <el-form :model="deliveryForm" label-width="120px" label-position="top" :rules="deliveryRules" ref="deliveryFormRef"> <el-row :gutter="30"> <el-col :span="24"> <el-form-item label="发货日期:" prop="shippingDate"> <el-date-picker <el-form-item label="发货类型:" prop="type"> <el-select v-model="deliveryForm.type" placeholder="请选择发货类型" style="width: 100%" v-model="deliveryForm.shippingDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="date" placeholder="请选择发货日期" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="30"> <el-col :span="24"> <el-form-item label="发货车牌号:" prop="shippingCarNumber"> <el-input v-model="deliveryForm.shippingCarNumber" placeholder="请输入发货车牌号" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="30"> <el-col :span="24"> <el-form-item label="审批人:" prop="approverId"> <el-select v-model="deliveryForm.approverId" placeholder="请选择审批人" clearable :disabled="operationType === 'view'"> <el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.userId" /> > <el-option label="货车" value="货车" /> <el-option label="快递" value="快递" /> </el-select> </el-form-item> </el-col> </el-row> <!-- 审批人选择(仿协同审批里的审批人节点选择) --> <el-row> <el-col :span="24"> <el-form-item> <template #label> <span>审批人选择:</span> <el-button type="primary" @click="addApproverNode" style="margin-left: 8px;">新增节点</el-button> </template> <div style="display: flex; align-items: flex-end; flex-wrap: wrap;"> <div v-for="(node, index) in approverNodes" :key="node.id" style="margin-right: 20px; text-align: center; margin-bottom: 10px;" > <div> <span>审批人</span> → </div> <el-select v-model="node.userId" placeholder="选择人员" filterable style="width: 140px; margin-bottom: 8px;" > <el-option v-for="user in userList" :key="user.userId" :label="user.nickName" :value="user.userId" /> </el-select> <div> <el-button type="danger" size="small" @click="removeApproverNode(index)" v-if="approverNodes.length > 1" >删除</el-button> </div> </div> </div> </el-form-item> </el-col> </el-row> @@ -486,44 +552,6 @@ <div class="dialog-footer"> <el-button type="primary" @click="submitDelivery">确认发货</el-button> <el-button @click="closeDeliveryDia">取消</el-button> </div> </template> </el-dialog> <FileListDialog ref="fileListRef" v-model="fileListDialogVisible" /> <!-- 导入对话框 --> <el-dialog :title="importUpload.title" v-model="importUpload.open" width="400px" append-to-body > <el-upload ref="importUploadRef" :limit="1" accept=".xlsx, .xls" :headers="importUpload.headers" :action="importUpload.url" :disabled="importUpload.isUploading" :before-upload="importUpload.beforeUpload" :on-progress="importUpload.onProgress" :on-success="importUpload.onSuccess" :on-error="importUpload.onError" :on-change="importUpload.onChange" :auto-upload="false" drag > <el-icon class="el-icon--upload"><UploadFilled /></el-icon> <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> <template #tip> <div class="el-upload__tip text-center"> <span>仅允许导入xls、xlsx格式文件。</span> </div> </template> </el-upload> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="submitImportFile" :loading="importUpload.isUploading">确 定</el-button> <el-button @click="importUpload.open = false">取 消</el-button> </div> </template> </el-dialog> @@ -536,10 +564,12 @@ import {onMounted, ref, getCurrentInstance} from "vue"; import { addShippingInfo } from "@/api/salesManagement/deliveryLedger.js"; import { ElMessageBox, ElMessage } from "element-plus"; import { UploadFilled } from "@element-plus/icons-vue"; import { UploadFilled, Download } from "@element-plus/icons-vue"; import useUserStore from "@/store/modules/user"; import { userListNoPage } from "@/api/system/user.js"; import FileList from "./fileList.vue"; import FileListDialog from '@/components/Dialog/FileListDialog.vue'; import FormDialog from '@/components/Dialog/FormDialog.vue'; import { getQuotationList } from "@/api/salesManagement/salesQuotation.js"; import { ledgerListPage, productList, @@ -554,6 +584,7 @@ import { modelList, productTreeList } from "@/api/basicData/product.js"; import useFormData from "@/hooks/useFormData.js"; import dayjs from "dayjs"; import { getCurrentDate } from "@/utils/index.js"; const userStore = useUserStore(); const { proxy } = getCurrentInstance(); @@ -579,9 +610,7 @@ const data = reactive({ searchForm: { customerName: "", // 客户名称 customerContractNo: "", // 客户合同编号 salesContractNo: "", // 销售合同编号 projectName: "", // 项目名称 entryDate: null, // 录入日期 entryDateStart: undefined, entryDateEnd: undefined, @@ -589,23 +618,16 @@ form: { salesContractNo: "", salesman: "", customerContractNo: "", customerId: "", projectName: "", entryPerson: "", entryDate: "", maintenanceTime: "", productData: [], executionDate: "", paymentMethod: "", }, rules: { salesman: [{ required: true, message: "请选择", trigger: "change" }], customerContractNo: [ { required: true, message: "请输入", trigger: "blur" }, ], customerId: [{ required: true, message: "请选择", trigger: "change" }], projectName: [{ required: true, message: "请输入", trigger: "blur" }], entryPerson: [{ required: true, message: "请选择", trigger: "change" }], entryDate: [{ required: true, message: "请选择", trigger: "change" }], executionDate: [{ required: true, message: "请选择", trigger: "change" }], @@ -663,29 +685,40 @@ const printPreviewVisible = ref(false); const printData = ref([]); // 报价单导入相关 const quotationDialogVisible = ref(false); const quotationLoading = ref(false); const quotationList = ref([]); const quotationSearchForm = reactive({ quotationNo: "", customer: "", }); const selectedQuotation = ref(null); // 发货相关 const deliveryFormVisible = ref(false); const currentDeliveryRow = ref(null); const deliveryFormData = reactive({ deliveryForm: { shippingDate: "", shippingCarNumber: "", type: "货车", // 货车, 快递 }, deliveryRules: { shippingDate: [ { required: true, message: "请选择发货日期", trigger: "change" } ], shippingCarNumber: [ { required: true, message: "请输入发货车牌号", trigger: "blur" } ], approverId:[ { required: true,message: "", } type: [ { required: true, message: "请选择发货类型", trigger: "change" } ] }, }); const { deliveryForm, deliveryRules } = toRefs(deliveryFormData); // 发货审批人节点(仿协同审批 infoFormDia.vue) const approverNodes = ref([{ id: 1, userId: null }]); let nextApproverId = 2; const addApproverNode = () => { approverNodes.value.push({ id: nextApproverId++, userId: null }); }; const removeApproverNode = (index) => { approverNodes.value.splice(index, 1); }; // 导入相关 const importUploadRef = ref(null); @@ -749,7 +782,11 @@ // 查询列表 /** 搜索按钮操作 */ const handleQuery = () => { // 只有在点击搜索按钮时才重置页码到第一页 // 避免表单字段change事件干扰分页 if (arguments.length === 0) { page.current = 1; } expandedRowKeys.value = []; getList(); }; @@ -758,12 +795,14 @@ page.size = obj.limit; getList(); }; const getList =async () => { let userLists = await userListNoPage(); userList.value = userLists.data; const getList = () => { tableLoading.value = true; const { entryDate, ...rest } = searchForm; ledgerListPage({ ...rest, ...page }) // 将范围日期字段传递给后端 const params = { ...rest, ...page }; // 移除录入日期的默认值设置,只保留范围日期字段 delete params.entryDate; ledgerListPage(params) .then((res) => { tableLoading.value = false; tableData.value = res.records; @@ -778,8 +817,10 @@ }; // 获取产品大类tree数据 const getProductOptions = () => { productTreeList().then((res) => { // 返回 Promise,便于在编辑产品时等待加载完成 return productTreeList().then((res) => { productOptions.value = convertIdToValue(res); return productOptions.value; }); }; const formattedNumber = (row, column, cellValue) => { @@ -817,7 +858,6 @@ return null; // 没有找到节点,返回null }; function convertIdToValue(data) { if (!data || !Array.isArray(data)) return []; return data.map((item) => { const { id, children, ...rest } = item; const newItem = { @@ -831,6 +871,19 @@ return newItem; }); } // 根据名称反查产品大类 id,便于仅存名称时的反显 function findNodeIdByLabel(nodes, label) { if (!label) return null; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node.label === label) return node.value; if (node.children && node.children.length > 0) { const found = findNodeIdByLabel(node.children, label); if (found !== null && found !== undefined) return found; } } return null; } // 表格选择数据 const handleSelectionChange = (selection) => { // 过滤掉子数据 @@ -841,31 +894,23 @@ productSelectedRows.value = selectedRows; }; const expandedRowKeys = ref([]); // 展开行(始终只展开一行) const expandChange = (row) => { const rowKey = row.id; const isExpanded = expandedRowKeys.value.includes(rowKey); if (isExpanded) { // 当前行已展开 -> 收起 // 展开行 const expandChange = (row, expandedRows) => { if (expandedRows.length > 0) { expandedRowKeys.value = []; return; } // 展开当前行前,先收起其它行 expandedRowKeys.value = []; try { productList({ salesLedgerId: row.id, type: 1 }).then((res) => { const index = tableData.value.findIndex((item) => item.id === row.id); if (index > -1) { tableData.value[index].children = res.data; } // 只保留当前这一行处于展开状态 expandedRowKeys.value = [rowKey]; expandedRowKeys.value.push(row.id); }); } catch (error) { console.log(error); } } else { expandedRowKeys.value = []; } }; // 主表合计方法 @@ -889,11 +934,19 @@ operationType.value = type; form.value = {}; productData.value = []; selectedQuotation.value = null; let userLists = await userListNoPage(); userList.value = userLists.data; customerList().then((res) => { customerOption.value = res; }); form.value.entryPerson = userStore.id; if (type !== "add") { if (type === "add") { // 新增时设置录入日期为当天 form.value.entryDate = getCurrentDate(); // 签订日期默认为当天 form.value.executionDate = getCurrentDate(); } else { currentId.value = row.id; getSalesLedgerWithProducts({ id: row.id, type: 1 }).then((res) => { form.value = { ...res }; @@ -910,6 +963,87 @@ // }); form.value.entryDate = getCurrentDate(); // 设置默认录入日期为当前日期 dialogFormVisible.value = true; }; // 打开报价单选择弹窗(仅审批通过) const openQuotationDialog = async () => { if (operationType.value === "view") return; quotationDialogVisible.value = true; // 先确保客户列表已加载,便于后续回填 customerId if (!customerOption.value || customerOption.value.length === 0) { try { const res = await customerList(); customerOption.value = res; } catch (e) { // ignore,允许用户后续手动选择客户 } } await fetchQuotationList(); }; const fetchQuotationList = async () => { quotationLoading.value = true; try { const params = { // 兼容后端分页字段:这里沿用报价页面已有可用的字段命名 currentPage: 1, pageSize: 100, ...quotationSearchForm, status: "通过", }; const res = await getQuotationList(params); quotationList.value = res?.data?.records || []; } finally { quotationLoading.value = false; } }; const resetQuotationSearch = async () => { quotationSearchForm.quotationNo = ""; quotationSearchForm.customer = ""; await fetchQuotationList(); }; // 选中报价单后回填到台账表单 const applyQuotation = (row) => { if (!row) return; selectedQuotation.value = row; // 业务员 form.value.salesman = row.salesperson || ""; // 客户名称 -> customerId const customer = (customerOption.value || []).find((c) => c.customerName === row.customer); if (customer?.id) { form.value.customerId = customer.id; } else { // 如果找不到,保留原值(允许用户手动选择/不打断已有输入) form.value.customerId = form.value.customerId || ""; } // 产品信息映射:报价 products -> 台账 productData const products = Array.isArray(row.products) ? row.products : []; productData.value = products.map((p) => { const quantity = Number(p.quantity ?? 0) || 0; const unitPrice = Number(p.unitPrice ?? 0) || 0; const taxRate = "13"; // 默认 13%,便于直接提交(如需可在产品中自行修改) const taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2); const taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(taxInclusiveTotalPrice, taxRate); return { // 台账字段 productCategory: p.product || p.productName || "", specificationModel: p.specification || "", unit: p.unit || "", quantity: quantity, taxRate: taxRate, taxInclusiveUnitPrice: unitPrice.toFixed(2), taxInclusiveTotalPrice: taxInclusiveTotalPrice, taxExclusiveTotalPrice: taxExclusiveTotalPrice, invoiceType: "增普票", }; }); quotationDialogVisible.value = false; }; function changs(val) { console.log(val); @@ -987,18 +1121,32 @@ productOperationType.value = type; productForm.value = {}; proxy.resetForm("productFormRef"); // 新增、编辑都需先加载产品树,否则 el-tree-select 无数据 try { await getProductOptions(); } catch (e) { console.error("加载产品树失败", e); } if (type === "edit") { productForm.value = { ...row }; productIndex.value = index; // 编辑时根据产品大类名称反查 tree 节点 id,并加载规格型号列表 try { const options = productOptions.value && productOptions.value.length > 0 ? productOptions.value : await getProductOptions(); const categoryId = findNodeIdByLabel(options, productForm.value.productCategory); if (categoryId) { const models = await modelList({ id: categoryId }); modelOptions.value = models || []; // 根据当前规格型号名称反查并设置 productModelId,便于下拉框显示已选值 const currentModel = (modelOptions.value || []).find( (m) => m.model === productForm.value.specificationModel ); if (currentModel) { productForm.value.productModelId = currentModel.id; } } } catch (e) { // 加载失败时保持可编辑,不中断弹窗 console.error("加载产品规格型号失败", e); } } productFormVisible.value = true; getProductOptions(); }; // 提交产品表单 const submitProduct = () => { @@ -1081,6 +1229,11 @@ if (importUploadRef.value) { importUploadRef.value.clearFiles(); } }; // 下载导入模板 const downloadTemplate = () => { proxy.download("/sales/ledger/exportTemplate", {}, "销售台账导入模板.xlsx"); }; // 提交导入文件 @@ -1454,15 +1607,6 @@ const seconds = String(date.getSeconds()).padStart(2, "0"); return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`; }; // 获取当前日期并格式化为 YYYY-MM-DD function getCurrentDate() { const today = new Date(); const year = today.getFullYear(); const month = String(today.getMonth() + 1).padStart(2, "0"); // 月份从0开始 const day = String(today.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; } // 计算产品总数量 const getTotalQuantity = (products) => { if (!products || products.length === 0) return '0'; @@ -1675,9 +1819,13 @@ * @param row 下载文件的相关信息对象 */ const fileListRef = ref(null) const fileListDialogVisible = ref(false) const downLoadFile = (row) => { getSalesLedgerWithProducts({ id: row.id, type: 1 }).then((res) => { if (fileListRef.value) { fileListRef.value.open(res.salesLedgerFiles) fileListDialogVisible.value = true } }); } @@ -1685,9 +1833,11 @@ const openDeliveryForm = (row) => { currentDeliveryRow.value = row; deliveryForm.value = { shippingDate: "", // 移除默认值设置 shippingCarNumber: "", type: "货车", }; // 重置审批人节点(默认一个空节点) approverNodes.value = [{ id: 1, userId: null }]; nextApproverId = 2; deliveryFormVisible.value = true; }; @@ -1695,22 +1845,24 @@ const submitDelivery = () => { proxy.$refs["deliveryFormRef"].validate((valid) => { if (valid) { // 审批人必填校验(所有节点都要选人) const hasEmptyApprover = approverNodes.value.some(node => !node.userId); if (hasEmptyApprover) { proxy.$modal.msgError("请为所有审批节点选择审批人!"); return; } const approveUserIds = approverNodes.value.map(node => node.userId).join(","); addShippingInfo({ approverId:deliveryForm.value.approverId, salesLedgerId: currentDeliveryRow.value.salesLedgerId, salesLedgerProductId: currentDeliveryRow.value.id, shippingDate: deliveryForm.value.shippingDate, shippingCarNumber: deliveryForm.value.shippingCarNumber, type: deliveryForm.value.type, approveUserIds, }) .then(() => { proxy.$modal.msgSuccess("发货成功"); closeDeliveryDia(); getList(); expandedRowKeys.value = []; }) .catch(() => { proxy.$modal.msgError("发货失败,请重试"); }); } }); }; @@ -1721,9 +1873,17 @@ deliveryFormVisible.value = false; currentDeliveryRow.value = null; }; const currentFactoryName = ref(""); const getCurrentFactoryName = async () => { let res = await userStore.getInfo(); currentFactoryName.value = res.user.currentFactoryName; }; onMounted(() => { getList(); userListNoPage().then(res => { userList.value = res.data; }) getCurrentFactoryName(); }); </script>