Files
pc-10584/app/pages/console/orders.vue
赵忠林 775841eed3 feat(core): 初始化项目基础架构和CMS功能模块
- 添加Docker相关配置文件(.dockerignore, .env.example, .gitignore)
- 实现服务端API代理功能,支持文件、模块和服务器API转发
- 创建文章详情页、栏目文章列表页和单页内容展示页面
- 集成Ant Design Vue组件库并实现SSR样式提取功能
- 定义API响应数据结构类型和应用布局组件
- 开发开发者应用中心和文章管理页面
- 实现CMS导航菜单获取和多租户切换功能
2026-01-27 00:14:08 +08:00

395 lines
12 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.

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