feat(bid): add in-transit order management feature

实现了在途订单的统计查询、列表展示以及相关的订单操作功能,包括收货完成、延期、撤回等,同时新增了对应的后台接口、Mapper以及路由页面。
This commit is contained in:
2026-06-12 10:12:19 +08:00
parent 91f29d36ee
commit b975fd0bc6
7 changed files with 301 additions and 1 deletions

View File

@@ -99,4 +99,14 @@ public class BizDeliveryOrderController extends BaseController {
public AjaxResult materialRecords(@PathVariable Long materialId) {
return success(service.selectMaterialRecords(materialId));
}
// ════════════════════════════════════════
// 在途统计
// ════════════════════════════════════════════
@PreAuthorize("@ss.hasPermi('bid:order:transit')")
@GetMapping("/transit/stats")
public AjaxResult transitStats() {
return success(service.selectTransitStats());
}
}

View File

@@ -23,4 +23,7 @@ public interface BizDeliveryOrderMapper {
// 物料发货记录
List<Map<String, Object>> selectMaterialRecords(@Param("materialId") Long materialId);
// 在途统计
Map<String, Object> selectTransitStats();
}

View File

@@ -20,4 +20,7 @@ public interface IBizDeliveryOrderService {
// 物料发货记录
List<Map<String, Object>> selectMaterialRecords(Long materialId);
// 在途统计
Map<String, Object> selectTransitStats();
}

View File

@@ -140,4 +140,9 @@ public class BizDeliveryOrderServiceImpl implements IBizDeliveryOrderService {
public List<Map<String, Object>> selectMaterialRecords(Long materialId) {
return mapper.selectMaterialRecords(materialId);
}
@Override
public Map<String, Object> selectTransitStats() {
return mapper.selectTransitStats();
}
}

View File

@@ -69,6 +69,15 @@
WHERE do_id=#{doId}
</update>
<select id="selectTransitStats" resultType="java.util.Map">
SELECT
COUNT(*) AS totalTransit,
SUM(CASE WHEN DATEDIFF(delivery_date, CURDATE()) BETWEEN 0 AND 3 THEN 1 ELSE 0 END) AS expiringSoon,
SUM(CASE WHEN DATEDIFF(delivery_date, CURDATE()) &lt; 0 THEN 1 ELSE 0 END) AS overdue
FROM biz_delivery_order
WHERE delivery_status = 'transit'
</select>
<delete id="deleteBizDeliveryOrderById">DELETE FROM biz_delivery_order WHERE do_id=#{id}</delete>
<delete id="deleteBizDeliveryOrderByIds">

View File

@@ -189,7 +189,7 @@ export const dynamicRoutes = [
}]
},
// ── 订单履约:待发订单 ──
// ── 订单履约 ──
{
path: '/bid/order/pending',
component: Layout,
@@ -201,6 +201,17 @@ export const dynamicRoutes = [
meta: { title: '待发订单', activeMenu: '/bid/order' }
}]
},
{
path: '/bid/order/transit',
component: Layout,
permissions: ['bid:order:transit'],
children: [{
path: '',
component: () => import('@/views/bid/order/transit'),
name: 'OrderTransit',
meta: { title: '在途订单', activeMenu: '/bid/order' }
}]
},
{
path: '/bid/comparison/detail',

View File

@@ -0,0 +1,259 @@
<template>
<div class="order-page">
<!-- 标题栏 -->
<div class="page-header">
<span class="page-title">在途订单</span>
<el-tag type="primary" size="small" effect="dark" class="status-tag">STATUS: TRANSIT</el-tag>
</div>
<!-- 统计卡片 -->
<el-row :gutter="12" class="stat-row">
<el-col :span="6">
<div class="stat-card" style="border-top-color:#4A6FA5">
<div class="stat-body"><div class="stat-num">{{ stats.totalTransit != null ? stats.totalTransit : '-' }}</div><div class="stat-lbl">在途总数</div></div>
<i class="el-icon-ship stat-icon" style="color:#4A6FA5"></i>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card" style="border-top-color:#67c23a">
<div class="stat-body"><div class="stat-num">{{ stats.todayShipped != null ? stats.todayShipped : '-' }}</div><div class="stat-lbl">今日发货</div></div>
<i class="el-icon-upload2 stat-icon" style="color:#67c23a"></i>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card" style="border-top-color:#e6a23c">
<div class="stat-body"><div class="stat-num">{{ stats.expiringSoon != null ? stats.expiringSoon : '-' }}</div><div class="stat-lbl">即将到期</div></div>
<i class="el-icon-time stat-icon" style="color:#e6a23c"></i>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card" style="border-top-color:#f56c6c">
<div class="stat-body"><div class="stat-num">{{ stats.overdue != null ? stats.overdue : '-' }}</div><div class="stat-lbl">已逾期</div></div>
<i class="el-icon-warning-outline stat-icon" style="color:#f56c6c"></i>
</div>
</el-col>
</el-row>
<!-- 搜索栏 -->
<div class="search-bar">
<el-input v-model="queryParams.doNo" placeholder="搜索发货单号" clearable size="small" style="width:150px" @keyup.enter.native="handleSearch" />
<el-input v-model="queryParams.supplierName" placeholder="搜索供应商名称" clearable size="small" style="width:160px" @keyup.enter.native="handleSearch" />
<el-button type="primary" size="small" icon="el-icon-search" @click="handleSearch">搜索</el-button>
<el-button size="small" icon="el-icon-refresh" @click="resetSearch">重置</el-button>
<div class="search-right">
<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" class="order-table">
<el-table-column label="发货单号" prop="doNo" width="165" />
<el-table-column label="供应商" prop="supplierName" min-width="150" show-overflow-tooltip />
<el-table-column label="金额" width="130" align="right">
<template slot-scope="s"><span class="amount">¥{{ s.row.totalAmount }}</span></template>
</el-table-column>
<el-table-column label="交货期" width="100" align="center">
<template slot-scope="s">
<span :class="getUrgentClass(s.row)">{{ s.row.deliveryDate }}</span>
</template>
</el-table-column>
<el-table-column label="延期至" prop="delayDate" width="100" align="center">
<template slot-scope="s">{{ s.row.delayDate || '-' }}</template>
</el-table-column>
<el-table-column label="物料" prop="itemCount" width="60" align="center" />
<el-table-column label="逾期提示" width="110" align="center">
<template slot-scope="s"><span v-html="getUrgentBadge(s.row)" /></template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template slot-scope="s">
<el-tag :type="transitTagType(s.row)" size="small" effect="dark">{{ transitStatusLabel(s.row) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="230" align="center" fixed="right">
<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="handleComplete(s.row)">收货完成</el-button>
<el-button size="mini" type="text" style="color:#e6a23c" @click="handleDelay(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="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
<!-- 详情弹窗 -->
<el-dialog title="发货单详情" :visible.sync="detailOpen" width="780px" append-to-body>
<div v-if="detailData">
<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"><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>
<div v-if="detailData.remark" class="detail-remark">备注{{ detailData.remark }}</div>
<div class="section-bar">物料明细</div>
<el-table :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" />
<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>
<!-- 延期弹窗 -->
<el-dialog title="申请延期" :visible.sync="delayOpen" width="450px" append-to-body>
<el-form :model="delayForm" label-width="90px" size="small">
<el-form-item label="发货单号">{{ delayForm.doNo }}</el-form-item>
<el-form-item label="原交货期">{{ delayForm.deliveryDate }}</el-form-item>
<el-form-item label="延期至"><el-date-picker v-model="delayForm.newDelayDate" type="date" value-format="yyyy-MM-dd" style="width:100%" placeholder="选择新日期" /></el-form-item>
<el-form-item label="延期原因"><el-input v-model="delayForm.reason" type="textarea" :rows="3" placeholder="请输入延期原因" /></el-form-item>
</el-form>
<div slot="footer">
<el-button @click="delayOpen = false">取消</el-button>
<el-button type="primary" @click="submitDelay">确认延期</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listDelivery, getDelivery, completeDelivery, recallDelivery, updateDelivery } from "@/api/bid/delivery"
import request from '@/utils/request'
export default {
name: "OrderTransit",
data() {
return {
loading: false, list: [], total: 0, stats: {},
queryParams: { pageNum: 1, pageSize: 20, deliveryStatus: "transit", doNo: "", supplierName: "" },
detailOpen: false, detailData: null,
delayOpen: false, delayForm: {}
}
},
created() { this.getList(); this.getStats() },
methods: {
getList() {
this.loading = true
listDelivery(this.queryParams).then(r => {
this.list = (r.rows || []).map(d => ({ ...d, deliveryDate: d.deliveryDate ? d.deliveryDate.substring(0, 10) : '', delayDate: d.delayDate ? d.delayDate.substring(0, 10) : '' }))
this.total = r.total || 0; this.loading = false
}).catch(() => { this.loading = false })
},
getStats() {
request({ url: '/bid/delivery/transit/stats', method: 'get' }).then(r => { this.stats = r.data || {} }).catch(() => {})
},
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(() => {}) },
handleComplete(row) {
this.$modal.confirm("确认该订单已收货完成?").then(() => completeDelivery(row.doId))
.then(() => { this.$modal.msgSuccess("已确认收货"); this.getList(); this.getStats() }).catch(() => {})
},
handleDelay(row) {
this.delayForm = { doId: row.doId, doNo: row.doNo, deliveryDate: row.deliveryDate, newDelayDate: "", reason: "" }
this.delayOpen = true
},
submitDelay() {
if (!this.delayForm.newDelayDate) { this.$modal.msgError("请选择延期日期"); return }
updateDelivery({ doId: this.delayForm.doId, delayDate: this.delayForm.newDelayDate }).then(() => {
this.$modal.msgSuccess("延期成功"); this.delayOpen = false; this.getList()
}).catch(() => {})
},
handleRecall(row) {
this.$modal.confirm("确认撤回该订单?将回到待发状态。").then(() => recallDelivery(row.doId))
.then(() => { this.$modal.msgSuccess("已撤回"); this.getList(); this.getStats() }).catch(() => {})
},
// 状态判断
transitTagType(row) {
if (!row.deliveryDate) return "primary"
const diff = Math.round((new Date(row.deliveryDate) - new Date()) / 86400000)
if (diff < 0) return "danger"
if (diff <= 3) return "warning"
return "primary"
},
transitStatusLabel(row) {
if (!row.deliveryDate) return "运输中"
const diff = Math.round((new Date(row.deliveryDate) - new Date()) / 86400000)
if (diff < 0) return "已逾期"
if (diff <= 3) return "即将到期"
return "正常在途"
},
getUrgentClass(row) {
if (!row.deliveryDate) return ""
const diff = Math.round((new Date(row.deliveryDate) - new Date()) / 86400000)
if (diff < 0) return "urgent-overdue"
if (diff <= 3) return "urgent-soon"
return ""
},
getUrgentBadge(row) {
if (!row.deliveryDate) return ""
const diff = Math.round((new Date(row.deliveryDate) - new Date()) / 86400000)
if (diff < 0) return '<span class="urgent-overdue">⚠ 逾期' + Math.abs(diff) + '天</span>'
if (diff === 0) return '<span class="urgent-overdue">⚠ 今日到期</span>'
if (diff <= 3) return '<span class="urgent-soon">⚡ 剩' + diff + '天</span>'
return ""
}
}
}
</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;
box-shadow: 0 1px 4px rgba(0,0,0,0.06); display: flex; align-items: center; gap: 12px;
}
.page-title { font-size: 16px; font-weight: 700; color: #1a2c4e; }
.status-tag { margin-left: auto; }
/* ═══ 统计卡片 ═══ */
.stat-row { margin-bottom: 12px !important; }
.stat-card {
background: #fff; border-radius: 4px; border-top: 3px solid #4A6FA5;
padding: 16px 20px; display: flex; align-items: center; justify-content: space-between;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
.stat-num { font-size: 26px; font-weight: 700; color: #1a2c4e; line-height: 1.2; }
.stat-lbl { font-size: 12px; color: #8c97a8; margin-top: 4px; }
.stat-icon { font-size: 28px; opacity: 0.5; }
/* ═══ 搜索栏 ═══ */
.search-bar {
background: #fff; padding: 12px 16px; border-radius: 4px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06); margin-bottom: 12px;
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.search-right { margin-left: auto; }
/* ═══ 表格 ═══ */
.order-table { box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.amount { color: #409EFF; font-weight: 700; }
.urgent-overdue { color: #f56c6c; font-weight: 700; font-size: 12px; }
.urgent-soon { color: #e6a23c; font-weight: 700; font-size: 12px; }
/* ═══ 详情 ═══ */
.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; }
.detail-remark { padding: 8px 12px; background: #fdf6ec; border: 1px solid #faecd8; border-radius: 4px; font-size: 12px; color: #e6a23c; margin-bottom: 16px; }
.section-bar { font-size: 13px; font-weight: 700; color: #1a2c4e; padding: 6px 0 6px 10px; margin-bottom: 10px; border-left: 4px solid #4A6FA5; }
</style>