refactor(plan):移除套餐相关的所有修改
This commit is contained in:
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 }) {
|
||||
|
||||
@@ -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="请填写退款原因" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user