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

@@ -26,6 +26,9 @@ public class BizDeliveryOrder extends BaseEntity {
private List<BizDeliveryOrderItem> items;
private String supplierName;
private Integer itemCount;
private String type;
private Long clientQuoteId;
private String clientName;
public Long getDoId() { return doId; }
public void setDoId(Long doId) { this.doId = doId; }
@@ -59,4 +62,10 @@ public class BizDeliveryOrder extends BaseEntity {
public void setSupplierName(String supplierName) { this.supplierName = supplierName; }
public Integer getItemCount() { return itemCount; }
public void setItemCount(Integer itemCount) { this.itemCount = itemCount; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public Long getClientQuoteId() { return clientQuoteId; }
public void setClientQuoteId(Long clientQuoteId) { this.clientQuoteId = clientQuoteId; }
public String getClientName() { return clientName; }
public void setClientName(String clientName) { this.clientName = clientName; }
}

View File

@@ -37,7 +37,8 @@ public class BizDeliveryOrderServiceImpl implements IBizDeliveryOrderService {
@Transactional
public int insertBizDeliveryOrder(BizDeliveryOrder record) {
if (record.getDoNo() == null || record.getDoNo().isEmpty()) {
record.setDoNo("DO" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()));
String prefix = "client".equals(record.getType()) ? "CD" : "DO";
record.setDoNo(prefix + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()));
}
if (record.getDeliveryStatus() == null || record.getDeliveryStatus().isEmpty()) {
record.setDeliveryStatus("pending");

View File

@@ -23,19 +23,27 @@
<result property="updateTime" column="update_time"/>
<result property="supplierName" column="supplier_name"/>
<result property="itemCount" column="item_count"/>
<result property="type" column="type"/>
<result property="clientQuoteId" column="client_quote_id"/>
<result property="clientName" column="client_name"/>
</resultMap>
<select id="selectBizDeliveryOrderList" resultMap="BaseRM">
SELECT d.*, s.supplier_name,
COALESCE(cl.client_name, cq.client_name) AS client_name,
(SELECT COUNT(*) FROM biz_delivery_order_item WHERE do_id = d.do_id) AS item_count
FROM biz_delivery_order d
LEFT JOIN biz_supplier s ON d.supplier_id=s.supplier_id
LEFT JOIN biz_client_quote cq ON d.client_quote_id = cq.quote_id
LEFT JOIN biz_client cl ON cq.client_id = cl.client_id
<where>
<if test="tenantId != null"> AND d.tenant_id=#{tenantId}</if>
<if test="type != null and type != ''"> AND d.type=#{type}</if>
<if test="doNo != null and doNo != ''"> AND d.do_no LIKE CONCAT('%',#{doNo},'%')</if>
<if test="supplierId != null"> AND d.supplier_id=#{supplierId}</if>
<if test="deliveryStatus != null and deliveryStatus != ''"> AND d.delivery_status=#{deliveryStatus}</if>
<if test="supplierName != null and supplierName != ''"> AND s.supplier_name LIKE CONCAT('%',#{supplierName},'%')</if>
<if test="clientName != null and clientName != ''"> AND cl.client_name LIKE CONCAT('%',#{clientName},'%')</if>
</where>
ORDER BY d.create_time DESC
</select>
@@ -48,8 +56,8 @@
</select>
<insert id="insertBizDeliveryOrder" useGeneratedKeys="true" keyProperty="doId">
INSERT INTO biz_delivery_order(tenant_id,do_no,rfq_id,quotation_id,supplier_id,total_amount,currency,delivery_date,delay_date,actual_close_date,close_date_set_by,delivery_status,remark,create_by,create_time)
VALUES(#{tenantId},#{doNo},#{rfqId},#{quotationId},#{supplierId},#{totalAmount},#{currency},#{deliveryDate},#{delayDate},#{actualCloseDate},#{closeDateSetBy},#{deliveryStatus},#{remark},#{createBy},NOW())
INSERT INTO biz_delivery_order(tenant_id,do_no,type,rfq_id,quotation_id,client_quote_id,supplier_id,total_amount,currency,delivery_date,delay_date,actual_close_date,close_date_set_by,delivery_status,remark,create_by,create_time)
VALUES(#{tenantId},#{doNo},#{type},#{rfqId},#{quotationId},#{clientQuoteId},#{supplierId},#{totalAmount},#{currency},#{deliveryDate},#{delayDate},#{actualCloseDate},#{closeDateSetBy},#{deliveryStatus},#{remark},#{createBy},NOW())
</insert>
<update id="updateBizDeliveryOrder">
@@ -119,8 +127,9 @@
<!-- 物料发货记录多表JOIN追溯 -->
<select id="selectMaterialRecords" resultType="java.util.Map">
SELECT d.do_no,
d.type,
s.supplier_name,
cl.client_name,
COALESCE(cl.client_name, cq.client_name, cq2.client_name) AS client_name,
di.quantity,
di.unit_price,
di.total_price,
@@ -130,9 +139,12 @@
FROM biz_delivery_order_item di
JOIN biz_delivery_order d ON di.do_id = d.do_id
LEFT JOIN biz_supplier s ON d.supplier_id = s.supplier_id
-- 供应商履约链路: delivery → rfq → client_quote
LEFT JOIN biz_rfq r ON d.rfq_id = r.rfq_id
LEFT JOIN biz_client_quote cq ON r.client_quote_id = cq.quote_id
LEFT JOIN biz_client cl ON cq.client_id = cl.client_id
-- 甲方履约链路: delivery → client_quote直连
LEFT JOIN biz_client_quote cq2 ON d.type = 'client' AND d.client_quote_id = cq2.quote_id
LEFT JOIN biz_client cl ON COALESCE(cq.client_id, cq2.client_id) = cl.client_id
WHERE di.material_id = #{materialId}
ORDER BY d.create_time DESC
</select>

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))

View File

@@ -0,0 +1,5 @@
SET NAMES utf8mb4;
UPDATE sys_menu SET menu_name = '待发订单' WHERE menu_id = 2041;
UPDATE sys_menu SET menu_name = '在途订单' WHERE menu_id = 2042;
UPDATE sys_menu SET menu_name = '历史订单' WHERE menu_id = 2043;
SELECT menu_id, menu_name, parent_id FROM sys_menu WHERE menu_id IN (2040, 2041, 2042, 2043) ORDER BY menu_id;

30
sql/fix_menu_rename.sql Normal file
View File

@@ -0,0 +1,30 @@
SET NAMES utf8mb4;
-- 1. 订单履约 → 供应商履约
UPDATE sys_menu SET menu_name = '供应商履约' WHERE menu_id = 2023;
-- 2. 新建甲方履约根菜单 (2040)
INSERT IGNORE INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2040, '甲方履约', 0, 23, 'client-delivery', NULL, 1, 0, 'M', '0', '0', 'bid:clientdelivery:list', 's-order', 'admin', NOW());
-- 3. 子菜单
INSERT IGNORE INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2041, '甲方待发', 2040, 1, 'pending', 'bid/clientDelivery/pending', 1, 0, 'C', '0', '0', 'bid:clientdelivery:pending', 'time', 'admin', NOW());
INSERT IGNORE INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2042, '甲方在途', 2040, 2, 'transit', 'bid/clientDelivery/transit', 1, 0, 'C', '0', '0', 'bid:clientdelivery:transit', 'truck', 'admin', NOW());
INSERT IGNORE INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2043, '甲方签收', 2040, 3, 'signed', 'bid/clientDelivery/signed', 1, 0, 'C', '0', '0', 'bid:clientdelivery:signed', 'finished', 'admin', NOW());
-- 4. 按钮权限
INSERT IGNORE INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2050, '发货确认', 2041, 1, '#', NULL, 1, 0, 'F', '0', '0', 'bid:clientdelivery:ship', '#', 'admin', NOW());
INSERT IGNORE INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2051, '签收确认', 2043, 1, '#', NULL, 1, 0, 'F', '0', '0', 'bid:clientdelivery:sign', '#', 'admin', NOW());
-- 5. admin 角色授权
INSERT IGNORE INTO sys_role_menu (role_id, menu_id)
SELECT 1, menu_id FROM sys_menu WHERE menu_id IN (2040, 2041, 2042, 2043, 2050, 2051);
SELECT menu_id, menu_name, parent_id FROM sys_menu WHERE menu_id IN (2023, 2040, 2041, 2042, 2043) ORDER BY menu_id;