- 添加Docker相关配置文件(.dockerignore, .env.example, .gitignore) - 实现服务端API代理功能,支持文件、模块和服务器API转发 - 创建文章详情页、栏目文章列表页和单页内容展示页面 - 集成Ant Design Vue组件库并实现SSR样式提取功能 - 定义API响应数据结构类型和应用布局组件 - 开发开发者应用中心和文章管理页面 - 实现CMS导航菜单获取和多租户切换功能
395 lines
12 KiB
Vue
395 lines
12 KiB
Vue
<template>
|
||
<div class="space-y-4">
|
||
<a-page-header title="订单管理" sub-title="购买、续费与支付记录">
|
||
<template #extra>
|
||
<a-space>
|
||
<a-input
|
||
v-model:value="keywords"
|
||
allow-clear
|
||
placeholder="搜索订单号/产品"
|
||
class="w-64"
|
||
@press-enter="doSearch"
|
||
/>
|
||
<a-button :loading="loading" @click="reload">刷新</a-button>
|
||
</a-space>
|
||
</template>
|
||
</a-page-header>
|
||
|
||
<a-card :bordered="false" class="card">
|
||
<a-space class="mb-4">
|
||
<a-segmented
|
||
:value="payStatusSegment"
|
||
:options="payStatusOptions"
|
||
@update:value="onPayStatusChange"
|
||
/>
|
||
<a-select
|
||
v-model:value="orderStatus"
|
||
allow-clear
|
||
placeholder="订单状态"
|
||
:options="orderStatusOptions"
|
||
@change="reload"
|
||
style="min-width: 160px"
|
||
/>
|
||
</a-space>
|
||
|
||
<a-alert v-if="error" class="mb-4" show-icon type="error" :message="error" />
|
||
|
||
<a-table
|
||
:data-source="list"
|
||
:loading="loading"
|
||
:pagination="false"
|
||
size="middle"
|
||
:row-key="(r: any) => r.orderId ?? r.orderNo"
|
||
>
|
||
<a-table-column title="订单号" key="orderNo" width="220">
|
||
<template #default="{ record }">
|
||
<a-typography-text :copyable="{ text: record.orderNo || '' }">
|
||
{{ record.orderNo || '-' }}
|
||
</a-typography-text>
|
||
</template>
|
||
</a-table-column>
|
||
|
||
<a-table-column title="产品" key="product" width="200">
|
||
<template #default="{ record }">
|
||
<div class="min-w-0">
|
||
<div class="truncate">{{ resolveProductName(record) }}</div>
|
||
<div class="text-xs text-gray-500 truncate" v-if="resolveProductSub(record)">
|
||
{{ resolveProductSub(record) }}
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</a-table-column>
|
||
|
||
<a-table-column title="金额" key="amount" width="140">
|
||
<template #default="{ record }">
|
||
<span>{{ formatMoney(record.payPrice || record.totalPrice) }}</span>
|
||
</template>
|
||
</a-table-column>
|
||
|
||
<a-table-column title="支付" key="payStatus" width="110">
|
||
<template #default="{ record }">
|
||
<a-tag v-if="Number(record.payStatus) === 1" color="green">已支付</a-tag>
|
||
<a-tag v-else-if="Number(record.payStatus) === 0" color="default">未支付</a-tag>
|
||
<a-tag v-else color="default">-</a-tag>
|
||
</template>
|
||
</a-table-column>
|
||
|
||
<a-table-column title="状态" key="orderStatus" width="160">
|
||
<template #default="{ record }">
|
||
<a-tag :color="resolveOrderStatusColor(record.orderStatus)">{{ resolveOrderStatusText(record.orderStatus) }}</a-tag>
|
||
</template>
|
||
</a-table-column>
|
||
|
||
<a-table-column title="创建时间" data-index="createTime" width="180">
|
||
<template #default="{ record }">
|
||
<span>{{ record.createTime || '-' }}</span>
|
||
</template>
|
||
</a-table-column>
|
||
|
||
<a-table-column title="到期时间" data-index="expirationTime" width="180">
|
||
<template #default="{ record }">
|
||
<span>{{ record.expirationTime || '-' }}</span>
|
||
</template>
|
||
</a-table-column>
|
||
|
||
<a-table-column title="操作" key="actions" width="120" fixed="right">
|
||
<template #default="{ record }">
|
||
<a-button size="small" @click="openDetail(record)">查看</a-button>
|
||
</template>
|
||
</a-table-column>
|
||
</a-table>
|
||
|
||
<div class="mt-4 flex items-center justify-end">
|
||
<a-pagination
|
||
:current="page"
|
||
:page-size="limit"
|
||
:total="total"
|
||
show-size-changer
|
||
:page-size-options="['10', '20', '50', '100']"
|
||
@change="onPageChange"
|
||
@show-size-change="onPageSizeChange"
|
||
/>
|
||
</div>
|
||
</a-card>
|
||
|
||
<a-modal v-model:open="detailOpen" title="订单详情" :width="720" ok-text="关闭" cancel-text="取消" :footer="null">
|
||
<a-descriptions :column="2" size="small" bordered>
|
||
<a-descriptions-item label="订单号">
|
||
<a-typography-text :copyable="{ text: selected?.orderNo || '' }">
|
||
{{ selected?.orderNo || '-' }}
|
||
</a-typography-text>
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="订单ID">{{ selected?.orderId ?? '-' }}</a-descriptions-item>
|
||
<a-descriptions-item label="金额">{{ formatMoney(selected?.payPrice || selected?.totalPrice) }}</a-descriptions-item>
|
||
<a-descriptions-item label="支付方式">{{ resolvePayTypeText(selected?.payType) }}</a-descriptions-item>
|
||
<a-descriptions-item label="支付状态">
|
||
{{ Number(selected?.payStatus) === 1 ? '已支付' : Number(selected?.payStatus) === 0 ? '未支付' : '-' }}
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="订单状态">{{ resolveOrderStatusText(selected?.orderStatus) }}</a-descriptions-item>
|
||
<a-descriptions-item label="创建时间">{{ selected?.createTime || '-' }}</a-descriptions-item>
|
||
<a-descriptions-item label="支付时间">{{ selected?.payTime || '-' }}</a-descriptions-item>
|
||
<a-descriptions-item label="到期时间">{{ selected?.expirationTime || '-' }}</a-descriptions-item>
|
||
<a-descriptions-item label="产品">
|
||
{{ resolveProductName(selected) }}
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="备注">
|
||
<span class="break-all">{{ pickFirstRemark(selected) || '-' }}</span>
|
||
</a-descriptions-item>
|
||
</a-descriptions>
|
||
|
||
<a-divider />
|
||
|
||
<div class="text-sm text-gray-600 mb-2">解析到的扩展字段(buyerRemarks/merchantRemarks/comments)</div>
|
||
<a-typography-paragraph :copyable="{ text: prettyExtra(selected) }">
|
||
<pre class="m-0 whitespace-pre-wrap break-words">{{ prettyExtra(selected) }}</pre>
|
||
</a-typography-paragraph>
|
||
</a-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { message } from 'ant-design-vue'
|
||
import { getUserInfo } from '@/api/layout'
|
||
import { pageShopOrder } from '@/api/shop/shopOrder'
|
||
import type { ShopOrder } from '@/api/shop/shopOrder/model'
|
||
|
||
definePageMeta({ layout: 'console' })
|
||
|
||
const loading = ref(false)
|
||
const error = ref('')
|
||
const list = ref<ShopOrder[]>([])
|
||
const page = ref(1)
|
||
const limit = ref(10)
|
||
const total = ref(0)
|
||
|
||
const keywords = ref('')
|
||
const payStatus = ref<number | null>(null)
|
||
const orderStatus = ref<number | null>(null)
|
||
|
||
const currentUserId = ref<number | null>(null)
|
||
|
||
const payStatusOptions = [
|
||
{ label: '全部', value: 'all' },
|
||
{ label: '已支付', value: 1 },
|
||
{ label: '未支付', value: 0 }
|
||
]
|
||
|
||
const payStatusSegment = computed(() => (payStatus.value === null ? 'all' : payStatus.value))
|
||
|
||
const orderStatusOptions = [
|
||
{ label: '未使用', value: 0 },
|
||
{ label: '已完成', value: 1 },
|
||
{ label: '已取消', value: 2 },
|
||
{ label: '取消中', value: 3 },
|
||
{ label: '退款申请中', value: 4 },
|
||
{ label: '退款被拒绝', value: 5 },
|
||
{ label: '退款成功', value: 6 },
|
||
{ label: '客户申请退款', value: 7 }
|
||
]
|
||
|
||
const detailOpen = ref(false)
|
||
const selected = ref<ShopOrder | null>(null)
|
||
|
||
function safeJsonParse(value: string): unknown {
|
||
try {
|
||
return JSON.parse(value)
|
||
} catch {
|
||
return undefined
|
||
}
|
||
}
|
||
|
||
function pickFirstRemark(order?: ShopOrder | null) {
|
||
if (!order) return ''
|
||
const record = order as unknown as Record<string, unknown>
|
||
const keys = ['buyerRemarks', 'merchantRemarks', 'comments']
|
||
for (const key of keys) {
|
||
const v = record[key]
|
||
if (typeof v === 'string' && v.trim()) return v.trim()
|
||
}
|
||
return ''
|
||
}
|
||
|
||
function parseExtra(order?: ShopOrder | null): Record<string, unknown> | null {
|
||
const raw = pickFirstRemark(order)
|
||
if (!raw) return null
|
||
const parsed = safeJsonParse(raw)
|
||
if (!parsed || typeof parsed !== 'object') return null
|
||
return parsed as Record<string, unknown>
|
||
}
|
||
|
||
function prettyExtra(order?: ShopOrder | null) {
|
||
const extra = parseExtra(order)
|
||
if (!extra) return '-'
|
||
try {
|
||
return JSON.stringify(extra, null, 2)
|
||
} catch {
|
||
return '-'
|
||
}
|
||
}
|
||
|
||
const productCatalog: Record<string, { name: string }> = {
|
||
website: { name: '企业官网' },
|
||
shop: { name: '电商系统' },
|
||
mp: { name: '小程序/公众号' }
|
||
}
|
||
|
||
function resolveProductCode(order?: ShopOrder | null) {
|
||
const extra = parseExtra(order)
|
||
const code = typeof extra?.product === 'string' ? extra.product.trim() : ''
|
||
return code
|
||
}
|
||
|
||
function resolveProductSub(order?: ShopOrder | null) {
|
||
const extra = parseExtra(order)
|
||
const months = extra?.months
|
||
const tenantName = extra?.tenantName
|
||
const domain = extra?.domain
|
||
|
||
const parts: string[] = []
|
||
if (typeof months === 'number' || typeof months === 'string') {
|
||
const m = String(months).trim()
|
||
if (m) parts.push(`${m}个月`)
|
||
}
|
||
if (typeof tenantName === 'string' && tenantName.trim()) parts.push(tenantName.trim())
|
||
if (typeof domain === 'string' && domain.trim()) parts.push(domain.trim())
|
||
return parts.join(' · ')
|
||
}
|
||
|
||
function resolveProductName(order?: ShopOrder | null) {
|
||
const code = resolveProductCode(order)
|
||
if (code && productCatalog[code]) return productCatalog[code].name
|
||
if (code) return code
|
||
return '-'
|
||
}
|
||
|
||
function formatMoney(value?: string) {
|
||
const v = typeof value === 'string' ? value.trim() : ''
|
||
if (!v) return '-'
|
||
const n = Number(v)
|
||
if (!Number.isFinite(n)) return `¥${v}`
|
||
return `¥${n.toFixed(2)}`
|
||
}
|
||
|
||
function resolvePayTypeText(payType?: number) {
|
||
const v = Number(payType)
|
||
if (!Number.isFinite(v)) return '-'
|
||
const map: Record<number, string> = {
|
||
0: '余额',
|
||
1: '微信',
|
||
102: '微信 Native',
|
||
2: '会员卡',
|
||
3: '支付宝',
|
||
4: '现金',
|
||
5: 'POS',
|
||
12: '免费'
|
||
}
|
||
return map[v] || `方式${v}`
|
||
}
|
||
|
||
function resolveOrderStatusText(orderStatus?: number) {
|
||
const v = Number(orderStatus)
|
||
if (!Number.isFinite(v)) return '-'
|
||
const map: Record<number, string> = {
|
||
0: '未使用',
|
||
1: '已完成',
|
||
2: '已取消',
|
||
3: '取消中',
|
||
4: '退款申请中',
|
||
5: '退款被拒绝',
|
||
6: '退款成功',
|
||
7: '客户申请退款'
|
||
}
|
||
return map[v] || `状态${v}`
|
||
}
|
||
|
||
function resolveOrderStatusColor(orderStatus?: number) {
|
||
const v = Number(orderStatus)
|
||
if (v === 1) return 'green'
|
||
if (v === 2) return 'default'
|
||
if (v === 6) return 'default'
|
||
if (v === 4 || v === 3 || v === 7) return 'orange'
|
||
if (v === 5) return 'red'
|
||
return 'blue'
|
||
}
|
||
|
||
async function ensureUser() {
|
||
if (currentUserId.value) return
|
||
const user = await getUserInfo()
|
||
currentUserId.value = user.userId ?? null
|
||
}
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
error.value = ''
|
||
try {
|
||
await ensureUser()
|
||
const userId = currentUserId.value
|
||
if (!userId) {
|
||
throw new Error('缺少用户信息,无法查询当前用户订单')
|
||
}
|
||
|
||
const data = await pageShopOrder({
|
||
page: page.value,
|
||
limit: limit.value,
|
||
userId,
|
||
keywords: keywords.value?.trim() || undefined,
|
||
payStatus: payStatus.value === null ? undefined : payStatus.value,
|
||
orderStatus: orderStatus.value === null ? undefined : orderStatus.value
|
||
})
|
||
|
||
list.value = data?.list || []
|
||
total.value = data?.count || 0
|
||
} catch (e: unknown) {
|
||
console.error(e)
|
||
list.value = []
|
||
total.value = 0
|
||
error.value = e instanceof Error ? e.message : '加载订单失败'
|
||
message.error(error.value)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function reload() {
|
||
await load()
|
||
}
|
||
|
||
function doSearch() {
|
||
page.value = 1
|
||
load()
|
||
}
|
||
|
||
function onPayStatusChange(value: string | number) {
|
||
payStatus.value = value === 'all' ? null : Number(value)
|
||
page.value = 1
|
||
load()
|
||
}
|
||
|
||
function onPageChange(p: number) {
|
||
page.value = p
|
||
load()
|
||
}
|
||
|
||
function onPageSizeChange(_current: number, size: number) {
|
||
limit.value = size
|
||
page.value = 1
|
||
load()
|
||
}
|
||
|
||
function openDetail(order: ShopOrder) {
|
||
selected.value = order
|
||
detailOpen.value = true
|
||
}
|
||
|
||
onMounted(() => {
|
||
load()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.card {
|
||
border-radius: 12px;
|
||
}
|
||
</style>
|