221 lines
11 KiB
Vue
221 lines
11 KiB
Vue
<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/orderAfterSale/shopAfterSalePage', '/order/orderAfterSale/supplierPageList']
|
||
|
||
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>
|