Browse Source

feat: 账户中心页签与列表切换到 TDesign

main
xiaosi 3 weeks ago
parent
commit
02e5b80a9f
  1. 193
      src/views/AccountView/AccountView.vue

193
src/views/AccountView/AccountView.vue

@ -10,13 +10,13 @@
</div> </div>
<div class="business-workspace"> <div class="business-workspace">
<div class="business-tabs">
<button :class="{ active: tab === 'overview' }" @click="tab = 'overview'">概览</button>
<button :class="{ active: tab === 'traffic' }" @click="tab = 'traffic'">云媒体流量</button>
<button :class="{ active: tab === 'sim' }" @click="tab = 'sim'">通信卡</button>
<button :class="{ active: tab === 'security' }" @click="tab = 'security'">账号安全</button>
<button :class="{ active: tab === 'invoice' }" @click="tab = 'invoice'">发票管理</button>
</div>
<t-tabs v-model="tab" class="business-tdesign-tabs">
<t-tab-panel value="overview" label="概览" :destroy-on-hide="false" />
<t-tab-panel value="traffic" label="云媒体流量" :destroy-on-hide="false" />
<t-tab-panel value="sim" label="通信卡" :destroy-on-hide="false" />
<t-tab-panel value="security" label="账号安全" :destroy-on-hide="false" />
<t-tab-panel value="invoice" label="发票管理" :destroy-on-hide="false" />
</t-tabs>
<div class="business-panel account-panel" :class="{ active: tab === 'overview' }"> <div class="business-panel account-panel" :class="{ active: tab === 'overview' }">
<div class="account-overview-grid"> <div class="account-overview-grid">
@ -55,29 +55,102 @@
<div class="traffic-order-summary"><span>订单概要</span><strong>云媒体流量订单</strong><small>订单创建后由管理员人工确认金额以服务端订单为准</small><t-button theme="primary" @click="createOrder">创建订单</t-button></div> <div class="traffic-order-summary"><span>订单概要</span><strong>云媒体流量订单</strong><small>订单创建后由管理员人工确认金额以服务端订单为准</small><t-button theme="primary" @click="createOrder">创建订单</t-button></div>
</div> </div>
<div class="account-subsection-title"><h3>订单记录</h3><span> {{ orderTotal }} </span></div> <div class="account-subsection-title"><h3>订单记录</h3><span> {{ orderTotal }} </span></div>
<div class="account-order-table"><div class="order-head"><span>订单号</span><span>内容</span><span>单价</span><span>金额</span><span>状态</span><span>时间</span></div><div v-for="o in orders" :key="o.id"><span>{{ o.id }}</span><span>{{ o.amountGb }} GB 流量</span><span>{{ money(o.unitPrice) }}</span><strong>{{ money(o.totalPrice) }}</strong><em :class="{ processing: o.payStatus === 'unpaid' }">{{ payStatusName(o.payStatus) }}</em><span>{{ fmtTime(o.createdAt) }}</span></div></div>
<div v-if="!orders.length" class="table-empty">暂无流量订单</div>
<t-table row-key="id" :data="orders" :columns="orderColumns" empty="暂无流量订单">
<template #content="{ row }">{{ row.amountGb }} GB 流量</template>
<template #unitPrice="{ row }">{{ money(row.unitPrice) }}</template>
<template #totalPrice="{ row }"><strong>{{ money(row.totalPrice) }}</strong></template>
<template #payStatus="{ row }"><em :class="{ processing: row.payStatus === 'unpaid' }">{{ payStatusName(row.payStatus) }}</em></template>
<template #createdAt="{ row }">{{ fmtTime(row.createdAt) }}</template>
</t-table>
<div class="table-footer" v-if="orderTotal"><t-pagination v-model:current="orderPageNum" :total="orderTotal" :page-size="10" @current-change="loadOrders" /></div> <div class="table-footer" v-if="orderTotal"><t-pagination v-model:current="orderPageNum" :total="orderTotal" :page-size="10" @current-change="loadOrders" /></div>
<div class="account-subsection-title"><h3>消费明细</h3><span> {{ usageTotal }} </span></div> <div class="account-subsection-title"><h3>消费明细</h3><span> {{ usageTotal }} </span></div>
<div class="account-order-table usage-table"><div class="order-head"><span>来源</span><span>对象 ID</span><span>用量</span><span>扣减前</span><span>扣减后</span><span>时间</span></div><div v-for="u in usage" :key="u.id"><span>{{ sourceName(u.sourceType) }}</span><span>{{ u.sourceId || '--' }}</span><strong>{{ fmtGb(u.bytesUsed) }}</strong><span>{{ fmtGb(u.balanceBefore) }}</span><span>{{ fmtGb(u.balanceAfter) }}</span><span>{{ fmtTime(u.createdAt) }}</span></div></div>
<div v-if="!usage.length" class="table-empty">暂无流量消费记录</div>
<t-table row-key="id" :data="usage" :columns="usageColumns" empty="暂无流量消费记录">
<template #sourceType="{ row }">{{ sourceName(row.sourceType) }}</template>
<template #sourceId="{ row }">{{ row.sourceId || '--' }}</template>
<template #bytesUsed="{ row }"><strong>{{ fmtGb(row.bytesUsed) }}</strong></template>
<template #balanceBefore="{ row }">{{ fmtGb(row.balanceBefore) }}</template>
<template #balanceAfter="{ row }">{{ fmtGb(row.balanceAfter) }}</template>
<template #createdAt="{ row }">{{ fmtTime(row.createdAt) }}</template>
</t-table>
<div class="table-footer" v-if="usageTotal"><t-pagination v-model:current="usagePageNum" :total="usageTotal" :page-size="10" @current-change="loadUsage" /></div> <div class="table-footer" v-if="usageTotal"><t-pagination v-model:current="usagePageNum" :total="usageTotal" :page-size="10" @current-change="loadUsage" /></div>
<div class="account-subsection-title"><h3>下载记录</h3><span> {{ downloadTotal }} </span></div> <div class="account-subsection-title"><h3>下载记录</h3><span> {{ downloadTotal }} </span></div>
<div class="account-order-table"><div class="order-head"><span>文件</span><span>无人机</span><span>执行记录</span><span>下载用量</span><span>时间</span></div><div v-for="item in downloads" :key="item.id"><strong>{{ item.fileName || '--' }}</strong><span>{{ item.droneSn || '--' }}</span><span>{{ item.executionId || '--' }}</span><span>{{ fmtGb(item.bytes) }}</span><span>{{ fmtTime(item.createdAt) }}</span></div></div><div v-if="!downloads.length" class="table-empty">暂无下载记录</div>
<t-table row-key="id" :data="downloads" :columns="downloadColumns" empty="暂无下载记录">
<template #fileName="{ row }"><strong>{{ row.fileName || '--' }}</strong></template>
<template #droneSn="{ row }">{{ row.droneSn || '--' }}</template>
<template #executionId="{ row }">{{ row.executionId || '--' }}</template>
<template #bytes="{ row }">{{ fmtGb(row.bytes) }}</template>
<template #createdAt="{ row }">{{ fmtTime(row.createdAt) }}</template>
</t-table>
</div> </div>
<div class="business-panel account-panel" :class="{ active: tab === 'sim' }" style="padding:16px"> <div class="business-panel account-panel" :class="{ active: tab === 'sim' }" style="padding:16px">
<div class="communication-status-grid"><div><strong>{{ simTotal }}</strong><span>通信卡总数</span></div><div><strong>{{ activeSimCount }}</strong><span>正常</span></div><div><strong>{{ warnSimCount }}</strong><span>异常或未同步</span></div></div> <div class="communication-status-grid"><div><strong>{{ simTotal }}</strong><span>通信卡总数</span></div><div><strong>{{ activeSimCount }}</strong><span>正常</span></div><div><strong>{{ warnSimCount }}</strong><span>异常或未同步</span></div></div>
<div class="communication-table-wrap"><table class="communication-table"><thead><tr><th>运营商</th><th>ICCID / 号码</th><th>绑定机巢</th><th>套餐用量</th><th>状态</th><th>最后同步</th><th>操作</th></tr></thead><tbody><tr v-for="c in simCards" :key="c.id"><td><strong>{{ c.carrier || '--' }}</strong></td><td><strong>{{ c.iccid || '--' }}</strong><small>{{ c.phone || '--' }}</small></td><td>{{ c.dockId || '--' }}</td><td>{{ c.lastSyncAt ? `${Number(c.usedGb || 0).toFixed(1)} / ${c.planGb || 0} GB` : '暂无运营商同步数据' }}</td><td><span class="carrier-status" :class="simStatus(c).cls"><i></i>{{ simStatus(c).name }}</span></td><td>{{ fmtTime(c.lastSyncAt) }}</td><td><span>运营商通道未接入</span></td></tr></tbody></table><div v-if="!simCards.length" class="table-empty">暂无通信卡</div></div>
<t-table row-key="id" :data="simCards" :columns="simColumns" empty="暂无通信卡">
<template #carrier="{ row }"><strong>{{ row.carrier || '--' }}</strong></template>
<template #iccid="{ row }"><strong>{{ row.iccid || '--' }}</strong><small>{{ row.phone || '--' }}</small></template>
<template #dockId="{ row }">{{ row.dockId || '--' }}</template>
<template #usage="{ row }">{{ row.lastSyncAt ? `${Number(row.usedGb || 0).toFixed(1)} / ${row.planGb || 0} GB` : '暂无运营商同步数据' }}</template>
<template #status="{ row }"><span class="carrier-status" :class="simStatus(row).cls"><i></i>{{ simStatus(row).name }}</span></template>
<template #lastSyncAt="{ row }">{{ fmtTime(row.lastSyncAt) }}</template>
<template #actions><span>运营商通道未接入</span></template>
</t-table>
<div class="table-footer" v-if="simTotal"><t-pagination v-model:current="simPageNum" :total="simTotal" :page-size="10" @current-change="loadSim" /></div> <div class="table-footer" v-if="simTotal"><t-pagination v-model:current="simPageNum" :total="simTotal" :page-size="10" @current-change="loadSim" /></div>
<div class="account-subsection-title"><h3>通信卡充值记录</h3><span> {{ simRechargeTotal }} </span></div> <div class="account-subsection-title"><h3>通信卡充值记录</h3><span> {{ simRechargeTotal }} </span></div>
<div class="account-order-table sim-recharge-table"><div class="order-head"><span>通信卡</span><span>充值流量</span><span>金额</span><span>支付状态</span><span>充值状态</span><span>时间</span></div><div v-for="r in simRechargeLogs" :key="r.id"><span>{{ cardLabel(r.simCardId) }}</span><strong>{{ r.amountGb }} GB</strong><strong>{{ money(r.totalPrice) }}</strong><em>{{ payStatusName(r.payStatus) }}</em><em :class="{ processing: r.rechargeStatus === 'pending' }">{{ rechargeStatusName(r.rechargeStatus) }}</em><span>{{ fmtTime(r.createdAt) }}</span></div></div><div v-if="!simRechargeLogs.length" class="table-empty">暂无通信卡充值记录</div>
<t-table row-key="id" :data="simRechargeLogs" :columns="simRechargeColumns" empty="暂无通信卡充值记录">
<template #simCardId="{ row }">{{ cardLabel(row.simCardId) }}</template>
<template #amountGb="{ row }"><strong>{{ row.amountGb }} GB</strong></template>
<template #totalPrice="{ row }"><strong>{{ money(row.totalPrice) }}</strong></template>
<template #payStatus="{ row }"><em>{{ payStatusName(row.payStatus) }}</em></template>
<template #rechargeStatus="{ row }"><em :class="{ processing: row.rechargeStatus === 'pending' }">{{ rechargeStatusName(row.rechargeStatus) }}</em></template>
<template #createdAt="{ row }">{{ fmtTime(row.createdAt) }}</template>
</t-table>
<div class="table-footer" v-if="simRechargeTotal"><t-pagination v-model:current="simRechargePageNum" :total="simRechargeTotal" :page-size="10" @current-change="loadSim" /></div> <div class="table-footer" v-if="simRechargeTotal"><t-pagination v-model:current="simRechargePageNum" :total="simRechargeTotal" :page-size="10" @current-change="loadSim" /></div>
</div> </div>
<div class="business-panel account-panel" :class="{ active: tab === 'security' }" style="padding:18px"><div class="account-section-heading"><h2>账号安全</h2><span>修改密码后将退出所有设备</span></div><div class="security-list"><div><span><strong>登录手机号</strong><small>{{ maskPhone(profilePhone) }}</small></span></div><div><span><strong>绑定邮箱</strong><small>{{ profileEmail }}</small></span></div><div><span><strong>登录会话</strong><small> {{ sessions.length }} 个有效会话</small></span><t-button variant="outline" @click="revokeOthers">退出其他设备</t-button></div></div><div class="account-order-table"><div class="order-head"><span>设备</span><span>IP</span><span>最近活跃</span><span>过期时间</span><span>操作</span></div><div v-for="session in sessions" :key="session.id"><span>{{ session.userAgent || '未知设备' }}{{ session.current ? '(当前)' : '' }}</span><span>{{ session.ip || '--' }}</span><span>{{ fmtTime(session.lastActiveAt) }}</span><span>{{ fmtTime(session.expiresAt) }}</span><t-button v-if="!session.current" variant="text" theme="primary" @click="revokeSession(session.id)">退出</t-button><span v-else>当前会话</span></div></div><div v-if="!sessions.length" class="table-empty">暂无有效会话</div><div class="account-card-actions"><t-button theme="primary" @click="passwordOpen = true">修改密码</t-button></div></div>
<div class="business-panel account-panel" :class="{ active: tab === 'security' }" style="padding:18px">
<div class="account-section-heading"><h2>账号安全</h2><span>修改密码后将退出所有设备</span></div>
<div class="security-list">
<div><span><strong>登录手机号</strong><small>{{ maskPhone(profilePhone) }}</small></span></div>
<div><span><strong>绑定邮箱</strong><small>{{ profileEmail }}</small></span></div>
<div><span><strong>登录会话</strong><small> {{ sessions.length }} 个有效会话</small></span><t-button variant="outline" @click="revokeOthers">退出其他设备</t-button></div>
</div>
<t-table row-key="id" :data="sessions" :columns="sessionColumns" empty="暂无有效会话">
<template #device="{ row }">{{ row.userAgent || '未知设备' }}{{ row.current ? '(当前)' : '' }}</template>
<template #ip="{ row }">{{ row.ip || '--' }}</template>
<template #lastActiveAt="{ row }">{{ fmtTime(row.lastActiveAt) }}</template>
<template #expiresAt="{ row }">{{ fmtTime(row.expiresAt) }}</template>
<template #actions="{ row }">
<t-button v-if="!row.current" variant="text" theme="primary" @click="revokeSession(row.id)">退出</t-button>
<span v-else>当前会话</span>
</template>
</t-table>
<div class="account-card-actions"><t-button theme="primary" @click="passwordOpen = true">修改密码</t-button></div>
</div>
<div class="business-panel account-panel" :class="{ active: tab === 'invoice' }" style="padding:18px"><div class="account-section-heading"><h2>发票资料与申请</h2><span>仅对已人工确认的流量订单开放申请</span></div><div class="traffic-purchase-layout"><div class="traffic-amount-block"><label><span>发票抬头</span><t-input v-model.trim="invoiceForm.title" :maxlength="128" /></label><label><span>纳税人识别号</span><t-input v-model.trim="invoiceForm.taxpayerNumber" :maxlength="64" /></label><label><span>接收邮箱</span><t-input v-model.trim="invoiceForm.email" type="email" /></label><t-button theme="primary" @click="saveInvoiceProfile">保存默认发票资料</t-button></div><div class="traffic-order-summary"><span>可申请订单</span><t-select v-model="invoiceOrderId" :options="invoiceOrderOptions" style="width: 100%" /><t-button theme="primary" :disabled="!invoiceOrderId" @click="createInvoiceRequest">提交开具发票</t-button></div></div><div class="account-subsection-title"><h3>发票记录</h3><span>仅展示已提交的开具申请</span></div><div class="account-order-table"><div class="order-head"><span>订单号</span><span>金额</span><span>抬头</span><span>状态</span><span>申请时间</span></div><div v-for="request in invoiceRequests" :key="request.id"><span>{{ request.orderId }}</span><strong>{{ money(request.amount) }}</strong><span>{{ request.title }}</span><em :class="{ processing: request.status === 'pending' }">{{ invoiceStatusName(request.status) }}</em><span>{{ fmtTime(request.requestedAt) }}</span></div></div><div v-if="!invoiceRequests.length" class="table-empty">暂无发票申请</div></div>
<div class="business-panel account-panel" :class="{ active: tab === 'invoice' }" style="padding:18px">
<div class="account-section-heading"><h2>发票资料与申请</h2><span>仅对已人工确认的流量订单开放申请</span></div>
<div class="traffic-purchase-layout">
<div class="traffic-amount-block">
<label><span>发票抬头</span><t-input v-model.trim="invoiceForm.title" :maxlength="128" /></label>
<label><span>纳税人识别号</span><t-input v-model.trim="invoiceForm.taxpayerNumber" :maxlength="64" /></label>
<label><span>接收邮箱</span><t-input v-model.trim="invoiceForm.email" type="email" /></label>
<t-button theme="primary" @click="saveInvoiceProfile">保存默认发票资料</t-button>
</div>
<div class="traffic-order-summary">
<span>可申请订单</span>
<t-select v-model="invoiceOrderId" :options="invoiceOrderOptions" style="width: 100%" />
<t-button theme="primary" :disabled="!invoiceOrderId" @click="createInvoiceRequest">提交开具发票</t-button>
</div>
</div>
<div class="account-subsection-title"><h3>发票记录</h3><span>仅展示已提交的开具申请</span></div>
<t-table row-key="id" :data="invoiceRequests" :columns="invoiceColumns" empty="暂无发票申请">
<template #orderId="{ row }">{{ row.orderId }}</template>
<template #amount="{ row }"><strong>{{ money(row.amount) }}</strong></template>
<template #title="{ row }">{{ row.title }}</template>
<template #status="{ row }"><em :class="{ processing: row.status === 'pending' }">{{ invoiceStatusName(row.status) }}</em></template>
<template #requestedAt="{ row }">{{ fmtTime(row.requestedAt) }}</template>
</t-table>
</div>
</div> </div>
<t-dialog <t-dialog
@ -142,6 +215,61 @@ const simCards = ref([]); const simTotal = ref(0); const simPages = ref(1); cons
const simRechargeLogs = ref([]); const simRechargeTotal = ref(0); const simRechargePages = ref(1); const simRechargePageNum = ref(1) const simRechargeLogs = ref([]); const simRechargeTotal = ref(0); const simRechargePages = ref(1); const simRechargePageNum = ref(1)
const editOpen = ref(false); const saving = ref(false); const editForm = ref({ name: '', email: '' }) const editOpen = ref(false); const saving = ref(false); const editForm = ref({ name: '', email: '' })
const orderColumns = [
{ colKey: 'id', title: '订单号' },
{ colKey: 'content', title: '内容' },
{ colKey: 'unitPrice', title: '单价' },
{ colKey: 'totalPrice', title: '金额' },
{ colKey: 'payStatus', title: '状态' },
{ colKey: 'createdAt', title: '时间' }
]
const usageColumns = [
{ colKey: 'sourceType', title: '来源' },
{ colKey: 'sourceId', title: '对象 ID' },
{ colKey: 'bytesUsed', title: '用量' },
{ colKey: 'balanceBefore', title: '扣减前' },
{ colKey: 'balanceAfter', title: '扣减后' },
{ colKey: 'createdAt', title: '时间' }
]
const downloadColumns = [
{ colKey: 'fileName', title: '文件' },
{ colKey: 'droneSn', title: '无人机' },
{ colKey: 'executionId', title: '执行记录' },
{ colKey: 'bytes', title: '下载用量' },
{ colKey: 'createdAt', title: '时间' }
]
const simColumns = [
{ colKey: 'carrier', title: '运营商' },
{ colKey: 'iccid', title: 'ICCID / 号码' },
{ colKey: 'dockId', title: '绑定机巢' },
{ colKey: 'usage', title: '套餐用量' },
{ colKey: 'status', title: '状态' },
{ colKey: 'lastSyncAt', title: '最后同步' },
{ colKey: 'actions', title: '操作' }
]
const simRechargeColumns = [
{ colKey: 'simCardId', title: '通信卡' },
{ colKey: 'amountGb', title: '充值流量' },
{ colKey: 'totalPrice', title: '金额' },
{ colKey: 'payStatus', title: '支付状态' },
{ colKey: 'rechargeStatus', title: '充值状态' },
{ colKey: 'createdAt', title: '时间' }
]
const sessionColumns = [
{ colKey: 'device', title: '设备' },
{ colKey: 'ip', title: 'IP' },
{ colKey: 'lastActiveAt', title: '最近活跃' },
{ colKey: 'expiresAt', title: '过期时间' },
{ colKey: 'actions', title: '操作' }
]
const invoiceColumns = [
{ colKey: 'orderId', title: '订单号' },
{ colKey: 'amount', title: '金额' },
{ colKey: 'title', title: '抬头' },
{ colKey: 'status', title: '状态' },
{ colKey: 'requestedAt', title: '申请时间' }
]
const profileName = computed(() => profile.value?.name || '--') const profileName = computed(() => profile.value?.name || '--')
const profilePhone = computed(() => profile.value?.phone || '--') const profilePhone = computed(() => profile.value?.phone || '--')
const profileEmail = computed(() => profile.value?.email || '--') const profileEmail = computed(() => profile.value?.email || '--')
@ -153,6 +281,11 @@ const invoiceOrderOptions = computed(() => [{ label: '请选择已支付订单',
const activeSimCount = computed(() => simCards.value.filter((card) => simStatus(card).cls === 'normal').length) const activeSimCount = computed(() => simCards.value.filter((card) => simStatus(card).cls === 'normal').length)
const warnSimCount = computed(() => simTotal.value - activeSimCount.value) const warnSimCount = computed(() => simTotal.value - activeSimCount.value)
function applyPage(targetRef, page) {
const records = page?.records || []
targetRef.value = records
return { total: page?.total || 0, pages: page?.pages || 1 }
}
function fmtTime(value) { const date = new Date(value); return value && !Number.isNaN(date.getTime()) ? date.toLocaleString('zh-CN', { hour12: false }) : '--' } function fmtTime(value) { const date = new Date(value); return value && !Number.isNaN(date.getTime()) ? date.toLocaleString('zh-CN', { hour12: false }) : '--' }
function fmtGb(bytes) { return bytes == null ? '--' : `${(Number(bytes) / 1024 / 1024 / 1024).toFixed(2)} GB` } function fmtGb(bytes) { return bytes == null ? '--' : `${(Number(bytes) / 1024 / 1024 / 1024).toFixed(2)} GB` }
function money(value) { return value == null ? '--' : `¥${Number(value).toFixed(2)}` } function money(value) { return value == null ? '--' : `¥${Number(value).toFixed(2)}` }
@ -194,6 +327,32 @@ onMounted(load)
</script> </script>
<style scoped> <style scoped>
.business-tdesign-tabs :deep(.t-tabs__nav-container) {
padding: 0 18px;
}
.business-tdesign-tabs :deep(.t-tabs__nav-wrap) {
margin-left: 0;
}
.account-panel :deep(.t-table) {
background: transparent;
}
.account-panel :deep(em) {
font-style: normal;
color: var(--green);
font-size: 10px;
font-weight: 500;
}
.account-panel :deep(em.processing) {
color: #a46924;
}
.account-panel :deep(.t-table td small) {
display: block;
margin-top: 4px;
color: var(--muted);
font-size: 10px;
}
.traffic-amount-input { .traffic-amount-input {
height: 46px; height: 46px;
display: flex; display: flex;
@ -258,4 +417,4 @@ onMounted(load)
justify-content: flex-end; justify-content: flex-end;
gap: 8px; gap: 8px;
} }
</style>
</style>
Loading…
Cancel
Save