Browse Source

feat: add traffic purchase intake tab

Show platform pool status and allow creating purchase batches.
main
xiaosi 3 weeks ago
parent
commit
ffbbd5f11b
  1. 306
      src/views/TrafficOpsView/TrafficOpsView.vue

306
src/views/TrafficOpsView/TrafficOpsView.vue

@ -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);

Loading…
Cancel
Save