admin_new/src/modules/order/OrderListView.vue

274 lines
13 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 } from 'element-plus'
import { httpGet } from '@/utils/request'
import PageHeader from '@/components/PageHeader/PageHeader.vue'
import SearchBar from '@/components/SearchBar/SearchBar.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 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 {
const r: any = await httpGet(endpoint.value, { page: page.value, limit: limit.value })
const respList = r?.data?.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 是 string, 转 number
const t = r?.data?.total
total.value = typeof t === 'string' ? parseInt(t, 10) : (Number(t) || 0)
} 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 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}`">
<el-button v-if="selection.length > 0" type="primary" @click="onExportSelected">
导出选中 ({{ selection.length }})
</el-button>
</PageHeader>
<!-- 状态 tab -->
<div class="rr-tabs-row">
<div class="rr-tabs">
<div
v-for="tab in STATUS_TABS"
:key="tab.value"
class="rr-tab"
:class="{ active: filters.status === tab.value }"
@click="onStatusTabChange(tab.value)"
>
{{ tab.label }}
</div>
</div>
</div>
<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"
/>
<div class="rr-card">
<DataTable
:columns="[
{ type: 'selection', label: '', width: 50 },
{ prop: 'orderNo', label: '订单号', width: 180 },
{ prop: 'contractCode', label: '合同号', width: 160, slot: 'contractCode' },
{ prop: 'enterpriseName', label: '供应商', minWidth: 180 },
{ prop: 'orderUserName', label: '采购方', minWidth: 160 },
{ 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 },
{ label: '操作', width: 280, align: 'center', slot: 'action' }
]"
:data="list"
:loading="loading"
:total="total"
:page="page"
:limit="limit"
:show-pagination="total > 10"
:error="error"
empty-text="暂无订单"
borderless
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 #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 }">
<el-button text type="primary" size="small" @click="onViewDetail(row)">详情</el-button>
<el-button v-if="row.status === 1" text type="primary" size="small" @click="onPay(row)">付款</el-button>
<el-button v-if="row.status === 2" text type="primary" size="small" @click="onShip(row)">发货</el-button>
<el-button v-if="row.status === 3" text type="primary" size="small" @click="onConfirmReceipt(row)">收货</el-button>
<el-button v-if="[1,2,3,4].includes(row.status)" text type="warning" size="small" @click="onRefund(row)">退款</el-button>
<el-button v-if="[1,2].includes(row.status)" text type="danger" size="small" @click="onCancel(row)">取消</el-button>
<el-button text size="small" @click="onPrint(row)">打印</el-button>
</template>
</DataTable>
</div>
</div>
</template>
<style scoped lang="scss">
.rr-page { max-width: 1500px; }
.rr-tabs-row { margin-bottom: 16px; }
.rr-tabs { display: flex; gap: 4px; padding: 4px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); width: fit-content; flex-wrap: wrap; }
.rr-tab { padding: 6px 16px; border-radius: var(--radius); font-size: 13px; color: var(--ink-2); cursor: pointer; transition: all 0.15s; }
.rr-tab:hover { background: var(--bg-soft); }
.rr-tab.active { color: white; background: var(--primary); font-weight: 500; }
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
.rr-muted { color: var(--ink-3); }
.rr-amount { font-weight: 600; color: var(--primary); }
</style>