feat(bid): add historical delivery order statistics and page
实现了历史发货订单的统计查询功能,新增历史订单列表页面与路由权限配置,支持订单搜索、详情查看、撤回和再次下单操作
This commit is contained in:
@@ -109,4 +109,10 @@ public class BizDeliveryOrderController extends BaseController {
|
|||||||
public AjaxResult transitStats() {
|
public AjaxResult transitStats() {
|
||||||
return success(service.selectTransitStats());
|
return success(service.selectTransitStats());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('bid:order:history')")
|
||||||
|
@GetMapping("/history/stats")
|
||||||
|
public AjaxResult historyStats() {
|
||||||
|
return success(service.selectHistoryStats());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,4 +26,6 @@ public interface BizDeliveryOrderMapper {
|
|||||||
|
|
||||||
// 在途统计
|
// 在途统计
|
||||||
Map<String, Object> selectTransitStats();
|
Map<String, Object> selectTransitStats();
|
||||||
|
// 历史统计
|
||||||
|
Map<String, Object> selectHistoryStats();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,4 +23,6 @@ public interface IBizDeliveryOrderService {
|
|||||||
|
|
||||||
// 在途统计
|
// 在途统计
|
||||||
Map<String, Object> selectTransitStats();
|
Map<String, Object> selectTransitStats();
|
||||||
|
// 历史统计
|
||||||
|
Map<String, Object> selectHistoryStats();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,4 +145,9 @@ public class BizDeliveryOrderServiceImpl implements IBizDeliveryOrderService {
|
|||||||
public Map<String, Object> selectTransitStats() {
|
public Map<String, Object> selectTransitStats() {
|
||||||
return mapper.selectTransitStats();
|
return mapper.selectTransitStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> selectHistoryStats() {
|
||||||
|
return mapper.selectHistoryStats();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,16 @@
|
|||||||
WHERE delivery_status = 'transit'
|
WHERE delivery_status = 'transit'
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectHistoryStats" resultType="java.util.Map">
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS totalHistory,
|
||||||
|
SUM(CASE WHEN DATE_FORMAT(actual_close_date, '%Y%m') = DATE_FORMAT(CURDATE(), '%Y%m') THEN 1 ELSE 0 END) AS monthCompleted,
|
||||||
|
COALESCE(SUM(total_amount), 0) AS totalAmount,
|
||||||
|
ROUND(AVG(DATEDIFF(actual_close_date, delivery_date)), 1) AS avgDeliveryDays
|
||||||
|
FROM biz_delivery_order
|
||||||
|
WHERE delivery_status = 'history'
|
||||||
|
</select>
|
||||||
|
|
||||||
<delete id="deleteBizDeliveryOrderById">DELETE FROM biz_delivery_order WHERE do_id=#{id}</delete>
|
<delete id="deleteBizDeliveryOrderById">DELETE FROM biz_delivery_order WHERE do_id=#{id}</delete>
|
||||||
|
|
||||||
<delete id="deleteBizDeliveryOrderByIds">
|
<delete id="deleteBizDeliveryOrderByIds">
|
||||||
|
|||||||
@@ -212,6 +212,17 @@ export const dynamicRoutes = [
|
|||||||
meta: { title: '在途订单', activeMenu: '/bid/order' }
|
meta: { title: '在途订单', activeMenu: '/bid/order' }
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/bid/order/history',
|
||||||
|
component: Layout,
|
||||||
|
permissions: ['bid:order:history'],
|
||||||
|
children: [{
|
||||||
|
path: '',
|
||||||
|
component: () => import('@/views/bid/order/history'),
|
||||||
|
name: 'OrderHistory',
|
||||||
|
meta: { title: '历史订单', activeMenu: '/bid/order' }
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/bid/comparison/detail',
|
path: '/bid/comparison/detail',
|
||||||
|
|||||||
282
ruoyi-ui/src/views/bid/order/history.vue
Normal file
282
ruoyi-ui/src/views/bid/order/history.vue
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
<template>
|
||||||
|
<div class="order-page">
|
||||||
|
<!-- ═══ 标题栏 ═══ -->
|
||||||
|
<div class="page-header">
|
||||||
|
<span class="page-title">历史订单</span>
|
||||||
|
<div class="header-right">
|
||||||
|
<el-tag type="success" size="small" effect="dark">STATUS: HISTORY</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ 统计卡片 ═══ -->
|
||||||
|
<el-row :gutter="12" class="stat-row">
|
||||||
|
<el-col :span="6" v-for="card in statCards" :key="card.key">
|
||||||
|
<div class="stat-card" :style="{ borderTopColor: card.color }">
|
||||||
|
<div class="stat-body"><div class="stat-num">{{ stats[card.key] != null ? stats[card.key] : '-' }}</div><div class="stat-lbl">{{ card.label }}</div></div>
|
||||||
|
<i :class="card.icon" class="stat-icon" :style="{ color: card.color }"></i>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- ═══ 搜索栏 ═══ -->
|
||||||
|
<div class="search-bar">
|
||||||
|
<el-input v-model="q.doNo" placeholder="搜索发货单号" clearable size="small" style="width:150px" @keyup.enter.native="handleSearch" />
|
||||||
|
<el-input v-model="q.supplierName" placeholder="搜索供应商名称" clearable size="small" style="width:160px" @keyup.enter.native="handleSearch" />
|
||||||
|
<el-date-picker v-model="closeDateRange" type="daterange" range-separator="至" start-placeholder="收货开始" end-placeholder="收货结束"
|
||||||
|
value-format="yyyy-MM-dd" size="small" style="width:210px" clearable />
|
||||||
|
<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="160" />
|
||||||
|
<el-table-column label="供应商" prop="supplierName" min-width="140" 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="100" align="center" />
|
||||||
|
<el-table-column label="收货日期" prop="actualCloseDate" width="100" align="center" />
|
||||||
|
<el-table-column label="交期差异" width="100" align="center">
|
||||||
|
<template slot-scope="s">
|
||||||
|
<span :class="diffClass(s.row)">{{ diffLabel(s.row) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="物料" prop="itemCount" width="60" align="center" />
|
||||||
|
<el-table-column label="状态" width="100" align="center">
|
||||||
|
<template slot-scope="s">
|
||||||
|
<el-tag :type="tagType(s.row)" size="small" effect="dark">{{ tagLabel(s.row) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="180" 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" @click="handleReOrder(s.row)">再次下单</el-button>
|
||||||
|
<el-button size="mini" type="text" style="color:#e6a23c" @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="820px" append-to-body>
|
||||||
|
<div v-if="detailData">
|
||||||
|
<!-- 时间轴 -->
|
||||||
|
<div class="timeline">
|
||||||
|
<div class="tl-node">
|
||||||
|
<div class="tl-dot" style="background:#909399"></div>
|
||||||
|
<div class="tl-content"><div class="tl-title">待发</div><div class="tl-time">{{ detailData.createTime || '-' }}</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="tl-node">
|
||||||
|
<div class="tl-dot" style="background:#4A6FA5"></div>
|
||||||
|
<div class="tl-content"><div class="tl-title">运输中</div><div class="tl-time">—</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="tl-node">
|
||||||
|
<div class="tl-dot" style="background:#67c23a"></div>
|
||||||
|
<div class="tl-content"><div class="tl-title">已收货</div><div class="tl-time">{{ detailData.actualCloseDate || '-' }}</div></div>
|
||||||
|
</div>
|
||||||
|
</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"><el-tag type="success" size="small" effect="dark">HISTORY</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.actualCloseDate || '-' }}</span></div>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listDelivery, getDelivery, addDelivery, recallDelivery } from "@/api/bid/delivery"
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "OrderHistory",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false, list: [], total: 0, stats: {},
|
||||||
|
closeDateRange: null,
|
||||||
|
q: { pageNum: 1, pageSize: 20, deliveryStatus: "history", doNo: "", supplierName: "" },
|
||||||
|
detailOpen: false, detailData: null,
|
||||||
|
statCards: [
|
||||||
|
{ key: "totalHistory", label: "历史订单总数", icon: "el-icon-document-copy", color: "#4A6FA5" },
|
||||||
|
{ key: "monthCompleted", label: "本月完成", icon: "el-icon-circle-check", color: "#67c23a" },
|
||||||
|
{ key: "totalAmount", label: "总金额", icon: "el-icon-money", color: "#e6a23c" },
|
||||||
|
{ key: "avgDeliveryDays", label: "平均交期(天)", icon: "el-icon-data-line", color: "#8e44ad" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() { this.getList(); this.getStats() },
|
||||||
|
methods: {
|
||||||
|
getList() {
|
||||||
|
this.loading = true
|
||||||
|
const p = { ...this.q }
|
||||||
|
if (this.closeDateRange && this.closeDateRange.length === 2) {
|
||||||
|
p.params = { beginTime: this.closeDateRange[0], endTime: this.closeDateRange[1] }
|
||||||
|
}
|
||||||
|
listDelivery(p).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 })
|
||||||
|
},
|
||||||
|
getStats() {
|
||||||
|
request({ url: '/bid/delivery/history/stats', method: 'get' }).then(r => { this.stats = r.data || {} }).catch(() => {})
|
||||||
|
},
|
||||||
|
handleSearch() { this.q.pageNum = 1; this.getList() },
|
||||||
|
resetSearch() { this.q.doNo = ""; this.q.supplierName = ""; this.closeDateRange = null; this.q.params = {}; this.handleSearch() },
|
||||||
|
|
||||||
|
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(); this.getStats() }).catch(() => {})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleReOrder(row) {
|
||||||
|
this.$modal.confirm("确认基于此订单再次下单?将创建新的发货单。").then(() => getDelivery(row.doId))
|
||||||
|
.then(r => {
|
||||||
|
const d = r.data
|
||||||
|
return addDelivery({
|
||||||
|
rfqId: d.rfqId, quotationId: d.quotationId, supplierId: d.supplierId,
|
||||||
|
deliveryStatus: "pending", totalAmount: d.totalAmount,
|
||||||
|
items: (d.items || []).map(it => ({
|
||||||
|
materialId: it.materialId, materialName: it.materialName, spec: it.spec,
|
||||||
|
unit: it.unit, quantity: it.quantity, unitPrice: it.unitPrice, totalPrice: it.totalPrice
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}).then(() => {
|
||||||
|
this.$modal.msgSuccess("已创建新发货单,请到待发订单查看")
|
||||||
|
}).catch(() => {})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 交期差异计算
|
||||||
|
diffDays(row) {
|
||||||
|
if (!row.deliveryDate || !row.actualCloseDate) return null
|
||||||
|
return Math.round((new Date(row.actualCloseDate) - new Date(row.deliveryDate)) / 86400000)
|
||||||
|
},
|
||||||
|
diffLabel(row) {
|
||||||
|
const d = this.diffDays(row)
|
||||||
|
if (d === null) return '-'
|
||||||
|
if (d < 0) return '提前' + Math.abs(d) + '天'
|
||||||
|
if (d === 0) return '准时'
|
||||||
|
return '延期' + d + '天'
|
||||||
|
},
|
||||||
|
diffClass(row) {
|
||||||
|
const d = this.diffDays(row)
|
||||||
|
if (d === null) return ''
|
||||||
|
if (d < 0) return 'diff-early'
|
||||||
|
if (d === 0) return 'diff-ontime'
|
||||||
|
return 'diff-late'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 状态标签
|
||||||
|
tagType(row) {
|
||||||
|
const d = this.diffDays(row)
|
||||||
|
if (d === null) return 'success'
|
||||||
|
if (d < 0) return 'primary'
|
||||||
|
if (d === 0) return 'success'
|
||||||
|
return 'warning'
|
||||||
|
},
|
||||||
|
tagLabel(row) {
|
||||||
|
const d = this.diffDays(row)
|
||||||
|
if (d === null) return '已完成'
|
||||||
|
if (d < 0) return '提前完成'
|
||||||
|
if (d === 0) return '正常完成'
|
||||||
|
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;
|
||||||
|
display: flex; align-items: center; gap: 12px;
|
||||||
|
}
|
||||||
|
.page-title { font-size: 16px; font-weight: 700; color: #1a2c4e; }
|
||||||
|
.header-right { 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;
|
||||||
|
}
|
||||||
|
.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; margin-bottom: 12px;
|
||||||
|
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.search-right { margin-left: auto; }
|
||||||
|
|
||||||
|
/* ═══════ 表格 ═══════ */
|
||||||
|
.order-table { }
|
||||||
|
.amount { color: #409EFF; font-weight: 700; }
|
||||||
|
|
||||||
|
/* ═══════ 交期差异颜色 ═══════ */
|
||||||
|
.diff-early { color: #67c23a; font-weight: 600; }
|
||||||
|
.diff-ontime { color: #909399; font-weight: 600; }
|
||||||
|
.diff-late { color: #f56c6c; font-weight: 600; }
|
||||||
|
|
||||||
|
/* ═══════ 详情 ═══════ */
|
||||||
|
.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; }
|
||||||
|
|
||||||
|
/* ═══════ 时间轴 ═══════ */
|
||||||
|
.timeline {
|
||||||
|
display: flex; justify-content: center; gap: 0; padding: 16px 0 24px;
|
||||||
|
}
|
||||||
|
.tl-node {
|
||||||
|
display: flex; flex-direction: column; align-items: center; gap: 6px;
|
||||||
|
position: relative; flex: 1; max-width: 140px;
|
||||||
|
}
|
||||||
|
.tl-node::after {
|
||||||
|
content: ''; position: absolute; top: 8px; left: 50%; width: 100%;
|
||||||
|
height: 2px; background: #e4e7ed; z-index: 0;
|
||||||
|
}
|
||||||
|
.tl-node:last-child::after { display: none; }
|
||||||
|
.tl-dot {
|
||||||
|
width: 18px; height: 18px; border-radius: 50%; z-index: 1;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
}
|
||||||
|
.tl-content { text-align: center; z-index: 1; }
|
||||||
|
.tl-title { font-size: 13px; font-weight: 600; color: #303133; }
|
||||||
|
.tl-time { font-size: 11px; color: #909399; margin-top: 2px; }
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user