|
|
|
@ -14,20 +14,356 @@ |
|
|
|
<t-tab-panel value="ledger" label="账务流水" :destroy-on-hide="false" /> |
|
|
|
</t-tabs> |
|
|
|
|
|
|
|
<div v-show="tab === 'packages'">套餐管理占位</div> |
|
|
|
<div v-show="tab === 'packages'" class="packages-panel"> |
|
|
|
<div class="asset-toolbar"> |
|
|
|
<div class="toolbar-group"> |
|
|
|
<t-select |
|
|
|
v-model="pkgStatusFilter" |
|
|
|
:options="pkgStatusFilterOptions" |
|
|
|
placeholder="全部状态" |
|
|
|
style="width: 140px" |
|
|
|
@change="reloadPackages" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
<div class="page-actions"> |
|
|
|
<t-button shape="square" variant="outline" title="刷新" @click="loadPackages"> |
|
|
|
<svg><use href="#i-refresh" /></svg> |
|
|
|
</t-button> |
|
|
|
<t-button theme="primary" @click="openPkgAdd"> |
|
|
|
<svg><use href="#i-plus" /></svg>新建套餐 |
|
|
|
</t-button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="asset-table-wrap"> |
|
|
|
<t-table |
|
|
|
row-key="id" |
|
|
|
:data="pkgList" |
|
|
|
:columns="pkgColumns" |
|
|
|
empty="暂无流量套餐" |
|
|
|
> |
|
|
|
<template #status="{ row }"> |
|
|
|
<span class="table-status" :class="row.statusClass"><i></i>{{ row.statusName }}</span> |
|
|
|
</template> |
|
|
|
<template #actions="{ row }"> |
|
|
|
<div class="row-actions"> |
|
|
|
<t-button variant="text" theme="primary" @click="openPkgEdit(row)">编辑</t-button> |
|
|
|
<t-button |
|
|
|
v-if="row.status === 'active'" |
|
|
|
variant="text" |
|
|
|
theme="danger" |
|
|
|
@click="disablePackage(row)" |
|
|
|
>下架</t-button> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
</t-table> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="table-footer"> |
|
|
|
<t-pagination |
|
|
|
v-model:current="pkgPageNum" |
|
|
|
:total="pkgTotal" |
|
|
|
:page-size="pkgPageSize" |
|
|
|
@current-change="loadPackages" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div v-show="tab === 'purchases'">采购入池占位</div> |
|
|
|
<div v-show="tab === 'ledger'">账务流水占位</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<t-dialog |
|
|
|
v-model:visible="pkgModalVisible" |
|
|
|
:header="pkgEditingId ? '编辑套餐' : '新建套餐'" |
|
|
|
width="520px" |
|
|
|
destroy-on-close |
|
|
|
dialog-class-name="add-dock-dialog" |
|
|
|
:close-on-overlay-click="false" |
|
|
|
placement="center" |
|
|
|
> |
|
|
|
<form novalidate @submit.prevent="submitPackage"> |
|
|
|
<label> |
|
|
|
<span>编码<b>*</b></span> |
|
|
|
<t-input |
|
|
|
v-model="pkgForm.code" |
|
|
|
:disabled="!!pkgEditingId" |
|
|
|
placeholder="唯一编码,如 MEDIA_10G" |
|
|
|
/> |
|
|
|
</label> |
|
|
|
<label> |
|
|
|
<span>名称<b>*</b></span> |
|
|
|
<t-input v-model="pkgForm.name" placeholder="套餐名称" /> |
|
|
|
</label> |
|
|
|
<div class="form-row"> |
|
|
|
<label> |
|
|
|
<span>额度(GB)<b>*</b></span> |
|
|
|
<t-input-number |
|
|
|
v-model="pkgForm.amountGb" |
|
|
|
theme="normal" |
|
|
|
:min="0.01" |
|
|
|
:step="1" |
|
|
|
style="width: 100%" |
|
|
|
/> |
|
|
|
</label> |
|
|
|
<label> |
|
|
|
<span>价格(元)</span> |
|
|
|
<t-input-number |
|
|
|
v-model="pkgForm.price" |
|
|
|
theme="normal" |
|
|
|
:min="0" |
|
|
|
:step="0.01" |
|
|
|
style="width: 100%" |
|
|
|
/> |
|
|
|
</label> |
|
|
|
</div> |
|
|
|
<div class="form-row"> |
|
|
|
<label> |
|
|
|
<span>有效期(天)</span> |
|
|
|
<t-input-number |
|
|
|
v-model="pkgForm.validityDays" |
|
|
|
theme="normal" |
|
|
|
:min="0" |
|
|
|
style="width: 100%" |
|
|
|
/> |
|
|
|
</label> |
|
|
|
<label> |
|
|
|
<span>排序</span> |
|
|
|
<t-input-number |
|
|
|
v-model="pkgForm.sort" |
|
|
|
theme="normal" |
|
|
|
:min="0" |
|
|
|
style="width: 100%" |
|
|
|
/> |
|
|
|
</label> |
|
|
|
</div> |
|
|
|
<label v-if="pkgEditingId"> |
|
|
|
<span>状态</span> |
|
|
|
<t-select v-model="pkgForm.status" :options="pkgStatusOptions" style="width: 100%" /> |
|
|
|
</label> |
|
|
|
<div v-if="!pkgEditingId" class="form-hint">有效期填 0 表示长期有效</div> |
|
|
|
<div class="form-error">{{ pkgFormError }}</div> |
|
|
|
</form> |
|
|
|
<template #footer> |
|
|
|
<t-button variant="outline" @click="pkgModalVisible = false">取消</t-button> |
|
|
|
<t-button theme="primary" @click="submitPackage"> |
|
|
|
{{ pkgEditingId ? '保存' : '确认新建' }} |
|
|
|
</t-button> |
|
|
|
</template> |
|
|
|
</t-dialog> |
|
|
|
</section> |
|
|
|
</template> |
|
|
|
|
|
|
|
<script setup> |
|
|
|
import { ref, watch } from 'vue' |
|
|
|
import { onMounted, reactive, ref, watch } from 'vue' |
|
|
|
import { useUiStore } from '@/stores/modules/uiStore' |
|
|
|
import request from '@/utils/http' |
|
|
|
import * as urls from '@/config/urls' |
|
|
|
|
|
|
|
const ui = useUiStore() |
|
|
|
const GB_BYTES = 1024 ** 3 |
|
|
|
|
|
|
|
function bytesToGb(n) { |
|
|
|
const v = Number(n || 0) / GB_BYTES |
|
|
|
return Number.isFinite(v) ? v : 0 |
|
|
|
} |
|
|
|
function bytesToGbText(n) { |
|
|
|
return `${bytesToGb(n).toFixed(2)} GB` |
|
|
|
} |
|
|
|
function fmtTime(t) { |
|
|
|
if (!t) return '--' |
|
|
|
const d = new Date(t) |
|
|
|
if (Number.isNaN(d.getTime())) return '--' |
|
|
|
return d.toLocaleString('zh-CN', { hour12: false }) |
|
|
|
} |
|
|
|
|
|
|
|
const tab = ref('packages') |
|
|
|
|
|
|
|
const pkgList = ref([]) |
|
|
|
const pkgTotal = ref(0) |
|
|
|
const pkgPageNum = ref(1) |
|
|
|
const pkgPageSize = ref(10) |
|
|
|
const pkgStatusFilter = ref('') |
|
|
|
const pkgModalVisible = ref(false) |
|
|
|
const pkgEditingId = ref(null) |
|
|
|
const pkgFormError = ref('') |
|
|
|
const pkgForm = reactive({ |
|
|
|
code: '', |
|
|
|
name: '', |
|
|
|
amountGb: 1, |
|
|
|
price: 0, |
|
|
|
validityDays: 0, |
|
|
|
sort: 0, |
|
|
|
status: 'active' |
|
|
|
}) |
|
|
|
|
|
|
|
const pkgStatusFilterOptions = [ |
|
|
|
{ label: '全部状态', value: '' }, |
|
|
|
{ label: '上架', value: 'active' }, |
|
|
|
{ label: '下架', value: 'inactive' } |
|
|
|
] |
|
|
|
const pkgStatusOptions = [ |
|
|
|
{ label: '上架', value: 'active' }, |
|
|
|
{ label: '下架', value: 'inactive' } |
|
|
|
] |
|
|
|
const pkgColumns = [ |
|
|
|
{ colKey: 'code', title: '编码', width: '12%' }, |
|
|
|
{ colKey: 'name', title: '名称', width: '14%' }, |
|
|
|
{ colKey: 'amountText', title: '额度', width: '10%' }, |
|
|
|
{ colKey: 'priceText', title: '价格', width: '8%' }, |
|
|
|
{ colKey: 'validityText', title: '有效期', width: '10%' }, |
|
|
|
{ colKey: 'status', title: '状态', width: '8%' }, |
|
|
|
{ colKey: 'sort', title: '排序', width: '6%' }, |
|
|
|
{ colKey: 'updatedAt', title: '更新时间', width: '16%' }, |
|
|
|
{ colKey: 'actions', title: '操作', width: '12%' } |
|
|
|
] |
|
|
|
|
|
|
|
async function loadPackages() { |
|
|
|
try { |
|
|
|
const params = { pageNum: pkgPageNum.value, pageSize: pkgPageSize.value } |
|
|
|
if (pkgStatusFilter.value) params.status = pkgStatusFilter.value |
|
|
|
const page = await request.get(urls.ADMIN_TRAFFIC_PACKAGES, { params }) |
|
|
|
pkgTotal.value = page?.total || 0 |
|
|
|
pkgList.value = (page?.records || []).map((p) => ({ |
|
|
|
id: String(p.id), |
|
|
|
code: p.code, |
|
|
|
name: p.name, |
|
|
|
amountText: bytesToGbText(p.amountBytes), |
|
|
|
priceText: `¥${Number(p.price || 0).toFixed(2)}`, |
|
|
|
validityText: Number(p.validityDays) > 0 ? `${p.validityDays} 天` : '长期有效', |
|
|
|
status: p.status, |
|
|
|
statusName: p.status === 'active' ? '上架' : '下架', |
|
|
|
statusClass: p.status === 'active' ? 'online' : 'offline', |
|
|
|
sort: p.sort ?? 0, |
|
|
|
updatedAt: fmtTime(p.updatedAt), |
|
|
|
raw: p |
|
|
|
})) |
|
|
|
} catch (e) { |
|
|
|
ui.toast(e.message || '加载套餐失败') |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function reloadPackages() { |
|
|
|
pkgPageNum.value = 1 |
|
|
|
loadPackages() |
|
|
|
} |
|
|
|
|
|
|
|
function openPkgAdd() { |
|
|
|
Object.assign(pkgForm, { |
|
|
|
code: '', |
|
|
|
name: '', |
|
|
|
amountGb: 1, |
|
|
|
price: 0, |
|
|
|
validityDays: 0, |
|
|
|
sort: 0, |
|
|
|
status: 'active' |
|
|
|
}) |
|
|
|
pkgEditingId.value = null |
|
|
|
pkgFormError.value = '' |
|
|
|
pkgModalVisible.value = true |
|
|
|
} |
|
|
|
|
|
|
|
function openPkgEdit(row) { |
|
|
|
const r = row.raw |
|
|
|
Object.assign(pkgForm, { |
|
|
|
code: r.code, |
|
|
|
name: r.name, |
|
|
|
amountGb: Number(bytesToGb(r.amountBytes).toFixed(4)) || 1, |
|
|
|
price: Number(r.price || 0), |
|
|
|
validityDays: Number(r.validityDays || 0), |
|
|
|
sort: Number(r.sort || 0), |
|
|
|
status: r.status || 'active' |
|
|
|
}) |
|
|
|
pkgEditingId.value = row.id |
|
|
|
pkgFormError.value = '' |
|
|
|
pkgModalVisible.value = true |
|
|
|
} |
|
|
|
|
|
|
|
async function submitPackage() { |
|
|
|
pkgFormError.value = '' |
|
|
|
if (!pkgForm.name || (!pkgEditingId.value && !pkgForm.code)) { |
|
|
|
pkgFormError.value = '请填写编码和名称' |
|
|
|
return |
|
|
|
} |
|
|
|
if (!(Number(pkgForm.amountGb) > 0)) { |
|
|
|
pkgFormError.value = '额度必须大于 0 GB' |
|
|
|
return |
|
|
|
} |
|
|
|
const amountBytes = Math.round(Number(pkgForm.amountGb) * GB_BYTES) |
|
|
|
try { |
|
|
|
if (pkgEditingId.value) { |
|
|
|
// 后端 Update: price/validityDays 仅 >0 才写入;编辑时若用户未改就原样提交正值 |
|
|
|
const payload = { |
|
|
|
name: pkgForm.name, |
|
|
|
amountBytes, |
|
|
|
sort: Number(pkgForm.sort || 0), |
|
|
|
status: pkgForm.status |
|
|
|
} |
|
|
|
if (Number(pkgForm.price) > 0) payload.price = Number(pkgForm.price) |
|
|
|
if (Number(pkgForm.validityDays) > 0) payload.validityDays = Number(pkgForm.validityDays) |
|
|
|
// 长期有效(0)无法通过 Update 写入;保持原值即可。若必须把有效期改成长期,需后端后续支持。 |
|
|
|
await request.put(urls.ADMIN_TRAFFIC_PACKAGE(pkgEditingId.value), payload) |
|
|
|
ui.toast('套餐已更新') |
|
|
|
} else { |
|
|
|
await request.post(urls.ADMIN_TRAFFIC_PACKAGES, { |
|
|
|
code: pkgForm.code.trim(), |
|
|
|
name: pkgForm.name.trim(), |
|
|
|
amountBytes, |
|
|
|
price: Number(pkgForm.price || 0), |
|
|
|
validityDays: Number(pkgForm.validityDays || 0), |
|
|
|
sort: Number(pkgForm.sort || 0) |
|
|
|
}) |
|
|
|
ui.toast('套餐已创建') |
|
|
|
} |
|
|
|
pkgModalVisible.value = false |
|
|
|
await loadPackages() |
|
|
|
} catch (e) { |
|
|
|
pkgFormError.value = e.message || '保存失败' |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
async function disablePackage(row) { |
|
|
|
const ok = await ui.confirm( |
|
|
|
`确认下架套餐 ${row.name}?`, |
|
|
|
'下架后用户侧购买区不可见,之后可再编辑上架。' |
|
|
|
) |
|
|
|
if (!ok) return |
|
|
|
try { |
|
|
|
await request.delete(urls.ADMIN_TRAFFIC_PACKAGE(row.id)) |
|
|
|
ui.toast('套餐已下架') |
|
|
|
await loadPackages() |
|
|
|
} catch (e) { |
|
|
|
ui.toast(e.message || '下架失败') |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
watch(tab, (v) => { |
|
|
|
// 后续各 Tab 按需懒加载 |
|
|
|
void v |
|
|
|
if (v === 'packages') loadPackages() |
|
|
|
}) |
|
|
|
|
|
|
|
onMounted(() => { |
|
|
|
if (tab.value === 'packages') loadPackages() |
|
|
|
}) |
|
|
|
</script> |
|
|
|
|
|
|
|
<style scoped> |
|
|
|
.packages-panel { |
|
|
|
flex: 1; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
min-height: 0; |
|
|
|
} |
|
|
|
.form-hint { |
|
|
|
margin-top: 4px; |
|
|
|
color: var(--muted); |
|
|
|
font-size: 11px; |
|
|
|
} |
|
|
|
</style> |
|
|
|
|
|
|
|
<style> |
|
|
|
.add-dock-dialog .t-input, |
|
|
|
.add-dock-dialog .t-input-number, |
|
|
|
.add-dock-dialog .t-select { |
|
|
|
width: 100%; |
|
|
|
} |
|
|
|
</style> |
|
|
|
|