fix(admin): restore Vite admin source files
This commit is contained in:
1 parent
8fe3a89c36
commit
ac5c0b2dd7
114 files changed
+17272
No files matched your search
@@ -0,0 +1,530 @@
|
||||
<template>
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader title="充值卡管理" desc="按状态和时间筛选充值卡,支持单张创建、批量生成、详情查看和当前筛选导出。" />
|
||||
|
||||
<div class="aksrt-charge-card-page__stats">
|
||||
<n-card v-for="card in statCards" :key="card.key" size="small">
|
||||
<n-statistic :label="card.label" :value="stats[card.key] || 0" />
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<n-card size="small" title="操作台">
|
||||
<n-space justify="space-between" wrap>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="createVisible = true">创建充值卡</n-button>
|
||||
<n-button @click="batchVisible = true">批量生成</n-button>
|
||||
</n-space>
|
||||
<n-button quaternary @click="handleExport">导出当前筛选</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card class="aksrt-charge-card-page__card" size="small" title="充值卡台账">
|
||||
<n-space vertical :size="12">
|
||||
<n-grid cols="1 s:2 m:4" :x-gap="12" :y-gap="12" responsive="screen">
|
||||
<n-grid-item>
|
||||
<n-input v-model:value="filters.keyword" clearable placeholder="搜索卡号、备注、使用人" />
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-select v-model:value="filters.status" :options="statusOptions" placeholder="状态" />
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-date-picker v-model:value="filters.dateRange" type="daterange" clearable style="width: 100%;" />
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleSearch">筛选</n-button>
|
||||
<n-button @click="resetFilters">重置</n-button>
|
||||
</n-space>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="rows"
|
||||
:loading="loading"
|
||||
:bordered="false"
|
||||
:pagination="pagination"
|
||||
:row-key="(row) => row.id"
|
||||
/>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-modal v-model:show="createVisible" preset="card" title="创建充值卡" style="max-width: 520px;">
|
||||
<n-form :model="createForm" label-placement="top">
|
||||
<n-form-item label="卡类型">
|
||||
<n-select v-model:value="createForm.type" :options="typeOptions" />
|
||||
</n-form-item>
|
||||
<n-form-item label="卡密">
|
||||
<n-input v-model:value="createForm.code" placeholder="手动填写唯一卡密" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="createForm.type === 'balance'" label="面额">
|
||||
<n-input-number v-model:value="createForm.amount" :min="0.01" :precision="2" style="width: 100%;" />
|
||||
</n-form-item>
|
||||
<n-grid v-if="createForm.type === 'vip'" cols="2" :x-gap="12">
|
||||
<n-grid-item>
|
||||
<n-form-item label="VIP 等级">
|
||||
<n-select v-model:value="createForm.level" :options="levelOptions" />
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-form-item label="天数(0=永久)">
|
||||
<n-input-number v-model:value="createForm.days" :min="0" :max="3650" style="width: 100%;" />
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
<n-form-item v-if="createForm.type === 'credits'" label="积分">
|
||||
<n-input-number v-model:value="createForm.points" :min="1" style="width: 100%;" />
|
||||
</n-form-item>
|
||||
<n-form-item label="过期时间">
|
||||
<n-date-picker v-model:value="createForm.expiresAt" type="datetime" clearable style="width: 100%;" />
|
||||
</n-form-item>
|
||||
<n-form-item label="备注">
|
||||
<n-input v-model:value="createForm.note" type="textarea" :rows="3" />
|
||||
</n-form-item>
|
||||
<n-space justify="end">
|
||||
<n-button @click="createVisible = false">取消</n-button>
|
||||
<n-button type="primary" :loading="submittingCreate" @click="handleCreate">创建</n-button>
|
||||
</n-space>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="batchVisible" preset="card" title="批量生成充值卡" style="max-width: 520px;">
|
||||
<n-form :model="batchForm" label-placement="top">
|
||||
<n-form-item label="卡类型">
|
||||
<n-select v-model:value="batchForm.type" :options="typeOptions" />
|
||||
</n-form-item>
|
||||
<n-grid cols="2" :x-gap="12">
|
||||
<n-grid-item>
|
||||
<n-form-item label="数量">
|
||||
<n-input-number v-model:value="batchForm.count" :min="1" :max="100" style="width: 100%;" />
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
<n-grid-item v-if="batchForm.type === 'balance'">
|
||||
<n-form-item label="面额">
|
||||
<n-input-number v-model:value="batchForm.amount" :min="0.01" :precision="2" style="width: 100%;" />
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
<n-grid-item v-if="batchForm.type === 'credits'">
|
||||
<n-form-item label="积分">
|
||||
<n-input-number v-model:value="batchForm.points" :min="1" style="width: 100%;" />
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
<n-grid v-if="batchForm.type === 'vip'" cols="2" :x-gap="12">
|
||||
<n-grid-item>
|
||||
<n-form-item label="VIP 等级">
|
||||
<n-select v-model:value="batchForm.level" :options="levelOptions" />
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-form-item label="天数(0=永久)">
|
||||
<n-input-number v-model:value="batchForm.days" :min="0" :max="3650" style="width: 100%;" />
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
<n-grid cols="2" :x-gap="12">
|
||||
<n-grid-item>
|
||||
<n-form-item label="前缀">
|
||||
<n-input v-model:value="batchForm.prefix" placeholder="AK" />
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-form-item label="随机长度">
|
||||
<n-input-number v-model:value="batchForm.length" :min="8" :max="32" style="width: 100%;" />
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
<n-form-item label="过期时间">
|
||||
<n-date-picker v-model:value="batchForm.expiresAt" type="datetime" clearable style="width: 100%;" />
|
||||
</n-form-item>
|
||||
<n-form-item label="备注">
|
||||
<n-input v-model:value="batchForm.note" type="textarea" :rows="3" />
|
||||
</n-form-item>
|
||||
<n-space justify="end">
|
||||
<n-button @click="batchVisible = false">取消</n-button>
|
||||
<n-button type="primary" :loading="submittingBatch" @click="handleBatchCreate">批量生成</n-button>
|
||||
</n-space>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
|
||||
<ChargeCardDetailDrawer v-model:show="drawerVisible" :card-id="currentCardId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { NButton, NSpace, NTag, useDialog, useMessage } from 'naive-ui'
|
||||
import api from '@/api'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import ChargeCardDetailDrawer from './components/ChargeCardDetailDrawer.vue'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const loading = ref(false)
|
||||
const rows = ref([])
|
||||
const createVisible = ref(false)
|
||||
const batchVisible = ref(false)
|
||||
const drawerVisible = ref(false)
|
||||
const currentCardId = ref(null)
|
||||
const submittingCreate = ref(false)
|
||||
const submittingBatch = ref(false)
|
||||
|
||||
const stats = reactive({
|
||||
total: 0,
|
||||
active: 0,
|
||||
used: 0,
|
||||
total_amount: 0,
|
||||
used_amount: 0,
|
||||
})
|
||||
|
||||
const filters = reactive({
|
||||
keyword: '',
|
||||
status: '',
|
||||
dateRange: null,
|
||||
})
|
||||
|
||||
const createForm = reactive({
|
||||
type: 'balance',
|
||||
code: '',
|
||||
amount: 10,
|
||||
level: 1,
|
||||
days: 30,
|
||||
points: 100,
|
||||
expiresAt: null,
|
||||
note: '',
|
||||
})
|
||||
|
||||
const batchForm = reactive({
|
||||
type: 'balance',
|
||||
count: 20,
|
||||
amount: 10,
|
||||
level: 1,
|
||||
days: 30,
|
||||
points: 100,
|
||||
prefix: 'AK',
|
||||
length: 12,
|
||||
expiresAt: null,
|
||||
note: '',
|
||||
})
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '余额卡', value: 'balance' },
|
||||
{ label: '会员卡', value: 'vip' },
|
||||
{ label: '积分卡', value: 'credits' },
|
||||
]
|
||||
|
||||
const levelOptions = [
|
||||
{ label: 'VIP1', value: 1 },
|
||||
{ label: 'VIP2', value: 2 },
|
||||
]
|
||||
|
||||
const pager = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
itemCount: 0,
|
||||
})
|
||||
|
||||
const optionsState = reactive({
|
||||
statuses: [],
|
||||
})
|
||||
|
||||
const statCards = [
|
||||
{ key: 'total', label: '总卡数' },
|
||||
{ key: 'active', label: '未使用' },
|
||||
{ key: 'used', label: '已使用' },
|
||||
{ key: 'total_amount', label: '总面额' },
|
||||
{ key: 'used_amount', label: '已兑换金额' },
|
||||
]
|
||||
|
||||
const statusOptions = computed(() => optionsState.statuses.length
|
||||
? optionsState.statuses
|
||||
: [
|
||||
{ label: '全部状态', value: '' },
|
||||
{ label: '未使用', value: 'active' },
|
||||
{ label: '已使用', value: 'used' },
|
||||
{ label: '已过期', value: 'expired' },
|
||||
{ label: '已禁用', value: 'disabled' },
|
||||
])
|
||||
|
||||
const pagination = computed(() => ({
|
||||
page: pager.page,
|
||||
pageSize: pager.pageSize,
|
||||
itemCount: pager.itemCount,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
onUpdatePage: (page) => {
|
||||
pager.page = page
|
||||
loadData()
|
||||
},
|
||||
onUpdatePageSize: (pageSize) => {
|
||||
pager.pageSize = pageSize
|
||||
pager.page = 1
|
||||
loadData()
|
||||
},
|
||||
}))
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', key: 'id', width: 70 },
|
||||
{ title: '卡密', key: 'code', minWidth: 170, ellipsis: { tooltip: true } },
|
||||
{
|
||||
title: '类型',
|
||||
key: 'type_label',
|
||||
width: 90,
|
||||
render: (row) => row.type_label || row.type || '-',
|
||||
},
|
||||
{
|
||||
title: '面额/权益',
|
||||
key: 'amount',
|
||||
width: 120,
|
||||
render: (row) => {
|
||||
if (row.type === 'vip') {
|
||||
const days = Number(row.days || 0)
|
||||
return `VIP${row.level || 1} / ${days > 0 ? `${days}天` : '永久'}`
|
||||
}
|
||||
if (row.type === 'credits') return `${row.points || row.amount || 0} 积分`
|
||||
return row.amount
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status_label',
|
||||
width: 100,
|
||||
render: (row) => {
|
||||
const typeMap = { active: 'success', used: 'warning', expired: 'default', disabled: 'error' }
|
||||
return h(NTag, { size: 'small', round: true, type: typeMap[row.status] || 'default' }, () => row.status_label)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '使用人',
|
||||
key: 'used_by_user',
|
||||
width: 160,
|
||||
render: (row) => row.used_by_user?.display_name || '-',
|
||||
},
|
||||
{ title: '创建时间', key: 'created_at', width: 170 },
|
||||
{
|
||||
title: '过期时间',
|
||||
key: 'expires_at',
|
||||
width: 170,
|
||||
render: (row) => row.expires_at || '-',
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
key: 'note',
|
||||
minWidth: 160,
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row) => row.note || '-',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 140,
|
||||
render: (row) => h(NSpace, { size: 'small' }, () => {
|
||||
const actions = [
|
||||
h(NButton, {
|
||||
size: 'tiny',
|
||||
type: 'primary',
|
||||
quaternary: true,
|
||||
onClick: () => openDrawer(row.id),
|
||||
}, () => '详情'),
|
||||
]
|
||||
|
||||
if (row.status === 'active') {
|
||||
actions.push(h(NButton, {
|
||||
size: 'tiny',
|
||||
type: 'warning',
|
||||
quaternary: true,
|
||||
onClick: () => handleDisable(row),
|
||||
}, () => '禁用'))
|
||||
}
|
||||
|
||||
return actions
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
function formatDateValue(value) {
|
||||
const date = new Date(value)
|
||||
const year = date.getFullYear()
|
||||
const month = `${date.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${date.getDate()}`.padStart(2, '0')
|
||||
const hours = `${date.getHours()}`.padStart(2, '0')
|
||||
const minutes = `${date.getMinutes()}`.padStart(2, '0')
|
||||
const seconds = `${date.getSeconds()}`.padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
function buildParams(page = pager.page, pageSize = pager.pageSize) {
|
||||
const params = {
|
||||
page,
|
||||
per_page: pageSize,
|
||||
}
|
||||
if (filters.keyword) params.keyword = filters.keyword
|
||||
if (filters.status) params.status = filters.status
|
||||
if (Array.isArray(filters.dateRange) && filters.dateRange.length === 2) {
|
||||
const [start, end] = filters.dateRange
|
||||
params.date_from = formatDateValue(start).slice(0, 10)
|
||||
params.date_to = formatDateValue(end).slice(0, 10)
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.request('/payment/charge-cards', { params: buildParams() })
|
||||
const data = res?.data || {}
|
||||
rows.value = data.items || []
|
||||
pager.itemCount = data.total || 0
|
||||
Object.assign(stats, data.stats || {})
|
||||
optionsState.statuses = data.options?.statuses || []
|
||||
} catch (error) {
|
||||
message.error(error?.message || '加载充值卡台账失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pager.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.keyword = ''
|
||||
filters.status = ''
|
||||
filters.dateRange = null
|
||||
pager.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
submittingCreate.value = true
|
||||
try {
|
||||
await api.request('/payment/charge-cards', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
type: createForm.type,
|
||||
code: createForm.code,
|
||||
password: createForm.code,
|
||||
amount: createForm.amount,
|
||||
level: createForm.level,
|
||||
days: createForm.days,
|
||||
points: createForm.points,
|
||||
expires_at: createForm.expiresAt ? formatDateValue(createForm.expiresAt) : '',
|
||||
note: createForm.note,
|
||||
},
|
||||
})
|
||||
message.success('充值卡已创建')
|
||||
createVisible.value = false
|
||||
createForm.code = ''
|
||||
createForm.note = ''
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
message.error(error?.message || '创建失败')
|
||||
} finally {
|
||||
submittingCreate.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBatchCreate() {
|
||||
submittingBatch.value = true
|
||||
try {
|
||||
await api.request('/payment/charge-cards/batch', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
type: batchForm.type,
|
||||
count: batchForm.count,
|
||||
amount: batchForm.amount,
|
||||
level: batchForm.level,
|
||||
days: batchForm.days,
|
||||
points: batchForm.points,
|
||||
prefix: batchForm.prefix,
|
||||
length: batchForm.length,
|
||||
expires_at: batchForm.expiresAt ? formatDateValue(batchForm.expiresAt) : '',
|
||||
note: batchForm.note,
|
||||
},
|
||||
})
|
||||
message.success('充值卡已批量生成')
|
||||
batchVisible.value = false
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
message.error(error?.message || '批量生成失败')
|
||||
} finally {
|
||||
submittingBatch.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDisable(row) {
|
||||
dialog.warning({
|
||||
title: '确认禁用',
|
||||
content: `确定要禁用充值卡 ${row.code} 吗?`,
|
||||
positiveText: '禁用',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await api.request(`/payment/charge-cards/${row.id}/disable`, { method: 'POST' })
|
||||
message.success('充值卡已禁用')
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
message.error(error?.message || '禁用失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function openDrawer(id) {
|
||||
currentCardId.value = id
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
try {
|
||||
const res = await api.request('/payment/charge-cards', { params: buildParams(1, 200) })
|
||||
const items = res?.data?.items || []
|
||||
const header = ['ID', '卡号', '面额', '余额', '状态', '使用人', '创建时间', '过期时间', '备注']
|
||||
const lines = items.map((row) => [
|
||||
row.id,
|
||||
row.code,
|
||||
row.amount,
|
||||
row.balance,
|
||||
row.status_label,
|
||||
row.used_by_user?.display_name || '',
|
||||
row.created_at || '',
|
||||
row.expires_at || '',
|
||||
row.note || '',
|
||||
])
|
||||
downloadCsv('charge-cards.csv', [header, ...lines])
|
||||
message.success('导出完成')
|
||||
} catch (error) {
|
||||
message.error(error?.message || '导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
function downloadCsv(filename, rows) {
|
||||
const csv = `\uFEFF${rows.map((row) => row.map((cell) => `"${String(cell ?? '').replaceAll('"', '""')}"`).join(',')).join('\n')}`
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-charge-card-page__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.aksrt-charge-card-page__card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader :title="title" :desc="desc">
|
||||
<template #actions>
|
||||
<n-space v-if="links.length">
|
||||
<n-button v-for="link in links" :key="link.path" quaternary @click="go(link.path)">
|
||||
{{ link.label }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<n-card v-if="note" class="aksrt-section-card">
|
||||
<n-alert type="info" :show-icon="false">
|
||||
{{ note }}
|
||||
</n-alert>
|
||||
</n-card>
|
||||
|
||||
<slot name="before-form" />
|
||||
|
||||
<AksFormRenderer
|
||||
:schema="pageMeta.groups"
|
||||
:form-data="formData"
|
||||
:dirty="dirty"
|
||||
:loading="loading"
|
||||
:saving="saving"
|
||||
:load-error="loadError"
|
||||
:show-search="false"
|
||||
@update:form-data="formData = $event"
|
||||
@save="handleSave"
|
||||
@reload="handleReload"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import api from '@/api'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import AksFormRenderer from '@/components/form/AksFormRenderer.vue'
|
||||
import { useSectionForm } from '@/composables/useSectionForm'
|
||||
import { getSchema } from '@/schema'
|
||||
|
||||
const props = defineProps({
|
||||
section: { type: String, required: true },
|
||||
title: { type: String, required: true },
|
||||
desc: { type: String, default: '' },
|
||||
note: { type: String, default: '' },
|
||||
links: { type: Array, default: () => [] },
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
const pageMeta = computed(() => getSchema('payment', props.section) || { groups: [] })
|
||||
const defaults = computed(() => {
|
||||
const result = {}
|
||||
;(pageMeta.value.groups || []).forEach((group) => {
|
||||
;(group.fields || []).forEach((field) => {
|
||||
if (field.default !== undefined) {
|
||||
result[field.key] = field.default
|
||||
}
|
||||
})
|
||||
})
|
||||
return result
|
||||
})
|
||||
|
||||
const {
|
||||
formData,
|
||||
loading,
|
||||
saving,
|
||||
loadError,
|
||||
dirty,
|
||||
save: doSave,
|
||||
reload,
|
||||
} = useSectionForm({
|
||||
fetch: () => api.getSchemaPage('payment', props.section),
|
||||
submit: (data) => api.saveSchemaPage('payment', props.section, data),
|
||||
defaults: defaults.value,
|
||||
onSaved: () => {
|
||||
message.success('设置已保存')
|
||||
},
|
||||
})
|
||||
|
||||
function go(path) {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
await doSave()
|
||||
} catch (error) {
|
||||
message.error(error.message || '保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
function handleReload() {
|
||||
reload()
|
||||
message.info('已放弃更改')
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<n-drawer :show="show" :width="680" @update:show="$emit('update:show', $event)">
|
||||
<n-drawer-content title="充值卡详情" closable>
|
||||
<n-spin :show="loading">
|
||||
<template v-if="detail.card">
|
||||
<n-descriptions bordered :column="1" size="small" label-placement="left">
|
||||
<n-descriptions-item label="卡号">{{ detail.card.code || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="面额">{{ detail.card.amount || 0 }}</n-descriptions-item>
|
||||
<n-descriptions-item label="余额">{{ detail.card.balance || 0 }}</n-descriptions-item>
|
||||
<n-descriptions-item label="状态">{{ detail.card.status_label || detail.card.status }}</n-descriptions-item>
|
||||
<n-descriptions-item label="创建时间">{{ detail.card.created_at || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="过期时间">{{ detail.card.expires_at || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="使用时间">{{ detail.card.used_at || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="使用者">
|
||||
{{ detail.card.used_by_user?.display_name || '-' }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
|
||||
<div class="aksrt-charge-drawer__section">
|
||||
<div class="aksrt-charge-drawer__title">备注</div>
|
||||
<n-alert type="info" :show-icon="false">
|
||||
{{ detail.card.note || '暂无备注' }}
|
||||
</n-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<n-empty v-else description="暂无充值卡详情" />
|
||||
</n-spin>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import api from '@/api'
|
||||
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, default: false },
|
||||
cardId: { type: [Number, String], default: null },
|
||||
})
|
||||
|
||||
defineEmits(['update:show'])
|
||||
|
||||
const message = useMessage()
|
||||
const loading = ref(false)
|
||||
const detail = ref({ card: null })
|
||||
|
||||
watch(
|
||||
() => [props.show, props.cardId],
|
||||
async ([visible, id]) => {
|
||||
if (visible && id) {
|
||||
await loadDetail()
|
||||
}
|
||||
if (!visible) {
|
||||
detail.value = { card: null }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
async function loadDetail() {
|
||||
if (!props.cardId) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.request(`/payment/charge-cards/${props.cardId}`)
|
||||
detail.value = res?.data || { card: null }
|
||||
} catch (error) {
|
||||
message.error(error?.message || '加载充值卡详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-charge-drawer__section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.aksrt-charge-drawer__title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<n-drawer :show="show" :width="720" @update:show="$emit('update:show', $event)">
|
||||
<n-drawer-content title="返佣详情" closable>
|
||||
<n-spin :show="loading">
|
||||
<template v-if="detail.record">
|
||||
<n-descriptions bordered :column="1" size="small" label-placement="left">
|
||||
<n-descriptions-item label="订单号">{{ detail.record.order_number || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="返佣对象">{{ detail.record.user?.display_name || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="来源用户">{{ detail.record.source_user?.display_name || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="返佣金额">{{ detail.record.amount || 0 }}</n-descriptions-item>
|
||||
<n-descriptions-item label="类型">{{ detail.record.type_label || detail.record.type }}</n-descriptions-item>
|
||||
<n-descriptions-item label="状态">{{ detail.record.status_label || detail.record.status }}</n-descriptions-item>
|
||||
<n-descriptions-item label="创建时间">{{ detail.record.created_at || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="结算时间">{{ detail.record.settled_at || '-' }}</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
|
||||
<div class="aksrt-rebate-drawer__section">
|
||||
<div class="aksrt-rebate-drawer__title">备注</div>
|
||||
<n-alert type="info" :show-icon="false">
|
||||
{{ detail.record.note || '暂无备注' }}
|
||||
</n-alert>
|
||||
</div>
|
||||
|
||||
<div class="aksrt-rebate-drawer__section">
|
||||
<div class="aksrt-rebate-drawer__title">处理记录</div>
|
||||
<div v-if="detail.history?.length" class="aksrt-rebate-drawer__timeline">
|
||||
<div v-for="(entry, index) in detail.history" :key="index" class="aksrt-rebate-drawer__timeline-item">
|
||||
<div class="aksrt-rebate-drawer__timeline-head">
|
||||
<strong>{{ entry.event || '-' }}</strong>
|
||||
<span>{{ entry.time || '-' }}</span>
|
||||
</div>
|
||||
<p>{{ entry.note || '无备注' }}</p>
|
||||
<small>{{ entry.operator_name || '系统' }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<n-empty v-else description="暂无处理记录" />
|
||||
</div>
|
||||
|
||||
<div v-if="detail.record.status === 'pending'" class="aksrt-rebate-drawer__section">
|
||||
<div class="aksrt-rebate-drawer__title">处理返佣</div>
|
||||
<n-space vertical :size="12">
|
||||
<n-input
|
||||
v-model:value="form.note"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="记录结算说明或取消原因"
|
||||
/>
|
||||
<n-space>
|
||||
<n-button type="primary" :loading="submitting === 'settle'" @click="handleAction('settle')">
|
||||
结算返佣
|
||||
</n-button>
|
||||
<n-button type="warning" secondary :loading="submitting === 'cancel'" @click="handleAction('cancel')">
|
||||
取消返佣
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<n-empty v-else description="暂无返佣详情" />
|
||||
</n-spin>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import api from '@/api'
|
||||
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, default: false },
|
||||
rebateId: { type: [Number, String], default: null },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:show', 'updated'])
|
||||
const message = useMessage()
|
||||
const loading = ref(false)
|
||||
const submitting = ref('')
|
||||
const detail = ref({ record: null, history: [] })
|
||||
const form = reactive({ note: '' })
|
||||
|
||||
watch(
|
||||
() => [props.show, props.rebateId],
|
||||
async ([visible, id]) => {
|
||||
if (visible && id) {
|
||||
await loadDetail()
|
||||
}
|
||||
if (!visible) {
|
||||
detail.value = { record: null, history: [] }
|
||||
form.note = ''
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
async function loadDetail() {
|
||||
if (!props.rebateId) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.request(`/payment/rebate/${props.rebateId}`)
|
||||
detail.value = res?.data || { record: null, history: [] }
|
||||
form.note = ''
|
||||
} catch (error) {
|
||||
message.error(error?.message || '加载返佣详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAction(action) {
|
||||
if (!props.rebateId) {
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = action
|
||||
try {
|
||||
await api.request(`/payment/rebate/${props.rebateId}/${action}`, {
|
||||
method: 'POST',
|
||||
data: { note: form.note },
|
||||
})
|
||||
message.success(action === 'settle' ? '返佣已结算' : '返佣已取消')
|
||||
await loadDetail()
|
||||
emit('updated')
|
||||
} catch (error) {
|
||||
message.error(error?.message || '处理失败')
|
||||
} finally {
|
||||
submitting.value = ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-rebate-drawer__section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.aksrt-rebate-drawer__title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.aksrt-rebate-drawer__timeline {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.aksrt-rebate-drawer__timeline-item {
|
||||
padding: 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.aksrt-rebate-drawer__timeline-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.aksrt-rebate-drawer__timeline-item p {
|
||||
margin: 0 0 6px;
|
||||
color: #111827;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.aksrt-rebate-drawer__timeline-item small {
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user