|
|
@ -69,7 +69,78 @@ |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div v-show="tab === 'purchases'">采购入池占位</div> |
|
|
|
|
|
|
|
|
<div v-show="tab === 'purchases'" class="purchases-panel"> |
|
|
|
|
|
<div v-if="poolMissing" class="pool-empty"> |
|
|
|
|
|
尚未建立资源池,首次录入采购后自动创建 |
|
|
|
|
|
</div> |
|
|
|
|
|
<div v-else-if="pool" class="business-metric-band pool-metrics"> |
|
|
|
|
|
<div> |
|
|
|
|
|
<span>资源池总量</span> |
|
|
|
|
|
<strong>{{ bytesToGbText(pool.totalBytes) }}</strong> |
|
|
|
|
|
<small>{{ pool.totalBytes ?? 0 }} bytes</small> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div> |
|
|
|
|
|
<span>可用流量</span> |
|
|
|
|
|
<strong>{{ bytesToGbText(pool.availableBytes) }}</strong> |
|
|
|
|
|
<small>{{ pool.availableBytes ?? 0 }} bytes</small> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div> |
|
|
|
|
|
<span>池状态</span> |
|
|
|
|
|
<strong>{{ pool.status === 'active' ? '有效' : (pool.status || '—') }}</strong> |
|
|
|
|
|
<small>{{ pool.status || '—' }}</small> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<div class="asset-toolbar"> |
|
|
|
|
|
<div class="asset-search"> |
|
|
|
|
|
<t-input |
|
|
|
|
|
v-model="purchaseProvider" |
|
|
|
|
|
clearable |
|
|
|
|
|
placeholder="按供应商筛选" |
|
|
|
|
|
@keyup.enter="reloadPurchases" |
|
|
|
|
|
@clear="reloadPurchases" |
|
|
|
|
|
> |
|
|
|
|
|
<template #prefix-icon><t-icon name="search" /></template> |
|
|
|
|
|
</t-input> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="toolbar-group"> |
|
|
|
|
|
<t-select |
|
|
|
|
|
v-model="purchaseStatus" |
|
|
|
|
|
:options="purchaseStatusOptions" |
|
|
|
|
|
placeholder="全部状态" |
|
|
|
|
|
style="width: 140px" |
|
|
|
|
|
@change="reloadPurchases" |
|
|
|
|
|
/> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="page-actions"> |
|
|
|
|
|
<t-button shape="square" variant="outline" title="刷新" @click="loadPurchasesTab"> |
|
|
|
|
|
<svg><use href="#i-refresh" /></svg> |
|
|
|
|
|
</t-button> |
|
|
|
|
|
<t-button theme="primary" @click="openPurchaseAdd"> |
|
|
|
|
|
<svg><use href="#i-plus" /></svg>录入采购 |
|
|
|
|
|
</t-button> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<div class="asset-table-wrap"> |
|
|
|
|
|
<t-table |
|
|
|
|
|
row-key="id" |
|
|
|
|
|
:data="purchaseList" |
|
|
|
|
|
:columns="purchaseColumns" |
|
|
|
|
|
empty="暂无采购记录" |
|
|
|
|
|
/> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<div class="table-footer"> |
|
|
|
|
|
<t-pagination |
|
|
|
|
|
v-model:current="purchasePageNum" |
|
|
|
|
|
:total="purchaseTotal" |
|
|
|
|
|
:page-size="purchasePageSize" |
|
|
|
|
|
@current-change="loadPurchases" |
|
|
|
|
|
/> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
<div v-show="tab === 'ledger'">账务流水占位</div> |
|
|
<div v-show="tab === 'ledger'">账务流水占位</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
@ -151,6 +222,77 @@ |
|
|
</t-button> |
|
|
</t-button> |
|
|
</template> |
|
|
</template> |
|
|
</t-dialog> |
|
|
</t-dialog> |
|
|
|
|
|
|
|
|
|
|
|
<t-dialog |
|
|
|
|
|
v-model:visible="purchaseModalVisible" |
|
|
|
|
|
header="录入采购" |
|
|
|
|
|
width="520px" |
|
|
|
|
|
destroy-on-close |
|
|
|
|
|
dialog-class-name="add-dock-dialog" |
|
|
|
|
|
:close-on-overlay-click="false" |
|
|
|
|
|
placement="center" |
|
|
|
|
|
> |
|
|
|
|
|
<form novalidate @submit.prevent="submitPurchase"> |
|
|
|
|
|
<label> |
|
|
|
|
|
<span>批次号<b>*</b></span> |
|
|
|
|
|
<t-input v-model="purchaseForm.batchNo" placeholder="唯一批次号" /> |
|
|
|
|
|
</label> |
|
|
|
|
|
<label> |
|
|
|
|
|
<span>供应商<b>*</b></span> |
|
|
|
|
|
<t-input v-model="purchaseForm.provider" placeholder="供应商名称" /> |
|
|
|
|
|
</label> |
|
|
|
|
|
<div class="form-row"> |
|
|
|
|
|
<label> |
|
|
|
|
|
<span>总量(GB)<b>*</b></span> |
|
|
|
|
|
<t-input-number |
|
|
|
|
|
v-model="purchaseForm.totalGb" |
|
|
|
|
|
theme="normal" |
|
|
|
|
|
:min="0.01" |
|
|
|
|
|
:step="1" |
|
|
|
|
|
style="width: 100%" |
|
|
|
|
|
/> |
|
|
|
|
|
</label> |
|
|
|
|
|
<label> |
|
|
|
|
|
<span>单价</span> |
|
|
|
|
|
<t-input-number |
|
|
|
|
|
v-model="purchaseForm.unitCost" |
|
|
|
|
|
theme="normal" |
|
|
|
|
|
:min="0" |
|
|
|
|
|
:step="0.01" |
|
|
|
|
|
style="width: 100%" |
|
|
|
|
|
/> |
|
|
|
|
|
</label> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="form-row"> |
|
|
|
|
|
<label> |
|
|
|
|
|
<span>总成本</span> |
|
|
|
|
|
<t-input-number |
|
|
|
|
|
v-model="purchaseForm.totalCost" |
|
|
|
|
|
theme="normal" |
|
|
|
|
|
:min="0" |
|
|
|
|
|
:step="0.01" |
|
|
|
|
|
style="width: 100%" |
|
|
|
|
|
/> |
|
|
|
|
|
</label> |
|
|
|
|
|
<label> |
|
|
|
|
|
<span>到期时间</span> |
|
|
|
|
|
<t-input |
|
|
|
|
|
v-model="purchaseForm.expiresAt" |
|
|
|
|
|
placeholder="YYYY-MM-DDTHH:mm:ss" |
|
|
|
|
|
/> |
|
|
|
|
|
</label> |
|
|
|
|
|
</div> |
|
|
|
|
|
<label> |
|
|
|
|
|
<span>备注</span> |
|
|
|
|
|
<t-input v-model="purchaseForm.remark" placeholder="可选备注" /> |
|
|
|
|
|
</label> |
|
|
|
|
|
<div class="form-error">{{ purchaseFormError }}</div> |
|
|
|
|
|
</form> |
|
|
|
|
|
<template #footer> |
|
|
|
|
|
<t-button variant="outline" @click="purchaseModalVisible = false">取消</t-button> |
|
|
|
|
|
<t-button theme="primary" @click="submitPurchase">确认录入</t-button> |
|
|
|
|
|
</template> |
|
|
|
|
|
</t-dialog> |
|
|
</section> |
|
|
</section> |
|
|
</template> |
|
|
</template> |
|
|
|
|
|
|
|
|
@ -337,8 +479,143 @@ async function disablePackage(row) { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const pool = ref(null) |
|
|
|
|
|
const poolMissing = ref(false) |
|
|
|
|
|
const purchaseList = ref([]) |
|
|
|
|
|
const purchaseTotal = ref(0) |
|
|
|
|
|
const purchasePageNum = ref(1) |
|
|
|
|
|
const purchasePageSize = ref(10) |
|
|
|
|
|
const purchaseProvider = ref('') |
|
|
|
|
|
const purchaseStatus = ref('') |
|
|
|
|
|
const purchaseModalVisible = ref(false) |
|
|
|
|
|
const purchaseFormError = ref('') |
|
|
|
|
|
const purchaseForm = reactive({ |
|
|
|
|
|
batchNo: '', |
|
|
|
|
|
provider: '', |
|
|
|
|
|
totalGb: 1, |
|
|
|
|
|
unitCost: 0, |
|
|
|
|
|
totalCost: 0, |
|
|
|
|
|
expiresAt: '', |
|
|
|
|
|
remark: '' |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
const purchaseStatusOptions = [ |
|
|
|
|
|
{ label: '全部状态', value: '' }, |
|
|
|
|
|
{ label: '有效', value: 'active' }, |
|
|
|
|
|
{ label: '失效', value: 'inactive' } |
|
|
|
|
|
] |
|
|
|
|
|
const purchaseColumns = [ |
|
|
|
|
|
{ colKey: 'batchNo', title: '批次号', width: '12%' }, |
|
|
|
|
|
{ colKey: 'provider', title: '供应商', width: '10%' }, |
|
|
|
|
|
{ colKey: 'totalText', title: '总量', width: '10%' }, |
|
|
|
|
|
{ colKey: 'costText', title: '成本', width: '10%' }, |
|
|
|
|
|
{ colKey: 'expiresAt', title: '到期', width: '14%' }, |
|
|
|
|
|
{ colKey: 'statusName', title: '状态', width: '8%' }, |
|
|
|
|
|
{ colKey: 'remark', title: '备注', width: '14%', ellipsis: true }, |
|
|
|
|
|
{ colKey: 'createdAt', title: '创建时间', width: '16%' } |
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
async function loadPool() { |
|
|
|
|
|
poolMissing.value = false |
|
|
|
|
|
try { |
|
|
|
|
|
pool.value = await request.get(urls.ADMIN_TRAFFIC_POOL) |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
if (e.status === 404 || /资源不存在|not found/i.test(e.message || '')) { |
|
|
|
|
|
pool.value = null |
|
|
|
|
|
poolMissing.value = true |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
ui.toast(e.message || '加载资源池失败') |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function loadPurchases() { |
|
|
|
|
|
try { |
|
|
|
|
|
const params = { pageNum: purchasePageNum.value, pageSize: purchasePageSize.value } |
|
|
|
|
|
if (purchaseProvider.value) params.provider = purchaseProvider.value |
|
|
|
|
|
if (purchaseStatus.value) params.status = purchaseStatus.value |
|
|
|
|
|
const page = await request.get(urls.ADMIN_TRAFFIC_PURCHASES, { params }) |
|
|
|
|
|
purchaseTotal.value = page?.total || 0 |
|
|
|
|
|
purchaseList.value = (page?.records || []).map((p) => ({ |
|
|
|
|
|
id: String(p.id), |
|
|
|
|
|
batchNo: p.batchNo || '—', |
|
|
|
|
|
provider: p.provider || '—', |
|
|
|
|
|
totalText: bytesToGbText(p.totalBytes), |
|
|
|
|
|
costText: `¥${Number(p.totalCost || 0).toFixed(2)}`, |
|
|
|
|
|
expiresAt: fmtTime(p.expiresAt), |
|
|
|
|
|
statusName: p.status === 'active' ? '有效' : (p.status || '—'), |
|
|
|
|
|
remark: p.remark || '', |
|
|
|
|
|
createdAt: fmtTime(p.createdAt), |
|
|
|
|
|
raw: p |
|
|
|
|
|
})) |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
ui.toast(e.message || '加载采购列表失败') |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function loadPurchasesTab() { |
|
|
|
|
|
await Promise.all([loadPool(), loadPurchases()]) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function reloadPurchases() { |
|
|
|
|
|
purchasePageNum.value = 1 |
|
|
|
|
|
loadPurchases() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function openPurchaseAdd() { |
|
|
|
|
|
Object.assign(purchaseForm, { |
|
|
|
|
|
batchNo: '', |
|
|
|
|
|
provider: '', |
|
|
|
|
|
totalGb: 1, |
|
|
|
|
|
unitCost: 0, |
|
|
|
|
|
totalCost: 0, |
|
|
|
|
|
expiresAt: '', |
|
|
|
|
|
remark: '' |
|
|
|
|
|
}) |
|
|
|
|
|
purchaseFormError.value = '' |
|
|
|
|
|
purchaseModalVisible.value = true |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function submitPurchase() { |
|
|
|
|
|
purchaseFormError.value = '' |
|
|
|
|
|
if (!purchaseForm.batchNo.trim() || !purchaseForm.provider.trim()) { |
|
|
|
|
|
purchaseFormError.value = '请填写批次号和供应商' |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
if (!(Number(purchaseForm.totalGb) > 0)) { |
|
|
|
|
|
purchaseFormError.value = '采购总量必须大于 0 GB' |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
const payload = { |
|
|
|
|
|
batchNo: purchaseForm.batchNo.trim(), |
|
|
|
|
|
provider: purchaseForm.provider.trim(), |
|
|
|
|
|
totalBytes: Math.round(Number(purchaseForm.totalGb) * GB_BYTES), |
|
|
|
|
|
unitCost: Number(purchaseForm.unitCost || 0), |
|
|
|
|
|
totalCost: Number(purchaseForm.totalCost || 0), |
|
|
|
|
|
remark: purchaseForm.remark || '' |
|
|
|
|
|
} |
|
|
|
|
|
if (purchaseForm.expiresAt) { |
|
|
|
|
|
const expires = new Date(purchaseForm.expiresAt) |
|
|
|
|
|
if (Number.isNaN(expires.getTime())) { |
|
|
|
|
|
purchaseFormError.value = '到期时间格式无效' |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
payload.expiresAt = expires.toISOString() |
|
|
|
|
|
} |
|
|
|
|
|
try { |
|
|
|
|
|
await request.post(urls.ADMIN_TRAFFIC_PURCHASES, payload) |
|
|
|
|
|
ui.toast('采购已录入') |
|
|
|
|
|
purchaseModalVisible.value = false |
|
|
|
|
|
await loadPurchasesTab() |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
purchaseFormError.value = e.message || '录入失败' |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
watch(tab, (v) => { |
|
|
watch(tab, (v) => { |
|
|
if (v === 'packages') loadPackages() |
|
|
if (v === 'packages') loadPackages() |
|
|
|
|
|
if (v === 'purchases') loadPurchasesTab() |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
onMounted(() => { |
|
|
onMounted(() => { |
|
|
@ -347,12 +624,37 @@ onMounted(() => { |
|
|
</script> |
|
|
</script> |
|
|
|
|
|
|
|
|
<style scoped> |
|
|
<style scoped> |
|
|
.packages-panel { |
|
|
|
|
|
|
|
|
.packages-panel, |
|
|
|
|
|
.purchases-panel { |
|
|
flex: 1; |
|
|
flex: 1; |
|
|
display: flex; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
flex-direction: column; |
|
|
min-height: 0; |
|
|
min-height: 0; |
|
|
} |
|
|
} |
|
|
|
|
|
.pool-metrics { |
|
|
|
|
|
margin: 12px 16px 0; |
|
|
|
|
|
grid-template-columns: repeat(3, 1fr); |
|
|
|
|
|
} |
|
|
|
|
|
.pool-empty { |
|
|
|
|
|
margin: 12px 16px 0; |
|
|
|
|
|
padding: 14px 16px; |
|
|
|
|
|
border: 1px dashed var(--line); |
|
|
|
|
|
border-radius: 6px; |
|
|
|
|
|
background: #fafbfc; |
|
|
|
|
|
color: var(--muted); |
|
|
|
|
|
font-size: 12px; |
|
|
|
|
|
} |
|
|
|
|
|
.asset-search { |
|
|
|
|
|
border: 0; |
|
|
|
|
|
padding: 0; |
|
|
|
|
|
height: auto; |
|
|
|
|
|
} |
|
|
|
|
|
.asset-search :deep(.t-input) { |
|
|
|
|
|
width: 100%; |
|
|
|
|
|
} |
|
|
|
|
|
.toolbar-group :deep(.t-select) { |
|
|
|
|
|
flex-shrink: 0; |
|
|
|
|
|
} |
|
|
.form-hint { |
|
|
.form-hint { |
|
|
margin-top: 4px; |
|
|
margin-top: 4px; |
|
|
color: var(--muted); |
|
|
color: var(--muted); |
|
|
|