- 33 个 ListView.vue 加 :borderless=true + :page-sizes=[10,20,50,100]
覆盖: order/inquiry/finance/operateguide/sys/Dept|Menu|File|Notice|
Params|User|Role|Post|DictType|DictData|Product|Schedule|RoleGroup
user/Enterprise|enterpriselevel|enterprisescore|enterpriserating
supplier/promoter|couponscope|historylog|logistics|ordercomment|Activity
other/goodunit|label|textconfig
api/feedback|help|lunbo
openapi/AuditLog|CompanyAccount
- OrderListView: columns 抽到 script setup (computed) + :borderless +
:page-sizes + sub 动态显示选中数
- router/index.ts: /product/shop → redirect /supplier/good
(删 src/modules/product/shop/ShopListView.vue 旧占位, 后端 404 无意义)
- 沿用 W17 设计规范 v2 (PageHeader+SearchBar+DataTable+ListTabs)
312 lines
14 KiB
Vue
312 lines
14 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* F006 订单管理列表 - W9 A3 标准化示范
|
|
*
|
|
* 沿用老 vue order/order-good.vue 289 行 + order/serviceorder 154 行等:
|
|
* - 搜索: 订单编号/合同编号/供应商/采购方/状态/创建时间范围
|
|
* - 状态 tab: 全部/待支付/待发货/待收货/待评价/已完成/已取消
|
|
* - 表格列: 订单号/合同号/供应商/采购方/订单类型/金额/已发/状态/时间
|
|
* - 操作: 详情/付款/发货/收货/取消/退款/导出
|
|
* - 批量导出
|
|
* - 通过 query.type 区分 good/service/group
|
|
*
|
|
* 字段: id/orderNo/contractCode/enterpriseName/orderUserName/orderType/totalAmount/totalNum/shippedNum/status/createDate/payDate/payType/pickupType
|
|
* 端点: /order/ordergood/page 200 (按 type 区分 good/service/group)
|
|
*
|
|
* W9 A3: 用 PageHeader / SearchBar 组件替换硬编码, 用 DataTable type:'selection' 替换裸表
|
|
*/
|
|
import { ref, onMounted, reactive, computed } from 'vue'
|
|
import { useRouter, useRoute } from 'vue-router'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { httpGet } from '@/utils/request'
|
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
|
import ListTabs from '@/components/ListTabs/ListTabs.vue'
|
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
|
|
|
interface Order {
|
|
id: string
|
|
orderNo: string
|
|
contractCode: string
|
|
enterpriseName: string
|
|
orderUserName: string
|
|
orderType: number
|
|
totalAmount: number
|
|
totalNum: number
|
|
shippedNum: number
|
|
status: number
|
|
createDate: string
|
|
payDate: string | null
|
|
payType: number
|
|
pickupType: number
|
|
raw: any
|
|
}
|
|
|
|
const STATUS_LABEL: Record<number, string> = {
|
|
1: '待支付', 2: '待发货', 3: '待收货', 4: '待付尾款', 5: '待评价', 6: '已完成', 7: '已取消', 8: '采购单待审核', 9: '付款单待审核'
|
|
}
|
|
|
|
const STATUS_TABS = [
|
|
{ value: '', label: '全部' },
|
|
{ value: '1', label: '待支付' },
|
|
{ value: '2', label: '待发货' },
|
|
{ value: '3', label: '待收货' },
|
|
{ value: '4', label: '待付尾款' },
|
|
{ value: '5', label: '待评价' },
|
|
{ value: '6', label: '已完成' },
|
|
{ value: '7', label: '已取消' }
|
|
]
|
|
|
|
const FALLBACK: Order[] = [
|
|
{ id: 'O001', orderNo: 'O20260705001', contractCode: 'HT-2026-07001', enterpriseName: '深圳科技股份有限公司', orderUserName: '深圳科技股份有限公司', orderType: 1, totalAmount: 12500, totalNum: 5, shippedNum: 5, status: 3, createDate: '2026-07-05 10:24:33', payDate: '2026-07-05 10:35:12', payType: 1, pickupType: 1, raw: {} },
|
|
{ id: 'O002', orderNo: 'O20260705002', contractCode: 'HT-2026-07002', enterpriseName: '上海制造企业', orderUserName: '中建八局', orderType: 1, totalAmount: 158500, totalNum: 100, shippedNum: 60, status: 2, createDate: '2026-07-05 09:15:00', payDate: '2026-07-05 09:30:00', payType: 2, pickupType: 1, raw: {} },
|
|
{ id: 'O003', orderNo: 'O20260705003', contractCode: '', enterpriseName: '北京电子有限公司', orderUserName: '北京城建', orderType: 2, totalAmount: 45800, totalNum: 20, shippedNum: 0, status: 1, createDate: '2026-07-04 16:42:00', payDate: null, payType: 0, pickupType: 1, raw: {} },
|
|
{ id: 'O004', orderNo: 'O20260705004', contractCode: 'HT-2026-07004', enterpriseName: '广州工厂', orderUserName: '广州建筑', orderType: 1, totalAmount: 89000, totalNum: 1, shippedNum: 0, status: 4, createDate: '2026-07-04 14:30:00', payDate: '2026-07-04 14:35:00', payType: 3, pickupType: 1, raw: {} },
|
|
{ id: 'O005', orderNo: 'O20260705005', contractCode: '', enterpriseName: '杭州电子', orderUserName: '杭州电子', orderType: 1, totalAmount: 320000, totalNum: 1, shippedNum: 1, status: 5, createDate: '2026-07-04 11:08:00', payDate: '2026-07-04 11:10:00', payType: 1, pickupType: 2, raw: {} },
|
|
{ id: 'O006', orderNo: 'O20260705006', contractCode: 'HT-2026-07006', enterpriseName: '深圳科技股份有限公司', orderUserName: '上海建工', orderType: 1, totalAmount: 45000, totalNum: 10, shippedNum: 0, status: 6, createDate: '2026-07-04 10:00:00', payDate: '2026-07-04 10:05:00', payType: 1, pickupType: 1, raw: {} },
|
|
{ id: 'O007', orderNo: 'O20260705007', contractCode: '', enterpriseName: '上海制造企业', orderUserName: '上海建工', orderType: 1, totalAmount: 56000, totalNum: 3, shippedNum: 0, status: 7, createDate: '2026-07-04 09:00:00', payDate: null, payType: 0, pickupType: 1, raw: {} },
|
|
{ id: 'O008', orderNo: 'O20260705008', contractCode: 'HT-2026-07008', enterpriseName: '北京电子有限公司', orderUserName: '北京城建', orderType: 2, totalAmount: 120000, totalNum: 1, shippedNum: 1, status: 3, createDate: '2026-07-04 08:30:00', payDate: '2026-07-04 08:35:00', payType: 2, pickupType: 1, raw: {} }
|
|
]
|
|
|
|
const list = ref<Order[]>([])
|
|
const total = ref(0)
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const page = ref(1)
|
|
const limit = ref(10)
|
|
const filters = reactive({
|
|
orderNo: '',
|
|
contractCode: '',
|
|
enterpriseName: '',
|
|
status: '',
|
|
startDate: '',
|
|
endDate: ''
|
|
})
|
|
const selection = ref<Order[]>([])
|
|
const columns: any = computed(() => [
|
|
{ type: 'selection', label: '', width: 50 },
|
|
{ prop: 'orderNo', label: '订单号', width: 180 },
|
|
{ prop: 'contractCode', label: '合同号', width: 160, slot: 'contractCode' },
|
|
{ prop: 'enterpriseName', label: '供应商', minWidth: 180, slot: 'enterpriseName' },
|
|
{ prop: 'orderUserName', label: '采购方', minWidth: 160, slot: 'orderUserName' },
|
|
{ prop: 'totalAmount', label: '金额', width: 130, align: 'right', slot: 'amount' },
|
|
{ prop: 'totalNum', label: '数量', width: 80, align: 'center' },
|
|
{ prop: 'shippedNum', label: '已发', width: 80, align: 'center' },
|
|
{ prop: 'status', label: '状态', width: 110, align: 'center', slot: 'status' },
|
|
{ prop: 'createDate', label: '下单时间', width: 170, slot: 'time' },
|
|
{ label: '操作', width: 280, align: 'center', slot: 'action' }
|
|
])
|
|
const tabItems = computed(() => STATUS_TABS.map(t => ({
|
|
key: t.value,
|
|
label: t.label,
|
|
count: t.value === '' ? stats.all : (stats[Number(t.value)] || 0)
|
|
})))
|
|
const stats = reactive<Record<number|string, number>>({ 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, all: 0 })
|
|
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
const listType = computed(() => {
|
|
const path = route.path
|
|
if (path.includes('/serviceorder') || path.includes('/service')) return 'service'
|
|
if (path.includes('/group-supplier') || path.includes('/groupsupplierorder')) return 'groupsupplier'
|
|
if (path.includes('/group-service') || path.includes('/groupserviceorder')) return 'groupservice'
|
|
if (path.includes('/after')) return 'aftersale'
|
|
if (path.includes('/commission')) return 'commission'
|
|
return 'good'
|
|
})
|
|
|
|
// W12.2 修正: 后端 OpenApiOrderController 只暴露 /openapi/order/list 一个端点,
|
|
// 旧路径 (/order/ordergood/page 等) 404。所有 5 种订单类型都走统一端点。
|
|
const endpoint = computed(() => '/sys/openapi/order/list')
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
// W12.3 修复: httpGet() 已解开 res.data.data, 所以 r 直接是 {total, list, page, limit}
|
|
const r: any = await httpGet(endpoint.value, { page: page.value, limit: limit.value })
|
|
const respList: any[] = Array.isArray(r?.list) ? r.list : []
|
|
const rows: Order[] = respList.map((it: any) => ({
|
|
id: it.orderId || '',
|
|
orderNo: it.orderId || '',
|
|
contractCode: '',
|
|
enterpriseName: '',
|
|
orderUserName: '',
|
|
orderType: 1,
|
|
totalAmount: Number(it.totalAmount ?? 0),
|
|
totalNum: Number(it.goodsQuantity ?? 0),
|
|
shippedNum: 0,
|
|
status: Number(it.status ?? 0),
|
|
createDate: it.createDate ? String(it.createDate) : '',
|
|
payDate: null,
|
|
payType: 0,
|
|
pickupType: 1,
|
|
raw: it
|
|
}))
|
|
list.value = rows
|
|
// 后端 total 是字符串 "1356", parseInt 兜底
|
|
const t = r?.total
|
|
total.value = typeof t === 'string' ? parseInt(t, 10) : (Number(t) || 0)
|
|
// 各 status 计数
|
|
Object.keys(stats).forEach(k => (stats as any)[k] = 0)
|
|
stats.all = rows.length
|
|
rows.forEach(r => { stats[r.status] = ((stats[r.status] as number) || 0) + 1 })
|
|
} catch (e: any) {
|
|
error.value = e?.msg || e?.message || '加载失败'
|
|
list.value = FALLBACK
|
|
total.value = FALLBACK.length
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function onSearch() { page.value = 1; load() }
|
|
function fmtDate(ts?: string | number | null) {
|
|
if (!ts) return '--'
|
|
let n: number
|
|
if (typeof ts === 'string') {
|
|
n = /^\d+$/.test(ts) ? Number(ts) : Date.parse(ts)
|
|
} else {
|
|
n = ts
|
|
}
|
|
if (!n || isNaN(n)) return String(ts)
|
|
const d = new Date(n)
|
|
if (isNaN(d.getTime())) return String(ts)
|
|
const pad = (x: number) => String(x).padStart(2, '0')
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
}
|
|
|
|
function onReset() {
|
|
filters.orderNo = ''
|
|
filters.contractCode = ''
|
|
filters.enterpriseName = ''
|
|
filters.status = ''
|
|
filters.startDate = ''
|
|
filters.endDate = ''
|
|
load()
|
|
}
|
|
function onPageChange(p: number) { page.value = p; load() }
|
|
function onLimitChange(l: number) { limit.value = l; load() }
|
|
function onSelectionChange(rows: Order[]) { selection.value = rows }
|
|
|
|
function onStatusTabChange(status: string) { filters.status = status; onSearch() }
|
|
function onViewDetail(row: Order) { router.push(`/order/${row.id}`) }
|
|
function onPay(row: Order) { ElMessage.success(`付款 ${row.orderNo} (mock)`); load() }
|
|
function onShip(row: Order) { ElMessage.success(`发货 ${row.orderNo} (mock)`); load() }
|
|
function onConfirmReceipt(row: Order) { ElMessage.success(`确认收货 ${row.orderNo} (mock)`); load() }
|
|
function onRefund(row: Order) { ElMessage.success(`退款 ${row.orderNo} (mock)`); load() }
|
|
function onCancel(row: Order) { ElMessage.success(`取消 ${row.orderNo} (mock)`); load() }
|
|
function onPrint(row: Order) { ElMessage.info(`打印 ${row.orderNo} (mock)`) }
|
|
function onExportSelected() {
|
|
if (!selection.value.length) { ElMessage.warning('请先选择订单'); return }
|
|
ElMessage.success(`导出 ${selection.value.length} 个订单 (mock)`)
|
|
}
|
|
|
|
onMounted(load)
|
|
</script>
|
|
|
|
<template>
|
|
<div class="rr-page">
|
|
<PageHeader title="订单管理" :sub="`共 ${total} 条订单${selection.length ? `, 已选 ${selection.length}` : ''}`">
|
|
<el-button v-if="selection.length > 0" type="primary" @click="onExportSelected">
|
|
导出选中 ({{ selection.length }})
|
|
</el-button>
|
|
</PageHeader>
|
|
|
|
<ListTabs v-model="filters.status" :items="tabItems" @change="onStatusTabChange" />
|
|
|
|
<SearchBar
|
|
v-model:filters="filters"
|
|
:fields="[
|
|
{ key: 'orderNo', label: '订单号', placeholder: '请输入订单号' },
|
|
{ key: 'contractCode', label: '合同号', placeholder: '请输入合同号' },
|
|
{ key: 'enterpriseName', label: '供应商', placeholder: '请输入供应商名称' },
|
|
{ key: 'startDate', label: '开始日期', type: 'date', placeholder: '选择开始日期' },
|
|
{ key: 'endDate', label: '结束日期', type: 'date', placeholder: '选择结束日期' }
|
|
]"
|
|
@search="onSearch"
|
|
@reset="onReset"
|
|
/>
|
|
|
|
<DataTable
|
|
:columns="columns"
|
|
:data="list"
|
|
:loading="loading"
|
|
:total="total"
|
|
:page="page"
|
|
:limit="limit"
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
:show-pagination="true"
|
|
:error="error"
|
|
empty-text="暂无订单"
|
|
:borderless="true"
|
|
row-key="id"
|
|
@page-change="onPageChange"
|
|
@limit-change="onLimitChange"
|
|
@selection-change="onSelectionChange"
|
|
>
|
|
<template #contractCode="{ row }">
|
|
<span v-if="row.contractCode">{{ row.contractCode }}</span>
|
|
<span v-else class="rr-muted">--</span>
|
|
</template>
|
|
<template #enterpriseName="{ row }">
|
|
<span v-if="row.enterpriseName">{{ row.enterpriseName }}</span>
|
|
<span v-else class="rr-muted">--</span>
|
|
</template>
|
|
<template #orderUserName="{ row }">
|
|
<span v-if="row.orderUserName">{{ row.orderUserName }}</span>
|
|
<span v-else class="rr-muted">--</span>
|
|
</template>
|
|
<template #time="{ row }">
|
|
<span class="rr-time">{{ fmtDate(row.createDate) }}</span>
|
|
</template>
|
|
<template #amount="{ row }">
|
|
<span class="rr-amount">¥ {{ row.totalAmount.toLocaleString() }}</span>
|
|
</template>
|
|
<template #status="{ row }">
|
|
<el-tag
|
|
:type="(STATUS_LABEL[row.status] === '已完成' ? 'success' : STATUS_LABEL[row.status] === '已取消' ? 'info' : 'primary') as any"
|
|
size="small"
|
|
effect="light"
|
|
>
|
|
{{ STATUS_LABEL[row.status] || `状态${row.status}` }}
|
|
</el-tag>
|
|
</template>
|
|
<template #action="{ row }">
|
|
<span class="rr-actions">
|
|
<el-link type="primary" :underline="false" @click="onViewDetail(row)">详情</el-link>
|
|
<span class="rr-actions__sep">|</span>
|
|
<el-link v-if="row.status === 1" type="primary" :underline="false" @click="onPay(row)">付款</el-link>
|
|
<el-link v-else-if="row.status === 2" type="primary" :underline="false" @click="onShip(row)">发货</el-link>
|
|
<el-link v-else-if="row.status === 3" type="primary" :underline="false" @click="onConfirmReceipt(row)">收货</el-link>
|
|
<el-link v-else-if="[1,2,3,4].includes(row.status)" type="warning" :underline="false" @click="onRefund(row)">退款</el-link>
|
|
<span v-if="[1,2].includes(row.status)" class="rr-actions__sep">|</span>
|
|
<el-link v-if="[1,2].includes(row.status)" type="danger" :underline="false" @click="onCancel(row)">取消</el-link>
|
|
<span class="rr-actions__sep">|</span>
|
|
<el-link :underline="false" @click="onPrint(row)">打印</el-link>
|
|
</span>
|
|
</template>
|
|
</DataTable>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.rr-page { max-width: 1500px; }
|
|
.rr-muted { color: var(--ink-3); }
|
|
.rr-actions {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0;
|
|
&__sep {
|
|
color: var(--rr-border-color, #E2E8F0);
|
|
margin: 0 8px;
|
|
font-size: 12px;
|
|
}
|
|
:deep(.el-link) {
|
|
font-size: 13px;
|
|
}
|
|
}
|
|
.rr-amount { font-weight: 600; color: var(--primary); }
|
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--ink-2); }
|
|
</style>
|