feat(F006-W15): 订单售后/退款/支付回调 3 view 接前端 max-effort mock + 同步 DataTable selection

This commit is contained in:
Sisyphus 2026-07-07 09:58:21 +08:00
parent b3018fe7d7
commit 04d3992c52
7 changed files with 670 additions and 6 deletions

View File

@ -44,7 +44,7 @@ interface Column {
label: string
width?: number | string
minWidth?: number | string
type?: 'status' | 'text'
type?: 'status' | 'text' | 'selection'
statusMap?: Record<string, { label: string; type: string }>
slot?: string
formatter?: (row: any) => string
@ -87,6 +87,7 @@ const emit = defineEmits<{
(e: 'row-click', row: any): void
(e: 'page-change', page: number): void
(e: 'limit-change', limit: number): void
(e: 'selection-change', rows: any[]): void
}>()
const hasStatusCol = computed(() => props.columns.some(c => c.type === 'status'))
@ -97,6 +98,7 @@ function getStatusMeta(col: Column, value: string) {
return col.statusMap?.[value] || { label: value || '--', type: 'info' }
}
function onSelectionChange(rows: any[]) { emit('selection-change', rows) }
function onRowClick(row: any) {
emit('row-click', row)
}
@ -118,6 +120,7 @@ function onLimitChange(l: number) {
:row-key="rowKey"
style="width: 100%"
@row-click="onRowClick"
@selection-change="onSelectionChange"
row-class-name="rr-clickable-row"
:header-row-style="{ height: '40px' }"
:row-style="{ height: '52px' }"
@ -141,9 +144,15 @@ function onLimitChange(l: number) {
height: '52px'
}"
>
<template v-for="(col, idx) in columns" :key="idx">
<el-table-column
v-for="(col, idx) in columns"
:key="idx"
v-if="col.type === 'selection'"
type="selection"
:width="col.width || 50"
:align="col.align || 'center'"
/>
<el-table-column
v-else
:prop="col.type === 'status' || col.slot ? undefined : col.prop"
:label="col.label"
:width="col.width"
@ -167,6 +176,7 @@ function onLimitChange(l: number) {
{{ col.formatter?.(row) }}
</template>
</el-table-column>
</template>
<template #empty>
<div class="rr-empty">

View File

@ -71,6 +71,9 @@ export const MENU_LABELS: MenuLabel[] = [
{ url: 'order/ordergood-commission', label: '实物订单分佣统计', icon: 'Coin', path: '/order/ordergood/commission', sort: 220 },
{ url: 'order/serviceorder-commission', label: '服务订单分佣统计', icon: 'Coin', path: '/order/serviceorder/commission', sort: 221 },
{ url: 'order/orderbase-price-analysis', label: '基础价格分析表', icon: 'TrendCharts', path: '/order/base-price-analysis', sort: 230 },
{ url: 'order/orderpay', label: '订单支付回调', icon: 'Wallet', path: '/order/orderpay', sort: 240 },
{ url: 'order/refund', label: '订单退款管理', icon: 'Refresh', path: '/order/refund', sort: 250 },
{ url: 'order/aftersale', label: '售后订单', icon: 'Service', path: '/order/aftersale', sort: 260 },
// ===== 供应商模块 =====
{ url: 'supplier/good', label: '实物商品', icon: 'Box', path: '/supplier/good', sort: 300 },

View File

@ -0,0 +1,220 @@
<script setup lang="ts">
/**
* F006 实物订单售后 - W15.5
*
* 沿用老 vue order/orderafter.vue 字段:
* afterSalesOrderId / shopType / afterSaleType / enterpriseName / name / title
* returnQuantity / totalAmount / requestTime / status (1=发起 2=处理中 3=同意 4=不同意 5=寄回 6=完成 7=取消)
*
* 端点: /order/aftersale/page 探测; 404 → FALLBACK seed (14 条)
*/
import { ref, reactive, onMounted, computed } from 'vue'
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 DataTable from '@/components/DataTable/DataTable.vue'
interface AftersaleRow {
afterSalesOrderId: string
shopType: number
afterSaleType: number
enterpriseName: string
name: string
title: string
returnQuantity: number
totalAmount: number
requestTime: string
status: number
reason: string
}
const SHOP_LABEL: Record<number, string> = { 1: '实物商品', 2: '服务商品', 3: '拼团商品', 4: '虚拟商品' }
const TYPE_LABEL: Record<number, string> = { 1: '仅退款', 2: '退货退款', 3: '换货', 4: '维修' }
const STATUS_LABEL: Record<number, string> = { 1: '发起申请', 2: '处理中', 3: '同意退/换', 4: '不同意', 5: '货物寄回', 6: '完成', 7: '取消' }
const STATUS_TYPE: Record<number, string> = { 1:'warning', 2:'primary', 3:'success', 4:'danger', 5:'info', 6:'success', 7:'info' }
const ENDPOINTS = ['/order/aftersale/page', '/order/aftersale/list', '/order/aftersale/search', '/order/aftersale/queryPage']
const FALLBACK: AftersaleRow[] = [
{ afterSalesOrderId:'AS20260701001', shopType:1, afterSaleType:2, enterpriseName:'坚美建筑材料有限公司', name:'张老板', title:'国标槽钢 10# 6m 热镀锌', returnQuantity:5, totalAmount:3450.00, requestTime:'2026-07-01 14:20:00', status:3, reason:'型号发错' },
{ afterSalesOrderId:'AS20260701002', shopType:1, afterSaleType:1, enterpriseName:'华建门窗厂', name:'李工', title:'断桥铝平开窗 60系列', returnQuantity:2, totalAmount:1800.00, requestTime:'2026-07-02 09:18:42', status:6, reason:'协商退款' },
{ afterSalesOrderId:'AS20260702001', shopType:1, afterSaleType:2, enterpriseName:'融鑫管业', name:'王经理', title:'PPR给水管 dn25 4m', returnQuantity:20, totalAmount:1260.00, requestTime:'2026-07-02 16:32:11', status:2, reason:'包装破损' },
{ afterSalesOrderId:'AS20260702002', shopType:1, afterSaleType:4, enterpriseName:'东方五金机械', name:'赵师傅', title:'电锤 GBH 2-28 800W', returnQuantity:1, totalAmount:680.00, requestTime:'2026-07-03 11:42:30', status:4, reason:'已过维保期' },
{ afterSalesOrderId:'AS20260703001', shopType:1, afterSaleType:3, enterpriseName:'美洁涂料', name:'陈女士', title:'环保内墙乳胶漆 5L', returnQuantity:4, totalAmount:920.00, requestTime:'2026-07-03 13:28:00', status:5, reason:'颜色偏差' },
{ afterSalesOrderId:'AS20260703002', shopType:1, afterSaleType:2, enterpriseName:'万家灯具', name:'周总', title:'LED投光灯 200W IP65', returnQuantity:3, totalAmount:1080.00, requestTime:'2026-07-04 08:15:55', status:1, reason:'不亮灯' },
{ afterSalesOrderId:'AS20260704001', shopType:2, afterSaleType:1, enterpriseName:'顺达物流', name:'吴先生', title:'同城配送服务 (50km内)', returnQuantity:1, totalAmount:280.00, requestTime:'2026-07-04 16:48:22', status:7, reason:'客户主动取消' },
{ afterSalesOrderId:'AS20260704002', shopType:1, afterSaleType:2, enterpriseName:'华建门窗厂', name:'林总', title:'铝合金推拉门 90系列', returnQuantity:1, totalAmount:2880.00, requestTime:'2026-07-05 10:14:42', status:3, reason:'尺寸不符' },
{ afterSalesOrderId:'AS20260705001', shopType:3, afterSaleType:1, enterpriseName:'众成厨具', name:'黄总', title:'不锈钢汤锅 30cm', returnQuantity:1, totalAmount:128.00, requestTime:'2026-07-05 14:30:00', status:6, reason:'拼团失败退款' },
{ afterSalesOrderId:'AS20260705002', shopType:1, afterSaleType:4, enterpriseName:'东方五金机械', name:'何师傅', title:'角磨机 100mm 720W', returnQuantity:1, totalAmount:380.00, requestTime:'2026-07-06 09:25:00', status:2, reason:'电机异响' },
{ afterSalesOrderId:'AS20260706001', shopType:1, afterSaleType:2, enterpriseName:'坚美建筑材料', name:'钱老板', title:'国标角钢 50×5 6m', returnQuantity:8, totalAmount:2240.00, requestTime:'2026-07-06 11:02:18', status:3, reason:'厚度偏差' },
{ afterSalesOrderId:'AS20260706002', shopType:1, afterSaleType:1, enterpriseName:'美洁涂料', name:'孙总', title:'环保外墙腻子粉 25kg', returnQuantity:6, totalAmount:780.00, requestTime:'2026-07-06 15:48:32', status:1, reason:'施工后剩余' },
{ afterSalesOrderId:'AS20260706003', shopType:1, afterSaleType:2, enterpriseName:'融鑫管业', name:'李经理', title:'PE给水管 dn32 6m', returnQuantity:12, totalAmount:960.00, requestTime:'2026-07-06 17:33:21', status:2, reason:'现场清点异常' },
{ afterSalesOrderId:'AS20260706004', shopType:2, afterSaleType:1, enterpriseName:'顺达物流', name:'郑总', title:'异地整车运输', returnQuantity:1, totalAmount:1680.00, requestTime:'2026-07-06 18:55:08', status:1, reason:'时效不达标' }
]
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: '已取消' }
]
type Col = {
prop?: string; label: string; width?: number; slot?: string; fixed?: 'right' | true; type?: 'status' | 'text' | 'selection'
}
const columns: Col[] = [
{ prop: 'afterSalesOrderId', label: '售后订单号', width: 180, fixed: true, slot: 'orderId' },
{ type: 'status', prop: 'shopType', label: '商品类型', width: 110 },
{ type: 'status', prop: 'afterSaleType', label: '售后类型', width: 100 },
{ prop: 'enterpriseName', label: '供应商信息', width: 200 },
{ prop: 'name', label: '用户信息', width: 120 },
{ prop: 'title', label: '商品信息', width: 240, slot: 'title' },
{ prop: 'returnQuantity', label: '售后数量', width: 90 },
{ prop: 'totalAmount', label: '退款金额', width: 110 },
{ prop: 'requestTime', label: '创建时间', width: 160, slot: 'time' },
{ type: 'status', prop: 'status', label: '订单状态', width: 110 },
{ prop: 'reason', label: '售后原因', width: 200, slot: 'reason' },
{ prop: 'actions', label: '操作', width: 220, fixed: 'right', slot: 'actions' }
]
const list = ref<AftersaleRow[]>([])
const total = ref(0)
const loading = ref(false)
const page = ref(1)
const limit = ref(10)
const filterKw = ref('')
const statusTab = ref('')
const dataSource = ref<'api' | 'mock' | 'loading'>('loading')
const apiError = ref<string>('')
async function load() {
loading.value = true
dataSource.value = 'loading'
for (const ep of ENDPOINTS) {
try {
const params: any = { page: page.value, limit: limit.value }
const data: any = await httpGet<any>(ep, params)
const rows: AftersaleRow[] = (data?.list || []).map((it: any) => ({ ...it }))
list.value = rows
total.value = Number(data?.total || rows.length)
dataSource.value = 'api'
apiError.value = ''
loading.value = false
return
} catch {}
}
list.value = [...FALLBACK]
total.value = FALLBACK.length
dataSource.value = 'mock'
apiError.value = '后端 API 暂未实装, 已加载前端示例数据'
loading.value = false
}
const filtered = computed(() => {
const kw = filterKw.value.trim().toLowerCase()
let rows = list.value
if (statusTab.value) rows = rows.filter(r => String(r.status) === statusTab.value)
if (kw) rows = rows.filter(r =>
String(r.afterSalesOrderId).toLowerCase().includes(kw) ||
String(r.enterpriseName).toLowerCase().includes(kw) ||
String(r.name).toLowerCase().includes(kw) ||
String(r.title).toLowerCase().includes(kw)
)
return rows
})
const searchFields: Array<{ key: string; label: string; type: 'text' }> = [
{ key: 'afterSalesOrderId', label: '售后订单号', type: 'text' },
{ key: 'enterpriseName', label: '供应商', type: 'text' },
{ key: 'title', label: '商品名', type: 'text' }
]
const filters = reactive<Record<string, any>>({})
function onSearch() {
filterKw.value = String(filters.afterSalesOrderId || filters.enterpriseName || filters.title || '')
page.value = 1
}
function onStatusTab(v: string) { statusTab.value = v; page.value = 1 }
async function onAgree(row: AftersaleRow) {
try { await ElMessageBox.confirm(`确认通过售后申请「${row.afterSalesOrderId}」?`, '提示', { type: 'warning' }) } catch { return }
row.status = 3
ElMessage.success('已通过 (前端示例)')
}
async function onReject(row: AftersaleRow) {
try {
const { value } = await ElMessageBox.prompt('请填写拒绝原因', '拒绝售后', { inputPlaceholder: '原因', confirmButtonText: '拒绝', cancelButtonText: '取消' })
row.status = 4
row.reason = (value || '已拒绝').trim()
ElMessage.success('已拒绝 (前端示例)')
} catch {}
}
async function onComplete(row: AftersaleRow) {
row.status = 6
ElMessage.success('已标记完成')
}
function onView(row: AftersaleRow) {
ElMessage.info(`打开售后详情 ${row.afterSalesOrderId}`)
}
function onExport() {
ElMessage.success('已发起导出 (前端示例, 共 ' + filtered.value.length + ' 条)')
}
onMounted(load)
</script>
<template>
<div class="rr-page">
<PageHeader title="实物订单售后" sub="实物商品的退/换/修售后流程审核" :data-source="dataSource" :api-error="apiError">
<el-button type="primary" @click="onExport">导出明细</el-button>
</PageHeader>
<el-tabs v-model="statusTab" @tab-change="onStatusTab" class="rr-tabs">
<el-tab-pane v-for="t in STATUS_TABS" :key="t.value" :name="t.value" :label="t.label" />
</el-tabs>
<SearchBar v-model:filters="filters" :fields="searchFields" @search="onSearch" />
<DataTable
:columns="columns"
:data="filtered"
:loading="loading"
:page="page"
:limit="limit"
:total="total"
row-key="afterSalesOrderId"
@page-change="(p: number) => { page = p; load() }"
@limit-change="(l: number) => { limit = l; page = 1; load() }"
>
<template #orderId="{ row }">
<el-link type="primary" :underline="false" @click="onView(row)">{{ row.afterSalesOrderId }}</el-link>
</template>
<template #title="{ row }">
<span :class="{ 'rr-muted': !row.title }">{{ row.title || '--' }}</span>
</template>
<template #time="{ row }">{{ row.requestTime }}</template>
<template #reason="{ row }">
<span :class="{ 'rr-muted': !row.reason }">{{ row.reason || '--' }}</span>
</template>
<template #actions="{ row }">
<template v-if="row.status === 1 || row.status === 2">
<el-button type="success" link @click="onAgree(row)">同意</el-button>
<el-button type="danger" link @click="onReject(row)">拒绝</el-button>
</template>
<el-button v-if="row.status === 5" type="success" link @click="onComplete(row)">确认收货完成</el-button>
<el-button type="primary" link @click="onView(row)">详情</el-button>
</template>
</DataTable>
</div>
</template>
<style scoped>
.rr-page { padding: 16px; }
.rr-tabs { margin-bottom: 12px; }
.rr-muted { color: var(--ink-muted); }
</style>

View File

@ -0,0 +1,214 @@
<script setup lang="ts">
/**
* F006 订单支付回调列表 - W15.2
*
* 沿用老 vue order/orderpay.vue 字段:
* - 选择 + id + orderUserId + userType + orderId + goodDetailId
* - orderGoodId + orderBatchId + payStatus + sum + couponMoney + couponId
* - freight + payMoney + unitPrice + totalNum + payDate + isDel + pickupType
*
* 端点: /order/orderpay/page 探测 (按 5.18 4 URL + 老 vue 字段口径)
* 端点 404 → 用 FALLBACK 前端 seed 数据 (15 条)
*
* 状态: payStatus 1=预付款 2=发货款 3=到货款 4=安装款 5=结算款 6=质保金 7=已结束
*/
import { ref, reactive, onMounted, computed } from 'vue'
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 DataTable from '@/components/DataTable/DataTable.vue'
interface PaymentRow {
id: string
orderUserId: string
userType: number
orderId: string
goodDetailId: string
orderGoodId: string
orderBatchId: string
payStatus: number
sum: number
couponMoney: number
couponId: string
freight: number
payMoney: number
unitPrice: number
totalNum: number
payDate: string
isDel: number
pickupType: number
}
const PAY_STATUS_LABEL: Record<number, string> = {
1: '预付款', 2: '发货款', 3: '到货款', 4: '安装款', 5: '结算款', 6: '质保金', 7: '已结束'
}
const PICKUP_LABEL: Record<number, string> = { 1: '商家配送', 2: '买家自提' }
const USER_TYPE_LABEL: Record<number, string> = { 1: 'C端用户', 2: '供应链' }
// 端点探测 (W5.18 教训: 4 URL 试探 + 老 vue 字段口径, 找不到走 FALLBACK seed)
const ENDPOINTS = ['/order/orderpay/page', '/order/orderpay/list', '/order/orderpay/search', '/order/orderpay/queryPage']
const FALLBACK: PaymentRow[] = [
{ id:'OP001', orderUserId:'u10086', userType:1, orderId:'O001', goodDetailId:'GD101', orderGoodId:'OG201', orderBatchId:'OB301', payStatus:7, sum:12800.00, couponMoney:300.00, couponId:'C202501', freight:0, payMoney:12500.00, unitPrice:1280.00, totalNum:10, payDate:'2026-07-01 14:32:11', isDel:0, pickupType:1 },
{ id:'OP002', orderUserId:'u10112', userType:2, orderId:'O002', goodDetailId:'GD102', orderGoodId:'OG202', orderBatchId:'OB302', payStatus:3, sum:5600.00, couponMoney:0, couponId:'', freight:80.00, payMoney:5680.00, unitPrice:560.00, totalNum:10, payDate:'2026-07-02 09:18:42', isDel:0, pickupType:1 },
{ id:'OP003', orderUserId:'u10133', userType:1, orderId:'O003', goodDetailId:'GD103', orderGoodId:'OG203', orderBatchId:'OB303', payStatus:5, sum:8800.00, couponMoney:200.00, couponId:'C202503', freight:100.00, payMoney:8700.00, unitPrice:880.00, totalNum:10, payDate:'2026-06-28 16:45:00', isDel:0, pickupType:1 },
{ id:'OP004', orderUserId:'u10144', userType:1, orderId:'O004', goodDetailId:'GD104', orderGoodId:'OG204', orderBatchId:'OB304', payStatus:7, sum:2200.00, couponMoney:0, couponId:'', freight:0, payMoney:2200.00, unitPrice:220.00, totalNum:10, payDate:'2026-07-03 11:02:18', isDel:0, pickupType:2 },
{ id:'OP005', orderUserId:'u10155', userType:2, orderId:'O005', goodDetailId:'GD105', orderGoodId:'OG205', orderBatchId:'OB305', payStatus:6, sum:4500.00, couponMoney:150.00, couponId:'C202505', freight:60.00, payMoney:4410.00, unitPrice:450.00, totalNum:10, payDate:'2026-07-04 08:30:55', isDel:0, pickupType:1 },
{ id:'OP006', orderUserId:'u10166', userType:1, orderId:'O006', goodDetailId:'GD106', orderGoodId:'OG206', orderBatchId:'OB306', payStatus:2, sum:13800.00, couponMoney:500.00, couponId:'C202506', freight:200.00, payMoney:13500.00, unitPrice:1380.00, totalNum:10, payDate:'2026-07-05 13:25:30', isDel:0, pickupType:1 },
{ id:'OP007', orderUserId:'u10177', userType:1, orderId:'O007', goodDetailId:'GD107', orderGoodId:'OG207', orderBatchId:'OB307', payStatus:4, sum:3200.00, couponMoney:0, couponId:'', freight:0, payMoney:3200.00, unitPrice:320.00, totalNum:10, payDate:'2026-07-06 10:12:00', isDel:0, pickupType:2 },
{ id:'OP008', orderUserId:'u10188', userType:2, orderId:'O008', goodDetailId:'GD108', orderGoodId:'OG208', orderBatchId:'OB308', payStatus:1, sum:6800.00, couponMoney:0, couponId:'', freight:120.00, payMoney:6920.00, unitPrice:680.00, totalNum:10, payDate:'', isDel:0, pickupType:1 },
{ id:'OP009', orderUserId:'u10199', userType:1, orderId:'O009', goodDetailId:'GD109', orderGoodId:'OG209', orderBatchId:'OB309', payStatus:3, sum:9800.00, couponMoney:280.00, couponId:'C202509', freight:0, payMoney:9520.00, unitPrice:980.00, totalNum:10, payDate:'2026-07-02 15:48:22', isDel:0, pickupType:1 },
{ id:'OP010', orderUserId:'u10210', userType:1, orderId:'O010', goodDetailId:'GD110', orderGoodId:'OG210', orderBatchId:'OB310', payStatus:7, sum:5400.00, couponMoney:100.00, couponId:'C202510', freight:60.00, payMoney:5360.00, unitPrice:540.00, totalNum:10, payDate:'2026-06-30 12:30:00', isDel:0, pickupType:1 },
{ id:'OP011', orderUserId:'u10221', userType:2, orderId:'O011', goodDetailId:'GD111', orderGoodId:'OG211', orderBatchId:'OB311', payStatus:5, sum:18800.00, couponMoney:600.00, couponId:'C202511', freight:300.00, payMoney:18500.00, unitPrice:1880.00, totalNum:10, payDate:'2026-07-03 09:55:08', isDel:0, pickupType:1 },
{ id:'OP012', orderUserId:'u10232', userType:1, orderId:'O012', goodDetailId:'GD112', orderGoodId:'OG212', orderBatchId:'OB312', payStatus:6, sum:2400.00, couponMoney:0, couponId:'', freight:0, payMoney:2400.00, unitPrice:240.00, totalNum:10, payDate:'2026-07-04 16:20:14', isDel:0, pickupType:2 },
{ id:'OP013', orderUserId:'u10243', userType:1, orderId:'O013', goodDetailId:'GD113', orderGoodId:'OG213', orderBatchId:'OB313', payStatus:7, sum:7200.00, couponMoney:240.00, couponId:'C202513', freight:80.00, payMoney:7040.00, unitPrice:720.00, totalNum:10, payDate:'2026-06-25 11:42:35', isDel:0, pickupType:1 },
{ id:'OP014', orderUserId:'u10254', userType:2, orderId:'O014', goodDetailId:'GD114', orderGoodId:'OG214', orderBatchId:'OB314', payStatus:2, sum:11500.00, couponMoney:400.00, couponId:'C202514', freight:180.00, payMoney:11280.00, unitPrice:1150.00, totalNum:10, payDate:'2026-07-05 14:18:50', isDel:0, pickupType:1 },
{ id:'OP015', orderUserId:'u10265', userType:1, orderId:'O015', goodDetailId:'GD115', orderGoodId:'OG215', orderBatchId:'OB315', payStatus:4, sum:4400.00, couponMoney:0, couponId:'', freight:50.00, payMoney:4450.00, unitPrice:440.00, totalNum:10, payDate:'2026-07-06 17:33:21', isDel:0, pickupType:2 }
]
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: '已结束' }
]
type Col = {
prop?: string; label: string; width?: number; slot?: string; fixed?: 'right' | true; type?: 'status' | 'text' | 'selection'
}
const columns: Col[] = [
{ prop: 'id', label: 'ID', width: 90, fixed: true },
{ prop: 'orderId', label: '订单编号', width: 180, slot: 'orderId' },
{ prop: 'orderUserId', label: '用户ID', width: 100 },
{ type: 'status', prop: 'userType', label: '用户类型', width: 100 },
{ prop: 'orderGoodId', label: '小订单ID', width: 120 },
{ type: 'status', prop: 'payStatus', label: '款项流程', width: 110 },
{ prop: 'sum', label: '优惠前金额', width: 110 },
{ prop: 'couponMoney', label: '优惠金额', width: 100 },
{ prop: 'freight', label: '运费', width: 80 },
{ prop: 'payMoney', label: '实际支付', width: 110 },
{ prop: 'unitPrice', label: '单价', width: 90 },
{ prop: 'totalNum', label: '待发货总数', width: 100 },
{ type: 'status', prop: 'pickupType', label: '配送方式', width: 110 },
{ prop: 'payDate', label: '支付时间', width: 160, slot: 'time' },
{ prop: 'actions', label: '操作', width: 180, fixed: 'right', slot: 'actions' }
]
const list = ref<PaymentRow[]>([])
const total = ref(0)
const loading = ref(false)
const page = ref(1)
const limit = ref(10)
const filterKw = ref('')
const statusTab = ref('')
const dataSource = ref<'api' | 'mock' | 'loading'>('loading')
const apiError = ref<string>('')
async function load() {
loading.value = true
dataSource.value = 'loading'
for (const ep of ENDPOINTS) {
try {
const params: any = { page: page.value, limit: limit.value }
if (statusTab.value) params.payStatus = Number(statusTab.value)
const data: any = await httpGet<any>(ep, params)
const rows: PaymentRow[] = (data?.list || []).map((it: any) => ({ ...it, raw: it }))
list.value = rows
total.value = Number(data?.total || rows.length)
dataSource.value = 'api'
apiError.value = ''
loading.value = false
return
} catch {}
}
// 全部失败 → 前端 seed
list.value = [...FALLBACK]
total.value = FALLBACK.length
dataSource.value = 'mock'
apiError.value = '后端 API 暂未实装, 已加载前端示例数据'
loading.value = false
}
const filtered = computed(() => {
const kw = filterKw.value.trim().toLowerCase()
let rows = list.value
if (statusTab.value) rows = rows.filter(r => String(r.payStatus) === statusTab.value)
if (kw) rows = rows.filter(r =>
String(r.id).toLowerCase().includes(kw) ||
String(r.orderId).toLowerCase().includes(kw) ||
String(r.orderGoodId).toLowerCase().includes(kw)
)
return rows
})
const searchFields: Array<{ key: string; label: string; type: 'text'; placeholder?: string }> = [
{ key: 'id', label: 'ID', type: 'text', placeholder: '支付ID' },
{ key: 'orderId', label: '订单ID', type: 'text', placeholder: '订单ID' },
{ key: 'orderGoodId', label: '小订单ID', type: 'text', placeholder: '小订单ID' }
]
const filters = reactive<Record<string, any>>({})
function onSearch() {
filterKw.value = String(filters.id || filters.orderId || filters.orderGoodId || '')
page.value = 1
}
function onStatusTab(v: string) { statusTab.value = v; page.value = 1 }
async function onMarkPaid(row: PaymentRow) {
try { await ElMessageBox.confirm(`确认「${row.id}」该款项标记为已支付?`, '提示', { type: 'warning' }) } catch { return }
ElMessage.success('已标记 (前端示例)')
}
function onViewOrder(row: PaymentRow) {
ElMessage.info(`打开订单 ${row.orderId}`)
}
function onExport() {
ElMessage.success('已发起导出 (前端示例, 共 ' + filtered.value.length + ' 条)')
}
onMounted(load)
</script>
<template>
<div class="rr-page">
<PageHeader title="订单支付回调" sub="实物订单分阶段支付款项管理" :data-source="dataSource" :api-error="apiError">
<el-button type="primary" @click="onExport">导出明细</el-button>
</PageHeader>
<el-tabs v-model="statusTab" @tab-change="onStatusTab" class="rr-tabs">
<el-tab-pane v-for="t in STATUS_TABS" :key="t.value" :name="t.value" :label="t.label" />
</el-tabs>
<SearchBar v-model:filters="filters" :fields="searchFields" @search="onSearch" />
<DataTable
:columns="columns"
:data="filtered"
:loading="loading"
:page="page"
:limit="limit"
:total="total"
row-key="id"
@page-change="(p: number) => { page = p; load() }"
@limit-change="(l: number) => { limit = l; page = 1; load() }"
>
<template #orderId="{ row }">
<el-link type="primary" :underline="false" @click="onViewOrder(row)">{{ row.orderId }}</el-link>
</template>
<template #time="{ row }">{{ row.payDate || '--' }}</template>
<template #actions="{ row }">
<el-button type="primary" link @click="onViewOrder(row)">详情</el-button>
<el-button v-if="row.payStatus !== 7" type="success" link @click="onMarkPaid(row)">标记已付</el-button>
</template>
</DataTable>
</div>
</template>
<style scoped>
.rr-page { padding: 16px; }
.rr-tabs { margin-bottom: 12px; }
</style>

View File

@ -0,0 +1,212 @@
<script setup lang="ts">
/**
* F006 订单退款管理 - W15.3
*
* 沿用老 vue order/orderrefund.vue 字段:
* - 选择 + id + userId + supplierId + userEnterpriseId + enterpriseId
* - supplierOrderCode + payStatus + payMoney + refundTime + requestTime + remark + delFlag
*
* 端点: /order/refund/page 探测; 404 → FALLBACK seed (12 条)
*
* 状态: payStatus 1=待支付 2=已支付 / 退款处理状态: 审核中/已退款/已拒绝
*/
import { ref, reactive, onMounted, computed } from 'vue'
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 DataTable from '@/components/DataTable/DataTable.vue'
interface RefundRow {
id: string
userId: string
supplierId: string
userEnterpriseId: string
enterpriseId: string
supplierOrderCode: string
payStatus: number
payMoney: number
refundTime: string
requestTime: string
remark: string
delFlag: number
refundStatus: number // 扩展字段 (老 vue 没有, 用于退款流程: 1=审核中 2=已退款 3=已拒绝 4=已撤销)
}
const PAY_LABEL: Record<number, string> = { 1: '待支付', 2: '已支付' }
const REFUND_LABEL: Record<number, string> = { 1: '审核中', 2: '已退款', 3: '已拒绝', 4: '已撤销' }
const REFUND_TYPE: Record<number, string> = { 1: 'primary', 2: 'success', 3: 'danger', 4: 'info' }
const ENDPOINTS = ['/order/refund/page', '/order/refund/list', '/order/refund/search', '/order/refund/queryPage']
const FALLBACK: RefundRow[] = [
{ id:'RF001', userId:'u10086', supplierId:'s20015', userEnterpriseId:'ue001', enterpriseId:'e008', supplierOrderCode:'SO20260701001', payStatus:2, payMoney:3280.00, refundTime:'2026-07-02 14:32:11', requestTime:'2026-07-01 11:25:00', remark:'商品质量问题', delFlag:0, refundStatus:2 },
{ id:'RF002', userId:'u10112', supplierId:'s20016', userEnterpriseId:'ue002', enterpriseId:'e009', supplierOrderCode:'SO20260701002', payStatus:2, payMoney:5680.00, refundTime:'', requestTime:'2026-07-03 09:42:18', remark:'发货延误', delFlag:0, refundStatus:1 },
{ id:'RF003', userId:'u10133', supplierId:'s20017', userEnterpriseId:'ue003', enterpriseId:'e010', supplierOrderCode:'SO20260702001', payStatus:2, payMoney:1880.00, refundTime:'2026-06-29 16:00:00', requestTime:'2026-06-28 13:18:30', remark:'规格不符', delFlag:0, refundStatus:3 },
{ id:'RF004', userId:'u10144', supplierId:'s20018', userEnterpriseId:'ue004', enterpriseId:'e011', supplierOrderCode:'SO20260702002', payStatus:1, payMoney:0, refundTime:'', requestTime:'', remark:'', delFlag:0, refundStatus:4 },
{ id:'RF005', userId:'u10155', supplierId:'s20019', userEnterpriseId:'ue005', enterpriseId:'e012', supplierOrderCode:'SO20260703001', payStatus:2, payMoney:4410.00, refundTime:'2026-07-04 11:08:55', requestTime:'2026-07-03 18:32:00', remark:'协商一致退款', delFlag:0, refundStatus:2 },
{ id:'RF006', userId:'u10166', supplierId:'s20020', userEnterpriseId:'ue006', enterpriseId:'e013', supplierOrderCode:'SO20260703002', payStatus:2, payMoney:13500.00, refundTime:'', requestTime:'2026-07-05 10:14:42', remark:'部分退款申请', delFlag:0, refundStatus:1 },
{ id:'RF007', userId:'u10177', supplierId:'s20021', userEnterpriseId:'ue007', enterpriseId:'e014', supplierOrderCode:'SO20260704001', payStatus:2, payMoney:3200.00, refundTime:'2026-07-06 14:30:00', requestTime:'2026-07-05 16:48:22', remark:'客户主动撤销', delFlag:0, refundStatus:4 },
{ id:'RF008', userId:'u10188', supplierId:'s20022', userEnterpriseId:'ue008', enterpriseId:'e015', supplierOrderCode:'SO20260704002', payStatus:2, payMoney:6920.00, refundTime:'', requestTime:'2026-07-06 09:55:08', remark:'物流丢件', delFlag:0, refundStatus:1 },
{ id:'RF009', userId:'u10199', supplierId:'s20023', userEnterpriseId:'ue009', enterpriseId:'e016', supplierOrderCode:'SO20260705001', payStatus:2, payMoney:9520.00, refundTime:'2026-07-02 17:20:14', requestTime:'2026-07-01 11:42:35', remark:'商品损坏', delFlag:0, refundStatus:2 },
{ id:'RF010', userId:'u10210', supplierId:'s20024', userEnterpriseId:'ue010', enterpriseId:'e017', supplierOrderCode:'SO20260705002', payStatus:2, payMoney:5360.00, refundTime:'2026-06-30 13:30:00', requestTime:'2026-06-28 16:18:50', remark:'客户拒收', delFlag:0, refundStatus:3 },
{ id:'RF011', userId:'u10221', supplierId:'s20025', userEnterpriseId:'ue011', enterpriseId:'e018', supplierOrderCode:'SO20260706001', payStatus:2, payMoney:18500.00, refundTime:'2026-07-05 14:18:50', requestTime:'2026-07-04 09:25:00', remark:'换货协商一致折现', delFlag:0, refundStatus:2 },
{ id:'RF012', userId:'u10232', supplierId:'s20026', userEnterpriseId:'ue012', enterpriseId:'e019', supplierOrderCode:'SO20260706002', payStatus:2, payMoney:2400.00, refundTime:'', requestTime:'2026-07-06 11:02:18', remark:'批次问题', delFlag:0, refundStatus:1 }
]
const STATUS_TABS = [
{ value: '', label: '全部' },
{ value: '1', label: '审核中' },
{ value: '2', label: '已退款' },
{ value: '3', label: '已拒绝' },
{ value: '4', label: '已撤销' }
]
type Col = {
prop?: string; label: string; width?: number; slot?: string; fixed?: 'right' | true; type?: 'status' | 'text' | 'selection'
}
const columns: Col[] = [
{ prop: 'id', label: '退款ID', width: 100, fixed: true },
{ prop: 'supplierOrderCode', label: '供应商订单号', width: 200, slot: 'orderCode' },
{ prop: 'userId', label: '用户ID', width: 90 },
{ prop: 'supplierId', label: '商户ID', width: 90 },
{ prop: 'enterpriseId', label: '商户企业ID', width: 110 },
{ prop: 'userEnterpriseId', label: '用户企业ID', width: 110 },
{ type: 'status', prop: 'payStatus', label: '原支付状态', width: 100 },
{ prop: 'payMoney', label: '实际支付金额', width: 120 },
{ type: 'status', prop: 'refundStatus', label: '退款状态', width: 100 },
{ prop: 'remark', label: '退款原因', width: 200, slot: 'remark' },
{ prop: 'requestTime', label: '申请时间', width: 160, slot: 'requestTime' },
{ prop: 'refundTime', label: '退款完成时间', width: 160, slot: 'refundTime' },
{ prop: 'actions', label: '操作', width: 200, fixed: 'right', slot: 'actions' }
]
const list = ref<RefundRow[]>([])
const total = ref(0)
const loading = ref(false)
const page = ref(1)
const limit = ref(10)
const filterKw = ref('')
const statusTab = ref('')
const dataSource = ref<'api' | 'mock' | 'loading'>('loading')
const apiError = ref<string>('')
async function load() {
loading.value = true
dataSource.value = 'loading'
for (const ep of ENDPOINTS) {
try {
const params: any = { page: page.value, limit: limit.value }
const data: any = await httpGet<any>(ep, params)
const rows: RefundRow[] = (data?.list || []).map((it: any) => ({ ...it, raw: it }))
list.value = rows
total.value = Number(data?.total || rows.length)
dataSource.value = 'api'
apiError.value = ''
loading.value = false
return
} catch {}
}
list.value = [...FALLBACK]
total.value = FALLBACK.length
dataSource.value = 'mock'
apiError.value = '后端 API 暂未实装, 已加载前端示例数据'
loading.value = false
}
const filtered = computed(() => {
const kw = filterKw.value.trim().toLowerCase()
let rows = list.value
if (statusTab.value) rows = rows.filter(r => String(r.refundStatus) === statusTab.value)
if (kw) rows = rows.filter(r =>
String(r.id).toLowerCase().includes(kw) ||
String(r.supplierOrderCode).toLowerCase().includes(kw) ||
String(r.userId).toLowerCase().includes(kw)
)
return rows
})
const searchFields: Array<{ key: string; label: string; type: 'text' }> = [
{ key: 'id', label: '退款ID', type: 'text' },
{ key: 'supplierOrderCode', label: '供应商订单号', type: 'text' },
{ key: 'userId', label: '用户ID', type: 'text' }
]
const filters = reactive<Record<string, any>>({})
function onSearch() {
filterKw.value = String(filters.id || filters.supplierOrderCode || filters.userId || '')
page.value = 1
}
function onStatusTab(v: string) { statusTab.value = v; page.value = 1 }
async function onApprove(row: RefundRow) {
try { await ElMessageBox.confirm(`确认通过退款申请「${row.id}」?`, '提示', { type: 'warning' }) } catch { return }
row.refundStatus = 2
row.refundTime = new Date().toLocaleString('zh-CN')
ElMessage.success('已通过 (前端示例)')
}
async function onReject(row: RefundRow) {
try {
const { value } = await ElMessageBox.prompt('请填写拒绝原因', '拒绝退款', { inputPlaceholder: '原因', confirmButtonText: '拒绝', cancelButtonText: '取消' })
row.refundStatus = 3
row.remark = (value || '已拒绝').trim()
ElMessage.success('已拒绝 (前端示例)')
} catch {}
}
function onView(row: RefundRow) {
ElMessage.info(`打开退款详情 ${row.id}`)
}
function onExport() {
ElMessage.success('已发起导出 (前端示例, 共 ' + filtered.value.length + ' 条)')
}
onMounted(load)
</script>
<template>
<div class="rr-page">
<PageHeader title="退款管理" sub="实物订单退款流程审核 + 跟踪" :data-source="dataSource" :api-error="apiError">
<el-button type="primary" @click="onExport">导出明细</el-button>
</PageHeader>
<el-tabs v-model="statusTab" @tab-change="onStatusTab" class="rr-tabs">
<el-tab-pane v-for="t in STATUS_TABS" :key="t.value" :name="t.value" :label="t.label" />
</el-tabs>
<SearchBar v-model:filters="filters" :fields="searchFields" @search="onSearch" />
<DataTable
:columns="columns"
:data="filtered"
:loading="loading"
:page="page"
:limit="limit"
:total="total"
row-key="id"
@page-change="(p: number) => { page = p; load() }"
@limit-change="(l: number) => { limit = l; page = 1; load() }"
>
<template #orderCode="{ row }">
<el-link type="primary" :underline="false" @click="onView(row)">{{ row.supplierOrderCode }}</el-link>
</template>
<template #remark="{ row }">
<span :class="{ 'rr-muted': !row.remark }">{{ row.remark || '--' }}</span>
</template>
<template #requestTime="{ row }">{{ row.requestTime || '--' }}</template>
<template #refundTime="{ row }">{{ row.refundTime || '--' }}</template>
<template #actions="{ row }">
<template v-if="row.refundStatus === 1">
<el-button type="success" link @click="onApprove(row)">通过</el-button>
<el-button type="danger" link @click="onReject(row)">拒绝</el-button>
</template>
<el-button v-else type="primary" link @click="onView(row)">详情</el-button>
</template>
</DataTable>
</div>
</template>
<style scoped>
.rr-page { padding: 16px; }
.rr-tabs { margin-bottom: 12px; }
.rr-muted { color: var(--ink-muted); }
</style>

View File

@ -6,7 +6,7 @@
* 走 /other/goodunit/list 真 API (返 data: [...] 数组, 共 112 行)
* 字段: id/name/type/supplierId/enterpriseId/createTime
*/
import { onMounted, ref, computed } from 'vue'
import { onMounted, ref, computed, reactive } from 'vue'
import { httpGet } from '@/utils/request'
import DataTable from '@/components/DataTable/DataTable.vue'
import PageHeader from '@/components/PageHeader/PageHeader.vue'
@ -26,6 +26,7 @@ const list = ref<GoodUnit[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const filterKw = ref('')
const filters = reactive({ kw: filterKw })
const TYPE_MAP: Record<number, string> = {
1: '系统',
@ -55,7 +56,7 @@ async function load() {
}
const filtered = computed(() => {
const kw = filterKw.value.trim().toLowerCase()
const kw = (filters.kw || "").trim().toLowerCase()
if (!kw) return list.value
return list.value.filter(it => it.name.toLowerCase().includes(kw))
})
@ -73,7 +74,7 @@ onMounted(load)
<PageHeader title="商品单位" :sub="`商品计量单位管理 (共 ${list.length} 个, 筛选后 ${filtered.length} 个)`" />
<SearchBar
v-model:filters="{ kw: filterKw }"
v-model:filters="filters"
:fields="[
{ key: 'kw', label: '名称', type: 'text', placeholder: '按名称筛选' }
]"

View File

@ -225,6 +225,10 @@ const routes: RouteRecordRaw[] = [
{ path: '/order/ordergood/after', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '商品售后订单' } },
{ path: '/order/orderserviceafter', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '服务售后订单' } },
{ path: '/order/base-price-analysis', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '基础价格分析表' } },
// W15 新增 - 订单售后 + 退款 + 付款回调 (前端 max-effort mock, 老 vue 同字段)
{ path: '/order/orderpay', component: () => import('@/modules/order/payment/PaymentCallbackListView.vue'), meta: { title: '订单支付回调' } },
{ path: '/order/refund', component: () => import('@/modules/order/refund/RefundProcessListView.vue'), meta: { title: '订单退款管理' } },
{ path: '/order/aftersale', component: () => import('@/modules/order/aftersale/AftersaleListView.vue'), meta: { title: '售后订单' } },
// other 7 个
{ path: '/other/brand', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-brand' }), meta: { title: '品牌管理' } },
{ path: '/other/category/good', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-category-good' }), meta: { title: '平台分类' } },