admin_new/src/modules/order/OrderListView.vue
Sisyphus 4b77d67dc2 fix(F006-W17.3.7): list 页统一规范 (:borderless + :page-sizes) + product/shop redirect
- 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)
2026-07-07 16:54:37 +08:00

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>