refactor(plan):移除套餐相关的所有修改

This commit is contained in:
qian committed 2026-05-25 22:26:32 +08:00
1 parent 8480d06d21
commit 5d48dbcdc2
73 files changed
+958 -2306

No files matched your search

@@ -36,6 +36,15 @@ const billColumns: TableColumnData[] = [
title: '总费用', dataIndex: 'total_cost', width: 100,
render({ record }) { return `$${(record.total_cost || 0).toFixed(6)}` },
},
{
title: '扣费来源', dataIndex: 'deduction_source', width: 120,
render({ record }) {
const tags = []
if (record.plan_deduction > 0) tags.push(h(Tag, { size: 'small', type: 'info' }, () => '套餐抵扣'))
if (record.wallet_deduction > 0) tags.push(h(Tag, { size: 'small' }, () => '钱包'))
return tags.length > 0 ? h('div', { class: 'flex gap-1' }, tags) : '-'
},
},
{
title: '状态', dataIndex: 'status', width: 80,
render({ record }) {
+4 -30
View File
@@ -30,7 +30,7 @@ const statusLabel: Record<string, string> = {
cancelled: '已取消', refunding: '退款中', refunded: '已退款', refund_failed: '退款失败',
}
const orderTypeLabel: Record<string, string> = {
new_plan: '套餐购买', renew: '续费', upgrade: '升级', downgrade: '降级', recharge: '充值',
new_plan: '新购', renew: '续费', upgrade: '升级', downgrade: '降级', recharge: '充值',
}
const columns: TableColumnData[] = [
@@ -85,29 +85,12 @@ const refundOrderId = ref<number | null>(null)
const refundAmount = ref(0)
const refundForm = reactive({ reason: '' })
const refundLoading = ref(false)
const refundPreview = ref<any>(null)
const refundPreviewLoading = ref(false)
async function openRefundModal(row: any) {
function openRefundModal(row: any) {
refundOrderId.value = row.id
refundForm.reason = ''
refundAmount.value = Number(row.final_amount)
refundPreview.value = null
refundForm.reason = ''
showRefundModal.value = true
// Fetch refund preview
refundPreviewLoading.value = true
try {
const res = await request.get(`/admin/orders/${row.id}/refund-preview`)
refundPreview.value = res.data?.data
if (refundPreview.value?.can_refund && refundPreview.value?.refund_amount >= 0) {
refundAmount.value = refundPreview.value.refund_amount
}
} catch {
// Fallback to order final_amount
} finally {
refundPreviewLoading.value = false
}
}
async function handleRefund(done: () => void) {
@@ -165,16 +148,7 @@ const { exporting, exportFile } = useExport({
<AModal v-model:visible="showRefundModal" title="发起退款" :width="450" :mask-closable="false" :on-before-ok="handleRefund" :ok-loading="refundLoading">
<AForm :model="refundForm" :auto-label-width="true" layout="vertical">
<AFormItem label="退款金额">
<div v-if="refundPreviewLoading" class="text-gray-400 text-sm">计算中...</div>
<template v-else-if="refundPreview && !refundPreview.can_refund">
<span class="text-gray-400">{{ refundPreview.message || '不可退款' }}</span>
</template>
<template v-else>
<span class="text-red-500 font-medium">¥{{ refundAmount.toFixed(2) }}</span>
<span v-if="refundPreview?.order_type === 'new_plan'" class="text-xs text-gray-400 ml-2">
(套餐按比例退款)
</span>
</template>
<span class="text-red-500 font-medium">¥{{ refundAmount.toFixed(2) }}</span>
</AFormItem>
<AFormItem label="退款原因" required>
<AInput v-model="refundForm.reason" type="textarea" :auto-size="{ minRows: 3, maxRows: 5 }" placeholder="请填写退款原因" />
+20 -93
View File
@@ -10,7 +10,6 @@ import { useExport } from '@/composables/useExport'
const loading = ref(false)
const plans = ref<any[]>([])
const allModels = ref<string[]>([])
const pagination = reactive({ current: 1, pageSize: 20, total: 0, showPageSize: true, pageSizeOptions: [10, 20, 50] })
const statusFilter = ref<string | undefined>(undefined)
const statusOptions = [
@@ -20,30 +19,20 @@ const statusOptions = [
]
const columns: TableColumnData[] = [
{ title: 'ID', dataIndex: 'id', width: 60 },
{ title: 'ID', dataIndex: 'id', width: 70 },
{ title: '名称', dataIndex: 'name', width: 120 },
{ title: '标识', dataIndex: 'identifier', width: 90 },
{ title: '价格(CNY)', dataIndex: 'price', width: 100, render({ record }) { return `¥${Number(record.price).toFixed(2)}` } },
{ title: '额度(USD)', dataIndex: 'credit_amount', width: 100, render({ record }) { return `$${Number(record.credit_amount).toFixed(2)}` } },
{ title: '赠送(USD)', dataIndex: 'bonus_amount', width: 90, render({ record }) { return record.bonus_amount > 0 ? `$${Number(record.bonus_amount).toFixed(2)}` : '-' } },
{ title: '有效期(天)', dataIndex: 'validity_days', width: 90 },
{ title: '可用模型', dataIndex: 'allowed_models', width: 120, render({ record }) {
const models = record.allowed_models
if (!models || models.length === 0) return '全部'
if (models.length <= 2) return models.join(', ')
return `${models.slice(0, 2).join(', ')}... (共${models.length}个)`
} },
{ title: '库存', dataIndex: 'stock', width: 70, render({ record }) { return record.stock === null || record.stock === 0 ? '不限' : record.stock } },
{ title: '已购', dataIndex: 'total_purchased', width: 60 },
{ title: '标识', dataIndex: 'identifier', width: 100 },
{ title: '月价', dataIndex: 'monthly_price', width: 100, render({ record }) { return `¥${Number(record.monthly_price).toFixed(2)}` } },
{ title: '年价', dataIndex: 'yearly_price', width: 100, render({ record }) { return `¥${Number(record.yearly_price).toFixed(2)}` } },
{
title: '状态', dataIndex: 'status', width: 70,
title: '状态', dataIndex: 'status', width: 80,
render({ record }) { return h(Tag, { color: record.status === 'active' ? 'green' : undefined, size: 'small' }, () => record.status === 'active' ? '上架' : '下架') },
},
{
title: '推荐', dataIndex: 'is_recommended', width: 60,
title: '推荐', dataIndex: 'is_recommended', width: 70,
render({ record }) { return h(Tag, { color: record.is_recommended ? 'orangered' : undefined, size: 'small' }, () => record.is_recommended ? '推荐' : '-') },
},
{ title: '排序', dataIndex: 'sort_order', width: 60 },
{ title: '排序', dataIndex: 'sort_order', width: 70 },
{
title: '操作', dataIndex: 'actions', width: 200, fixed: 'right',
render({ record }) {
@@ -68,57 +57,22 @@ async function fetchPlans() {
} catch { /* interceptor handles error toast */ } finally { loading.value = false }
}
async function fetchAllModels() {
try {
const names: string[] = []
let page = 1
while (true) {
const res = await request.get('/admin/models', { params: { page, page_size: 100 } })
const list = res.data?.data?.list || []
for (const m of list) names.push(m.model_id)
const total = res.data?.data?.total || 0
if (page * 100 >= total) break
page++
}
allModels.value = names
} catch { /* ignore */ }
}
const showModal = ref(false)
const editingId = ref<number | null>(null)
const formRef = ref<FormInstance | null>(null)
const formLoading = ref(false)
const formData = reactive({
name: '', identifier: '', description: '',
price: 0, credit_amount: 0, bonus_amount: 0, validity_days: 30,
allowed_models: [] as string[], purchase_limit: 0, purchase_limit_period: 'lifetime',
stock: null as number | null, sort_order: 0, is_recommended: false,
name: '', identifier: '', description: '', monthly_price: 0, yearly_price: 0,
sort_order: 0, is_recommended: false,
})
function resetForm() {
Object.assign(formData, {
name: '', identifier: '', description: '',
price: 0, credit_amount: 0, bonus_amount: 0, validity_days: 30,
allowed_models: [], purchase_limit: 0, purchase_limit_period: 'lifetime',
stock: null, sort_order: 0, is_recommended: false,
})
}
function openCreateModal() {
editingId.value = null
resetForm()
showModal.value = true
}
function openEditModal(row: any) {
editingId.value = row.id
Object.assign(formData, {
name: row.name, identifier: row.identifier, description: row.description || '',
price: row.price, credit_amount: row.credit_amount, bonus_amount: row.bonus_amount || 0,
validity_days: row.validity_days, allowed_models: row.allowed_models || [],
purchase_limit: row.purchase_limit || 0, purchase_limit_period: row.purchase_limit_period || 'lifetime',
stock: row.stock, sort_order: row.sort_order || 0, is_recommended: row.is_recommended || false,
})
showModal.value = true
}
@@ -126,7 +80,7 @@ async function handleSubmit() {
formLoading.value = true
try {
if (editingId.value) {
await request.put(`/admin/plans/${editingId.value}`, { update: { ...formData } })
await request.put(`/admin/plans/${editingId.value}`, { ...formData, status: undefined, created_at: undefined, updated_at: undefined, id: undefined, version: undefined })
Message.success('更新成功')
} else {
await request.post('/admin/plans', formData)
@@ -154,10 +108,7 @@ function archivePlan(row: any) {
})
}
onMounted(() => {
fetchPlans()
fetchAllModels()
})
onMounted(fetchPlans)
const { exporting, exportFile } = useExport({
url: '/admin/plans/export',
@@ -169,7 +120,7 @@ const { exporting, exportFile } = useExport({
<template>
<div>
<PageHeader title="套餐管理" description="管理平台套餐(额度包)定义和定价">
<PageHeader title="套餐管理" description="管理平台套餐定义和定价">
<template #actions>
<AButton type="primary" @click="openCreateModal">创建套餐</AButton>
<ADropdown trigger="hover">
@@ -189,7 +140,7 @@ const { exporting, exportFile } = useExport({
<ASelect v-model="statusFilter" :options="statusOptions" style="width: 120px" allow-clear @change="() => { pagination.current = 1; fetchPlans() }" />
</div>
</template>
<ATable :columns="columns" :data="plans" :loading="loading" row-key="id" :scroll="{ x: 1400 }" :pagination="false" />
<ATable :columns="columns" :data="plans" :loading="loading" row-key="id" :scroll="{ x: 1200 }" :pagination="false" />
<div class="mt-4 flex justify-end">
<APagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize" :total="pagination.total" :page-size-options="pagination.pageSizeOptions" show-page-size @change="fetchPlans" @page-size-change="(s: number) => { pagination.pageSize = s; pagination.current = 1; fetchPlans() }" />
</div>
@@ -197,38 +148,14 @@ const { exporting, exportFile } = useExport({
<ADrawer v-model:visible="showModal" :title="editingId ? '编辑套餐' : '创建套餐'" :width="520" :mask-closable="false" :footer="true" unmount-on-close>
<AForm ref="formRef" :model="formData" :auto-label-width="true" layout="vertical">
<ADivider>基本信息</ADivider>
<AFormItem label="名称" required><AInput v-model="formData.name" placeholder="如:基础套餐" /></AFormItem>
<AFormItem label="标识" required><AInput v-model="formData.identifier" placeholder="如:basic" :disabled="!!editingId" /></AFormItem>
<AFormItem label="名称" required><AInput v-model="formData.name" placeholder="如:专业版" /></AFormItem>
<AFormItem label="标识" required><AInput v-model="formData.identifier" placeholder="如:pro" :disabled="!!editingId" /></AFormItem>
<AFormItem label="描述"><AInput v-model="formData.description" type="textarea" :auto-size="{ minRows: 2, maxRows: 4 }" placeholder="面向用户的营销文案" /></AFormItem>
<ADivider>定价与额度</ADivider>
<AFormItem label="价格(CNY)" required><AInputNumber v-model="formData.price" :min="0.01" :precision="2" class="w-full" placeholder="购买价格" /></AFormItem>
<AFormItem label="额度(USD)" required><AInputNumber v-model="formData.credit_amount" :min="0.01" :precision="2" class="w-full" placeholder="包含的 USD 额度" /></AFormItem>
<AFormItem label="赠送额度(USD)"><AInputNumber v-model="formData.bonus_amount" :min="0" :precision="2" class="w-full" placeholder="额外赠送额度" /></AFormItem>
<AFormItem label="有效天数" required><AInputNumber v-model="formData.validity_days" :min="1" class="w-full" placeholder="从激活时起算" /></AFormItem>
<ADivider>使用限制</ADivider>
<AFormItem label="可用模型">
<ASelect v-model="formData.allowed_models" :options="allModels.map(m => ({ label: m, value: m }))" multiple allow-clear filterable placeholder="留空 = 全部模型" />
</AFormItem>
<AGrid :cols="2" :col-gap="12">
<AFormItem label="限购数量"><AInputNumber v-model="formData.purchase_limit" :min="0" class="w-full" placeholder="0 = 不限购" /></AFormItem>
<AFormItem label="限购周期">
<ASelect v-model="formData.purchase_limit_period">
<AOption value="lifetime">终身</AOption>
<AOption value="monthly">每月</AOption>
<AOption value="yearly">每年</AOption>
</ASelect>
</AFormItem>
</AGrid>
<AFormItem label="库存"><AInputNumber v-model="formData.stock" :min="0" class="w-full" placeholder="留空 = 不限库存" /></AFormItem>
<ADivider>其他设置</ADivider>
<AGrid :cols="2" :col-gap="12">
<AFormItem label="排序权重"><AInputNumber v-model="formData.sort_order" :min="0" class="w-full" placeholder="越小越靠前" /></AFormItem>
<AFormItem label="推荐"><ASwitch v-model="formData.is_recommended" /></AFormItem>
</AGrid>
<AFormItem label="月价格(CNY)"><AInputNumber v-model="formData.monthly_price" :min="0" :precision="2" class="w-full" /></AFormItem>
<AFormItem label="年价格(CNY)"><AInputNumber v-model="formData.yearly_price" :min="0" :precision="2" class="w-full" /></AFormItem>
<AFormItem label="月Token配额"><AInputNumber v-model="formData.monthly_quota_tokens" :min="0" class="w-full" placeholder="0 = 不限制" /></AFormItem>
<AFormItem label="排序权重"><AInputNumber v-model="formData.sort_order" :min="0" class="w-full" placeholder="越小越靠前" /></AFormItem>
<AFormItem label="推荐"><ASwitch v-model="formData.is_recommended" /></AFormItem>
</AForm>
<template #footer>
<Space>