admin_new/src/modules/order/aftersale/AftersaleListView.vue

221 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>