feat(core): 初始化项目基础架构和CMS功能模块
- 添加Docker相关配置文件(.dockerignore, .env.example, .gitignore) - 实现服务端API代理功能,支持文件、模块和服务器API转发 - 创建文章详情页、栏目文章列表页和单页内容展示页面 - 集成Ant Design Vue组件库并实现SSR样式提取功能 - 定义API响应数据结构类型和应用布局组件 - 开发开发者应用中心和文章管理页面 - 实现CMS导航菜单获取和多租户切换功能
This commit is contained in:
394
app/pages/console/orders.vue
Normal file
394
app/pages/console/orders.vue
Normal file
@@ -0,0 +1,394 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user