feat(bid): 新增甲方履约订单管理模块

1.  新增甲方履约菜单分类,包含待发、在途、签收三个子菜单并配置权限
2.  重构发货单号生成逻辑,支持区分供应商和甲方履约订单前缀
3.  新增甲方发货单生成功能,可从确认的甲方报价单一键创建
4.  新增京东红主题样式并支持快速切换
5.  优化物料发货记录查询,兼容两种履约订单的客户信息关联
6.  修复订单详情弹窗的空值判断和异常捕获逻辑
7.  新增配套SQL脚本用于菜单初始化和数据修复
This commit is contained in:
2026-06-15 11:09:56 +08:00
parent 8393e4940d
commit 24ab178ec1
14 changed files with 470 additions and 34 deletions

View File

@@ -5,6 +5,11 @@
@import './sidebar.scss';
@import './btn.scss';
/* ═══════════════════════════════════════════
主题系统 — 取消注释即可切换
═══════════════════════════════════════════ */
// @import './theme-jd.scss'; /* 京东红主题 */
body {
height: 100%;
-moz-osx-font-smoothing: grayscale;

View File

@@ -0,0 +1,83 @@
/* ═══════════════════════════════════════════
京东风格主题 — CSS 变量定义
主色:红色 #E2231A
辅色:白色 + 银灰色
用途:替换页面中所有硬编码的颜色值
═══════════════════════════════════════════ */
:root {
/* ═══ 品牌色(京东红) ═══ */
--brand-primary: #E2231A; /* 京东红 —— 主按钮、链接、选中态 */
--brand-primary-hover: #C01623; /* 深红 —— hover、激活 */
--brand-primary-light: #FFF0F0; /* 浅红背景 —— 选中行、标签浅色背景 */
/* ═══ 银灰色(边框、背景、禁用) ═══ */
--silver-border: #E0E0E0; /* 表格边框、卡片边框 */
--silver-border-light: #F0F0F0; /* 更浅的边框 */
--silver-bg: #F5F5F5; /* 页面背景、表头背景、卡片背景 */
--silver-bg-light: #FAFAFA; /* 交替行背景、悬浮背景 */
--silver-text: #9E9E9E; /* 禁用文字、placeholder、辅助信息 */
/* ═══ 背景色 ═══ */
--bg-white: #FFFFFF; /* 纯白 — 内容区、卡片、弹窗 */
--bg-page: #F5F5F5; /* 页面底色 */
/* ═══ 文字色 ═══ */
--text-primary: #333333; /* 主文字 —— 标题、表格数据 */
--text-secondary: #666666; /* 次要文字 —— 说明、次要信息 */
--text-muted: #999999; /* 弱化文字 —— 辅助信息 */
/* ═══ 金额色 ═══ */
--color-amount: #E2231A; /* 金额 —— 京东红色 */
/* ═══ 状态色 ═══ */
--color-success: #67C23A; /* 成功绿 */
--color-warning: #E6A23C; /* 警告橙 */
--color-danger: #F56C6C; /* 危险红 */
/* ═══ 圆角 ═══ */
--radius-base: 4px; /* 常规圆角 */
--radius-small: 2px; /* 小圆角 */
/* ═══ 阴影 ═══ */
--shadow-card: 0 1px 4px rgba(0,0,0,0.06);
--shadow-dialog: 0 4px 12px rgba(0,0,0,0.12);
/* ═══ 间距 ═══ */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 12px;
--space-lg: 16px;
--space-xl: 20px;
/* ═══ 表格 ═══ */
--table-header-bg: #F5F5F5;
--table-header-text: #333333;
--table-row-hover: #FFF0F0; /* 浅红悬浮 */
--table-stripe: #FAFAFA;
--table-border: #E0E0E0;
/* ═══ 搜索栏 / 卡片 ═══ */
--card-bg: #FFFFFF;
--card-border: #E0E0E0;
}
/* ═══ 部分 Element UI 覆盖 ═══ */
.el-button--primary {
background-color: var(--brand-primary) !important;
border-color: var(--brand-primary) !important;
}
.el-button--primary:hover,
.el-button--primary:focus {
background-color: var(--brand-primary-hover) !important;
border-color: var(--brand-primary-hover) !important;
}
.el-table thead th {
background-color: var(--table-header-bg) !important;
color: var(--table-header-text) !important;
}
.el-table__body .el-table__row:hover td {
background-color: var(--table-row-hover) !important;
}

View File

@@ -248,6 +248,26 @@ export const dynamicRoutes = [
}]
},
// ── 甲方履约 ──
{
path: '/bid/clientDelivery/pending',
component: Layout,
permissions: ['bid:clientdelivery:pending'],
children: [{ path: '', component: () => import('@/views/bid/clientDelivery/pending'), name: 'ClientDeliveryPending', meta: { title: '甲方待发', activeMenu: '/bid/clientDelivery' } }]
},
{
path: '/bid/clientDelivery/transit',
component: Layout,
permissions: ['bid:clientdelivery:transit'],
children: [{ path: '', component: () => import('@/views/bid/clientDelivery/transit'), name: 'ClientDeliveryTransit', meta: { title: '甲方在途', activeMenu: '/bid/clientDelivery' } }]
},
{
path: '/bid/clientDelivery/signed',
component: Layout,
permissions: ['bid:clientdelivery:signed'],
children: [{ path: '', component: () => import('@/views/bid/clientDelivery/signed'), name: 'ClientDeliverySigned', meta: { title: '甲方签收', activeMenu: '/bid/clientDelivery' } }]
},
{
path: '/bid/comparison/detail',
component: Layout,

View File

@@ -0,0 +1,87 @@
<template>
<div class="order-page">
<div class="page-header">
<span class="page-title">甲方待发</span>
<el-tag type="warning" size="small" effect="dark">STATUS: PENDING</el-tag>
</div>
<div class="search-bar">
<el-input v-model="q.doNo" placeholder="搜索发货单号" clearable size="small" style="width:150px" @keyup.enter.native="search" />
<el-input v-model="q.clientName" placeholder="搜索甲方客户" clearable size="small" style="width:160px" @keyup.enter.native="search" />
<el-button type="primary" size="small" icon="el-icon-search" @click="search">搜索</el-button>
<el-button size="small" icon="el-icon-refresh" @click="resetSearch">重置</el-button>
<div class="sr"><el-button size="small" icon="el-icon-refresh" @click="getList">刷新</el-button></div>
</div>
<el-table v-loading="loading" :data="list" border stripe size="small" style="width:100%">
<el-table-column label="发货单号" prop="doNo" width="165" />
<el-table-column label="甲方客户" prop="clientName" min-width="160" show-overflow-tooltip />
<el-table-column label="金额" width="120" align="right"><template slot-scope="s"><span class="amount">¥{{ s.row.totalAmount }}</span></template></el-table-column>
<el-table-column label="交货期" prop="deliveryDate" width="95" align="center" />
<el-table-column label="物料" prop="itemCount" width="55" align="center" />
<el-table-column label="状态" width="90" align="center"><template slot-scope="s"><el-tag type="warning" size="small" effect="dark">待发</el-tag></template></el-table-column>
<el-table-column label="操作" width="180" align="center">
<template slot-scope="s">
<el-button size="mini" type="text" @click="handleView(s.row)">详情</el-button>
<el-button size="mini" type="text" style="color:#67C23A" @click="handleShip(s.row)">发货确认</el-button>
<el-button size="mini" type="text" style="color:#f56c6c" @click="handleDelete(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="q.pageNum" :limit.sync="q.pageSize" @pagination="getList" />
<el-dialog title="发货单详情" :visible.sync="detailOpen" width="780px" append-to-body>
<div>
<div class="detail-grid">
<div class="detail-item"><span class="dl">发货单号</span><span class="dv"><b>{{ detailData ? detailData.doNo : '' }}</b></span></div>
<div class="detail-item"><span class="dl">甲方客户</span><span class="dv">{{ (detailData && detailData.clientName) || '-' }}</span></div>
<div class="detail-item"><span class="dl">总金额</span><span class="dv" style="color:#409EFF">¥{{ detailData ? detailData.totalAmount : 0 }}</span></div>
<div class="detail-item"><span class="dl">状态</span><span class="dv"><el-tag type="warning" size="small" effect="dark">待发</el-tag></span></div>
<div class="detail-item"><span class="dl">交货期</span><span class="dv">{{ (detailData && detailData.deliveryDate) || '-' }}</span></div>
<div class="detail-item"><span class="dl">备注</span><span class="dv">{{ (detailData && detailData.remark) || '-' }}</span></div>
</div>
<div v-if="detailData" class="section-bar">物料明细</div>
<el-table v-if="detailData" :data="detailData.items || []" border size="small">
<el-table-column label="物料名称" prop="materialName" min-width="150" />
<el-table-column label="规格" prop="spec" width="120" />
<el-table-column label="单位" prop="unit" width="60" />
<el-table-column label="数量" prop="quantity" width="80" align="right" />
<el-table-column label="单价" width="100" align="right"><template slot-scope="s">¥{{ s.row.unitPrice }}</template></el-table-column>
<el-table-column label="小计" width="100" align="right"><template slot-scope="s">¥{{ s.row.totalPrice }}</template></el-table-column>
</el-table>
</div>
<div slot="footer"><el-button @click="detailOpen = false">关闭</el-button></div>
</el-dialog>
</div>
</template>
<script>
import { listDelivery, getDelivery, delDelivery, shipDelivery } from "@/api/bid/delivery"
export default {
name: "ClientDeliveryPending",
data() { return {
loading: false, list: [], total: 0,
q: { pageNum: 1, pageSize: 20, type: "client", deliveryStatus: "pending", doNo: "", clientName: "" },
detailOpen: false, detailData: null
}},
created() { this.getList() },
methods: {
getList() { this.loading=true; listDelivery(this.q).then(r=>{this.list=(r.rows||[]).map(d=>({...d,deliveryDate:d.deliveryDate?d.deliveryDate.substring(0,10):''}));this.total=r.total||0;this.loading=false}).catch(()=>{this.loading=false}) },
search() { this.q.pageNum=1; this.getList() }, resetSearch() { this.q.doNo=""; this.q.clientName=""; this.search() },
handleView(row) { getDelivery(row.doId).then(r=>{this.detailData=r.data;this.detailOpen=true}).catch(()=>{}) },
handleShip(row) { this.$modal.confirm("确认发货?").then(()=>shipDelivery(row.doId)).then(()=>{this.$modal.msgSuccess("已发货");this.getList()}).catch(()=>{}) },
handleDelete(row) { this.$modal.confirm("确认删除?").then(()=>delDelivery(row.doId)).then(()=>{this.$modal.msgSuccess("已删除");this.getList()}).catch(()=>{}) }
}
}
</script>
<style scoped>
.order-page { background:#f5f7fa; padding:12px; min-height:calc(100vh - 84px); }
.page-header { background:#fff; padding:12px 16px; border-radius:4px; margin-bottom:12px; display:flex; align-items:center; gap:12px; }
.page-title { font-size:16px; font-weight:700; color:#1a2c4e; }
.search-bar { background:#fff; padding:12px 16px; border-radius:4px; margin-bottom:12px; display:flex; align-items:center; gap:8px; }
.sr { margin-left:auto; }
.amount { color:#409EFF; font-weight:700; }
.detail-grid { display:grid; grid-template-columns:1fr 1fr; gap:0; border:1px solid #ebeef5; border-radius:4px; margin-bottom:16px; }
.detail-item { display:flex; border-bottom:1px solid #ebeef5; }
.detail-item:nth-last-child(-n+2) { border-bottom:none; }
.detail-item:nth-child(odd) { border-right:1px solid #ebeef5; }
.dl { width:90px; flex-shrink:0; background:#f5f7fa; padding:10px 12px; font-size:12px; color:#606266; font-weight:600; border-right:1px solid #ebeef5; }
.dv { padding:10px 12px; font-size:13px; color:#303133; flex:1; }
.section-bar { font-size:13px; font-weight:700; color:#1a2c4e; padding:6px 0 6px 10px; margin-bottom:10px; border-left:4px solid #4A6FA5; }
</style>

View File

@@ -0,0 +1,80 @@
<template>
<div class="order-page">
<div class="page-header"><span class="page-title">历史订单甲方</span><el-tag type="success" size="small" effect="dark">STATUS: SIGNED</el-tag></div>
<div class="search-bar">
<el-input v-model="q.doNo" placeholder="搜索单号" clearable size="small" style="width:150px" @keyup.enter.native="search" />
<el-input v-model="q.clientName" placeholder="搜索甲方客户" clearable size="small" style="width:160px" @keyup.enter.native="search" />
<el-button type="primary" size="small" icon="el-icon-search" @click="search">搜索</el-button><el-button size="small" icon="el-icon-refresh" @click="resetSearch">重置</el-button>
<div class="sr"><el-button size="small" icon="el-icon-refresh" @click="getList">刷新</el-button></div>
</div>
<el-table v-loading="loading" :data="list" border stripe size="small" style="width:100%">
<el-table-column label="单号" prop="doNo" width="160" />
<el-table-column label="甲方客户" prop="clientName" min-width="150" show-overflow-tooltip />
<el-table-column label="金额" width="120" align="right"><template slot-scope="s"><span class="amount">¥{{ s.row.totalAmount }}</span></template></el-table-column>
<el-table-column label="交货期" prop="deliveryDate" width="95" align="center" />
<el-table-column label="签收日期" prop="actualCloseDate" width="95" align="center" />
<el-table-column label="配送差异" width="90" align="center"><template slot-scope="s"><span :class="diffClass(s)">{{ diffLabel(s.row) }}</span></template></el-table-column>
<el-table-column label="物料" prop="itemCount" width="55" align="center" />
<el-table-column label="状态" width="85" align="center"><el-tag type="success" size="small" effect="dark">已签收</el-tag></el-table-column>
<el-table-column label="操作" width="120" align="center"><template slot-scope="s"><el-button size="mini" type="text" @click="handleView(s.row)">详情</el-button><el-button size="mini" type="text" @click="handleRecall(s.row)">撤回</el-button></template></el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="q.pageNum" :limit.sync="q.pageSize" @pagination="getList" />
<el-dialog title="发货单详情" :visible.sync="detailOpen" width="780px" append-to-body>
<div>
<div class="detail-grid">
<div class="detail-item"><span class="dl">发货单号</span><span class="dv"><b>{{ detailData ? detailData.doNo : '' }}</b></span></div>
<div class="detail-item"><span class="dl">甲方客户</span><span class="dv">{{ (detailData && detailData.clientName) || '-' }}</span></div>
<div class="detail-item"><span class="dl">总金额</span><span class="dv" style="color:#409EFF">¥{{ detailData ? detailData.totalAmount : 0 }}</span></div>
<div class="detail-item"><span class="dl">状态</span><span class="dv"><el-tag type="success" size="small" effect="dark">已签收</el-tag></span></div>
<div class="detail-item"><span class="dl">交货期</span><span class="dv">{{ (detailData && detailData.deliveryDate) || '-' }}</span></div>
<div class="detail-item"><span class="dl">签收日期</span><span class="dv">{{ (detailData && detailData.actualCloseDate) || '-' }}</span></div>
</div>
<div v-if="detailData" class="section-bar">物料明细</div>
<el-table v-if="detailData" :data="detailData.items || []" border size="small">
<el-table-column label="物料名称" prop="materialName" min-width="150" />
<el-table-column label="规格" prop="spec" width="120" />
<el-table-column label="单位" prop="unit" width="60" />
<el-table-column label="数量" prop="quantity" width="80" align="right" />
<el-table-column label="单价" width="100" align="right"><template slot-scope="s">¥{{ s.row.unitPrice }}</template></el-table-column>
<el-table-column label="小计" width="100" align="right"><template slot-scope="s">¥{{ s.row.totalPrice }}</template></el-table-column>
</el-table>
</div>
<div slot="footer"><el-button @click="detailOpen = false">关闭</el-button></div>
</el-dialog>
</div>
</template>
<script>
import { listDelivery, getDelivery, recallDelivery } from "@/api/bid/delivery"
export default {
name: "ClientDeliverySigned",
data() { return { loading: false, list: [], total: 0, q: { pageNum: 1, pageSize: 20, type: "client", deliveryStatus: "history", doNo: "", clientName: "" }, detailOpen: false, detailData: null }},
created() { this.getList() },
methods: {
getList() { this.loading=true; listDelivery(this.q).then(r=>{this.list=(r.rows||[]).map(d=>({...d,deliveryDate:d.deliveryDate?d.deliveryDate.substring(0,10):'',actualCloseDate:d.actualCloseDate?d.actualCloseDate.substring(0,10):''}));this.total=r.total||0;this.loading=false}).catch(()=>{this.loading=false}) },
search() { this.q.pageNum=1; this.getList() }, resetSearch() { this.q.doNo=""; this.q.clientName=""; this.search() },
handleView(row) { getDelivery(row.doId).then(r=>{this.detailData=r.data;this.detailOpen=true}).catch(()=>{}) },
handleRecall(row) { this.$modal.confirm("确认撤回?").then(()=>recallDelivery(row.doId)).then(()=>{this.$modal.msgSuccess("已撤回");this.getList()}).catch(()=>{}) },
diffDays(r) { if(!r.deliveryDate||!r.actualCloseDate)return null; return Math.round((new Date(r.actualCloseDate)-new Date(r.deliveryDate))/86400000) },
diffLabel(r) { const d=this.diffDays(r); if(d===null)return '-'; if(d<0)return '提前'+Math.abs(d)+''; if(d===0)return '准时'; return '延期'+d+'' },
diffClass(s) { const d=this.diffDays(s.row); if(d===null)return ''; if(d<0)return 'diff-early'; if(d===0)return 'diff-ontime'; return 'diff-late' }
}
}
</script>
<style scoped>
.order-page { background:#f5f7fa; padding:12px; min-height:calc(100vh - 84px); }
.page-header { background:#fff; padding:12px 16px; border-radius:4px; margin-bottom:12px; display:flex; align-items:center; gap:12px; }
.page-title { font-size:16px; font-weight:700; color:#1a2c4e; }
.search-bar { background:#fff; padding:12px 16px; border-radius:4px; margin-bottom:12px; display:flex; align-items:center; gap:8px; }
.sr { margin-left:auto; }
.amount { color:#409EFF; font-weight:700; }
.detail-grid { display:grid; grid-template-columns:1fr 1fr; gap:0; border:1px solid #ebeef5; border-radius:4px; margin-bottom:16px; }
.detail-item { display:flex; border-bottom:1px solid #ebeef5; }
.detail-item:nth-last-child(-n+2) { border-bottom:none; }
.detail-item:nth-child(odd) { border-right:1px solid #ebeef5; }
.dl { width:90px; flex-shrink:0; background:#f5f7fa; padding:10px 12px; font-size:12px; color:#606266; font-weight:600; border-right:1px solid #ebeef5; }
.dv { padding:10px 12px; font-size:13px; color:#303133; flex:1; }
.section-bar { font-size:13px; font-weight:700; color:#1a2c4e; padding:6px 0 6px 10px; margin-bottom:10px; border-left:4px solid #4A6FA5; }
.diff-early { color:#67c23a; font-weight:600; }
.diff-ontime { color:#909399; font-weight:600; }
.diff-late { color:#f56c6c; font-weight:600; }
</style>

View File

@@ -0,0 +1,79 @@
<template>
<div class="order-page">
<div class="page-header"><span class="page-title">甲方在途</span><el-tag type="primary" size="small" effect="dark">STATUS: TRANSIT</el-tag></div>
<div class="search-bar">
<el-input v-model="q.doNo" placeholder="搜索单号" clearable size="small" style="width:150px" @keyup.enter.native="search" />
<el-input v-model="q.clientName" placeholder="搜索甲方客户" clearable size="small" style="width:160px" @keyup.enter.native="search" />
<el-button type="primary" size="small" icon="el-icon-search" @click="search">搜索</el-button><el-button size="small" icon="el-icon-refresh" @click="resetSearch">重置</el-button>
<div class="sr"><el-button size="small" icon="el-icon-refresh" @click="getList">刷新</el-button></div>
</div>
<el-table v-loading="loading" :data="list" border stripe size="small" style="width:100%">
<el-table-column label="单号" prop="doNo" width="165" />
<el-table-column label="甲方客户" prop="clientName" min-width="160" show-overflow-tooltip />
<el-table-column label="金额" width="120" align="right"><template slot-scope="s"><span class="amount">¥{{ s.row.totalAmount }}</span></template></el-table-column>
<el-table-column label="交货期" prop="deliveryDate" width="95" align="center" />
<el-table-column label="物料" prop="itemCount" width="55" align="center" />
<el-table-column label="状态" width="90" align="center"><el-tag type="primary" size="small" effect="dark">运输中</el-tag></el-table-column>
<el-table-column label="操作" width="180" align="center">
<template slot-scope="s">
<el-button size="mini" type="text" @click="handleView(s.row)">详情</el-button>
<el-button size="mini" type="text" style="color:#67C23A" @click="handleSign(s.row)">甲方签收</el-button>
<el-button size="mini" type="text" @click="handleRecall(s.row)">撤回</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="q.pageNum" :limit.sync="q.pageSize" @pagination="getList" />
<el-dialog title="发货单详情" :visible.sync="detailOpen" width="780px" append-to-body>
<div>
<div class="detail-grid">
<div class="detail-item"><span class="dl">发货单号</span><span class="dv"><b>{{ detailData ? detailData.doNo : '' }}</b></span></div>
<div class="detail-item"><span class="dl">甲方客户</span><span class="dv">{{ (detailData && detailData.clientName) || '-' }}</span></div>
<div class="detail-item"><span class="dl">总金额</span><span class="dv" style="color:#409EFF">¥{{ detailData ? detailData.totalAmount : 0 }}</span></div>
<div class="detail-item"><span class="dl">状态</span><span class="dv"><el-tag type="primary" size="small" effect="dark">运输中</el-tag></span></div>
<div class="detail-item"><span class="dl">交货期</span><span class="dv">{{ (detailData && detailData.deliveryDate) || '-' }}</span></div>
<div class="detail-item"><span class="dl">备注</span><span class="dv">{{ (detailData && detailData.remark) || '-' }}</span></div>
</div>
<div v-if="detailData" class="section-bar">物料明细</div>
<el-table v-if="detailData" :data="detailData.items || []" border size="small">
<el-table-column label="物料名称" prop="materialName" min-width="150" />
<el-table-column label="规格" prop="spec" width="120" />
<el-table-column label="单位" prop="unit" width="60" />
<el-table-column label="数量" prop="quantity" width="80" align="right" />
<el-table-column label="单价" width="100" align="right"><template slot-scope="s">¥{{ s.row.unitPrice }}</template></el-table-column>
<el-table-column label="小计" width="100" align="right"><template slot-scope="s">¥{{ s.row.totalPrice }}</template></el-table-column>
</el-table>
</div>
<div slot="footer"><el-button @click="detailOpen = false">关闭</el-button></div>
</el-dialog>
</div>
</template>
<script>
import { listDelivery, getDelivery, completeDelivery, recallDelivery } from "@/api/bid/delivery"
export default {
name: "ClientDeliveryTransit",
data() { return { loading: false, list: [], total: 0, q: { pageNum: 1, pageSize: 20, type: "client", deliveryStatus: "transit", doNo: "", clientName: "" }, detailOpen: false, detailData: null }},
created() { this.getList() },
methods: {
getList() { this.loading=true; listDelivery(this.q).then(r=>{this.list=(r.rows||[]).map(d=>({...d,deliveryDate:d.deliveryDate?d.deliveryDate.substring(0,10):''}));this.total=r.total||0;this.loading=false}).catch(()=>{this.loading=false}) },
search() { this.q.pageNum=1; this.getList() }, resetSearch() { this.q.doNo=""; this.q.clientName=""; this.search() },
handleView(row) { getDelivery(row.doId).then(r=>{this.detailData=r.data;this.detailOpen=true}).catch(()=>{}) },
handleSign(row) { this.$modal.confirm("确认甲方已签收?").then(()=>completeDelivery(row.doId)).then(()=>{this.$modal.msgSuccess("已签收");this.getList()}).catch(()=>{}) },
handleRecall(row) { this.$modal.confirm("确认撤回?").then(()=>recallDelivery(row.doId)).then(()=>{this.$modal.msgSuccess("已撤回");this.getList()}).catch(()=>{}) }
}
}
</script>
<style scoped>
.order-page { background:#f5f7fa; padding:12px; min-height:calc(100vh - 84px); }
.page-header { background:#fff; padding:12px 16px; border-radius:4px; margin-bottom:12px; display:flex; align-items:center; gap:12px; }
.page-title { font-size:16px; font-weight:700; color:#1a2c4e; }
.search-bar { background:#fff; padding:12px 16px; border-radius:4px; margin-bottom:12px; display:flex; align-items:center; gap:8px; }
.sr { margin-left:auto; }
.amount { color:#409EFF; font-weight:700; }
.detail-grid { display:grid; grid-template-columns:1fr 1fr; gap:0; border:1px solid #ebeef5; border-radius:4px; margin-bottom:16px; }
.detail-item { display:flex; border-bottom:1px solid #ebeef5; }
.detail-item:nth-last-child(-n+2) { border-bottom:none; }
.detail-item:nth-child(odd) { border-right:1px solid #ebeef5; }
.dl { width:90px; flex-shrink:0; background:#f5f7fa; padding:10px 12px; font-size:12px; color:#606266; font-weight:600; border-right:1px solid #ebeef5; }
.dv { padding:10px 12px; font-size:13px; color:#303133; flex:1; }
.section-bar { font-size:13px; font-weight:700; color:#1a2c4e; padding:6px 0 6px 10px; margin-bottom:10px; border-left:4px solid #4A6FA5; }
</style>

View File

@@ -88,6 +88,7 @@
<el-button size="mini" type="text" @click="handleUpdate(s.row)">编辑</el-button>
<el-button size="mini" type="text" @click="handleQuickCreate(s.row)">快速新建</el-button>
<el-button size="mini" type="text" style="color:#67C23A" @click="handleCreateRfq(s.row)">生成RFQ</el-button>
<el-button size="mini" type="text" style="color:#4A6FA5" @click="handleCreateDelivery(s.row)" v-if="s.row.status==='confirmed'">生成发货单</el-button>
<el-button size="mini" type="text" style="color:#f56c6c" @click="handleDelete(s.row)" v-if="s.row.status==='draft'">删除</el-button>
</template>
</el-table-column>
@@ -260,6 +261,7 @@ import { listClientQuote, getClientQuote, addClientQuote, updateClientQuote, del
getClientQuoteStatistics, quickCreateFromQuote } from "@/api/bid/clientquote";
import { listRfq, createRfqFromQuote } from "@/api/bid/rfq";
import { listMaterial } from "@/api/bid/material";
import { addDelivery } from "@/api/bid/delivery";
export default {
name: "ClientQuote",
@@ -310,6 +312,14 @@ export default {
handleCreateRfq(row) {
this.$modal.confirm("确认基于【" + row.quoteNo + "】生成RFQ").then(() => createRfqFromQuote(row.quoteId)).then(r => { this.detailOpen = false; this.$modal.msgSuccess("RFQ已创建"); this.$router.push({ path: '/bid/rfq/detail', query: { rfqId: r.data.rfqId, rfqNo: r.data.rfqNo, edit: '1' } }); }).catch(() => {});
},
handleCreateDelivery(row) {
this.$modal.confirm("确认基于【" + row.quoteNo + "】生成甲方发货单?").then(() => getClientQuote(row.quoteId)).then(r => {
const q = r.data; if (!q || !q.items || !q.items.length) throw new Error("报价单无明细,无法生成发货单");
return addDelivery({ type: "client", clientQuoteId: q.quoteId, totalAmount: q.totalAmount, deliveryStatus: "pending",
items: q.items.map(it => ({ materialId: it.materialId, materialName: it.materialName, spec: it.spec, unit: it.unit, quantity: it.quantity, unitPrice: it.unitPrice, totalPrice: (it.quantity||0)*(it.unitPrice||0) }))
});
}).then(() => { this.$modal.msgSuccess("甲方发货单已生成"); this.getList() }).catch(e => { if (e.message) this.$modal.msgError(e.message) });
},
handleDelete(row) { this.$modal.confirm("确认删除【" + row.quoteNo + "】?").then(() => delClientQuote(row.quoteId)).then(() => { this.$modal.msgSuccess("删除成功"); this.getList(); this.getStats(); }); },
loadRfqForDetail(quoteId) { this.detailRfqLoading = true; listRfq({ clientQuoteId: quoteId, pageSize: 50 }).then(r => { this.detailRfqList = r.rows || []; this.detailRfqLoading = false; }).catch(() => { this.detailRfqLoading = false; }); },
viewRfqDetail(rfq) { this.detailOpen = false; this.$router.push({ path: '/bid/rfq/detail', query: { rfqId: rfq.rfqId } }); },

View File

@@ -77,21 +77,26 @@
</el-select>
<el-table v-loading="recordLoading" :data="recordList" border stripe size="small" style="width:100%;margin-top:12px">
<el-table-column label="发货单号" prop="doNo" width="160" />
<el-table-column label="供应商" prop="supplierName" width="150" show-overflow-tooltip />
<el-table-column label="甲方客户" prop="clientName" width="150" show-overflow-tooltip />
<el-table-column label="数量" prop="quantity" width="80" align="right" />
<el-table-column label="单价" width="100" align="right">
<template slot-scope="s">¥{{ s.row.unitPrice }}</template>
<el-table-column label="发货单号" width="150">
<template slot-scope="s">{{ s.row.do_no || '-' }}</template>
</el-table-column>
<el-table-column label="小计" width="100" align="right">
<template slot-scope="s">¥{{ s.row.totalPrice }}</template>
<el-table-column label="类型" width="60" align="center">
<template slot-scope="s"><el-tag :type="s.row.type==='client'?'primary':'warning'" size="mini" effect="plain">{{ s.row.type==='client'?'甲方':'供应商' }}</el-tag></template>
</el-table-column>
<el-table-column label="交货期" prop="deliveryDate" width="95" align="center" />
<el-table-column label="结单日期" prop="actualCloseDate" width="95" align="center" />
<el-table-column label="状态" width="85" align="center">
<el-table-column label="供应商" width="140" show-overflow-tooltip>
<template slot-scope="s">{{ s.row.type==='client' ? '——' : (s.row.supplier_name || '-') }}</template>
</el-table-column>
<el-table-column label="甲方客户" width="140" show-overflow-tooltip>
<template slot-scope="s">{{ s.row.client_name || '-' }}</template>
</el-table-column>
<el-table-column label="数量" prop="quantity" width="75" align="right" />
<el-table-column label="单价" width="95" align="right"><template slot-scope="s">¥{{ s.row.unit_price || 0 }}</template></el-table-column>
<el-table-column label="小计" width="95" align="right"><template slot-scope="s">¥{{ s.row.total_price || 0 }}</template></el-table-column>
<el-table-column label="交货期" prop="delivery_date" width="90" align="center" />
<el-table-column label="结单" prop="actual_close_date" width="90" align="center" />
<el-table-column label="状态" width="80" align="center">
<template slot-scope="s">
<el-tag :type="recordStatusType(s.row.deliveryStatus)" size="small" effect="dark">{{ recordStatusLabel(s.row.deliveryStatus) }}</el-tag>
<el-tag :type="recordStatusType(s.row.delivery_status)" size="small" effect="dark">{{ recordStatusLabel(s.row.delivery_status) }}</el-tag>
</template>
</el-table-column>
</el-table>
@@ -339,16 +344,19 @@ export default {
.catch(() => { this.recordLoading = false })
},
recordStatusType(s) { return { pending: "warning", transit: "primary", history: "success" }[s] || "" },
recordStatusLabel(s) { return { pending: "待发", transit: "在途", history: "已收货" }[s] || s || "-" }
recordStatusLabel(s) { return { pending: "待发", transit: "在途", history: "已收货" }[s] || s || "-" },
// 兼容 snake_case 和 camelCase
fmtRow(r) { return r }
}
};
</script>
<style scoped>
/* ═══ 京东主题 — 页面级变量覆盖 ═══ */
.app-container {
background: #fff;
background: var(--bg-page);
padding: 16px 20px;
border-radius: 4px;
border-radius: var(--radius-base);
}
/* 紧凑表格行 */
@@ -356,11 +364,11 @@ export default {
.el-table th { padding: 6px 4px !important; }
/* 圆角按钮 */
.el-button--mini { border-radius: 4px !important; }
.el-button--mini { border-radius: var(--radius-base) !important; }
/* 搜索按钮浅蓝 */
.search-btn { background: #409EFF; color: #fff; border: none; border-radius: 4px; }
.search-btn:hover { background: #66b1ff; }
/* 搜索按钮(适配京东红) */
.search-btn { background: var(--brand-primary); color: #fff; border: none; border-radius: var(--radius-base); }
.search-btn:hover { background: var(--brand-primary-hover); }
/* 搜索表单样式 */
.el-form--inline .el-form-item {

View File

@@ -80,19 +80,19 @@
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
<!-- 详情弹窗 -->
<el-dialog title="发货单详情" :visible.sync="detailOpen" width="780px" append-to-body>
<div v-if="detailData">
<el-dialog title="发货单详情" :visible.sync="detailOpen" width="780px" top="5vh" append-to-body>
<div>
<div class="detail-grid">
<div class="detail-item"><span class="dl">发货单号</span><span class="dv"><b>{{ detailData.doNo }}</b></span></div>
<div class="detail-item"><span class="dl">供应商</span><span class="dv">{{ detailData.supplierName || '-' }}</span></div>
<div class="detail-item"><span class="dl">总金额</span><span class="dv" style="color:#409EFF;font-weight:700">¥{{ detailData.totalAmount }}</span></div>
<div class="detail-item"><span class="dl">发货单号</span><span class="dv"><b>{{ detailData ? detailData.doNo : '' }}</b></span></div>
<div class="detail-item"><span class="dl">供应商</span><span class="dv">{{ (detailData && detailData.supplierName) || '-' }}</span></div>
<div class="detail-item"><span class="dl">总金额</span><span class="dv" style="color:#409EFF">¥{{ detailData ? detailData.totalAmount : 0 }}</span></div>
<div class="detail-item"><span class="dl">状态</span><span class="dv"><el-tag type="primary" size="small" effect="dark">TRANSIT</el-tag></span></div>
<div class="detail-item"><span class="dl">交货期</span><span class="dv">{{ detailData.deliveryDate || '-' }}</span></div>
<div class="detail-item"><span class="dl">延期日期</span><span class="dv">{{ detailData.delayDate || '-' }}</span></div>
<div class="detail-item"><span class="dl">交货期</span><span class="dv">{{ (detailData && detailData.deliveryDate) || '-' }}</span></div>
<div class="detail-item"><span class="dl">延期日期</span><span class="dv">{{ (detailData && detailData.delayDate) || '-' }}</span></div>
</div>
<div v-if="detailData.remark" class="detail-remark">备注{{ detailData.remark }}</div>
<div v-if="detailData && detailData.remark" class="detail-remark">备注{{ detailData.remark }}</div>
<div class="section-bar">物料明细</div>
<el-table :data="detailData.items || []" border size="small">
<el-table v-if="detailData" :data="detailData.items || []" border size="small">
<el-table-column label="物料名称" prop="materialName" min-width="150" />
<el-table-column label="规格" prop="spec" width="120" show-overflow-tooltip />
<el-table-column label="单位" prop="unit" width="60" />
@@ -149,7 +149,14 @@ export default {
handleSearch() { this.queryParams.pageNum = 1; this.getList() },
resetSearch() { this.queryParams.doNo = ""; this.queryParams.supplierName = ""; this.handleSearch() },
handleView(row) { getDelivery(row.doId).then(r => { this.detailData = r.data; this.detailOpen = true }).catch(() => {}) },
handleView(row) {
getDelivery(row.doId).then(r => {
this.detailData = JSON.parse(JSON.stringify(r.data))
this.detailOpen = true
}).catch(e => {
this.$modal.msgError("获取详情失败: " + (e.message || '未知错误'))
})
},
handleComplete(row) {
this.$modal.confirm("确认该订单已收货完成?").then(() => completeDelivery(row.doId))