@ -1,17 +1,12 @@
< template >
< section class = "operation-page" >
< header class = "operation-page-header" >
< div >
< h1 > 账户中心 < / h1 >
< span > 管理云媒体流量 、 通信卡 、 订单与账号安全 < / span >
< / div >
< / header >
< header class = "operation-page-header" > < div > < h1 > 账户中心 < / h1 > < span > 管理云媒体流量 、 通信卡和账户资料 < / span > < / div > < / header >
< div class = "business-metric-band account-metrics" >
< div > < span > 企业 账户< / span > < strong > { { tenant Name } } < / strong > < small > 状态正常 < / small > < / div >
< div > < span > 账户 < / span > < strong > { { profileName } } < / strong > < small > { { accountStatus } } < / small > < / div >
< div > < span > 云媒体流量 < / span > < strong > { { balance } } < / strong > < small > GB < / small > < / div >
< div > < span > 通信卡 < / span > < strong > { { simCards . length } } < / strong > < small > 张 < / small > < / div >
< div > < span > 接入设备 < / span > < strong > { { deviceCount } } < / strong > < small > 机巢 + 无人机 < / small > < / div >
< div > < span > 通信卡 < / span > < strong > { { simTotal } } < / strong > < small > 张 < / small > < / div >
< div > < span > 接入设备 < / span > < strong > { { deviceCount } } < / strong > < small > { { deviceLoadFailed ? '加载失败' : '机巢 + 无人机' } } < / small > < / div >
< / div >
< div class = "business-workspace" >
@ -20,395 +15,157 @@
< 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 >
<!-- 概览 -- >
< div class = "business-panel account-panel" : class = "{ active: tab === 'overview' }" >
< div class = "account-overview-grid" >
< section class = "account-info-block" >
< div class = "account-section-heading" > < h2 > 企业信息 < / h2 > < span > 入驻信息 < / span > < / div >
< div class = "account-section-heading" > < h2 > 账户资料 < / h2 > < span > 以账户资料为准 < / span > < / div >
< div class = "tenant-identity" >
< span > { { tenant Initial } } < / span >
< div > < strong > { { tenant Name } } < / strong > < small > 企业账号 · { { tenant Role } } < / small > < / div >
< button class = "table-text-action" @click ="ui.toast('编辑企业信息')" > 编辑 < / button >
< span > { { profile Initial } } < / span >
< div > < strong > { { profile Name } } < / strong > < small > 账户角色 · { { profile Role } } < / small > < / div >
< button class = "table-text-action" @ click = "editOpen = true" > 编辑 < / button >
< / div >
< dl class = "account-details" >
< div > < dt > 联系人 < / dt > < dd > { { profileName } } < / dd > < / div >
< div > < dt > 账户名称 < / dt > < dd > { { profileName } } < / dd > < / div >
< div > < dt > 联系电话 < / dt > < dd > { { profilePhone } } < / dd > < / div >
< div > < dt > 企业 邮箱< / dt > < dd > { { profileEmail } } < / dd > < / div >
< div > < dt > 入驻 时间< / dt > < dd > { { createdAt } } < / dd > < / div >
< div > < dt > 邮箱 < / dt > < dd > { { profileEmail } } < / dd > < / div >
< div > < dt > 注册 时间< / dt > < dd > { { createdAt } } < / dd > < / div >
< / dl >
< / section >
< section class = "account-info-block" >
< div class = "account-section-heading" > < h2 > 资源概览 < / h2 > < span > 云媒体流量 · 通信卡 < / span > < / div >
< div class = "resource-progress" >
< div > < span > 云媒体流量 < b > { { balance } } GB < / b > < / span > < i > < em style = "width:42%" > < / em > < / i > < / div >
< div > < span > 通信卡 < b > { { activeSimCount } } 张正常 < / b > < / span > < i > < em : style = "{ width: simUsagePct + '%' }" > < / em > < / i > < / div >
< div > < span > 设备接入 < b > { { deviceCount } } 台 < / b > < / span > < i > < em style = "width:35%" > < / em > < / i > < / div >
< / div >
< div class = "account-card-actions" >
< button class = "primary-command" @ click = "tab = 'traffic'" > 购买流量 < / button >
< button class = "plain-command" @ click = "tab = 'sim'" > 管理通信卡 < / button >
< / div >
< div class = "account-section-heading" > < h2 > 资源概览 < / h2 > < span > 实时账户数据 < / span > < / div >
< div class = "resource-progress resource-values" >
< div > < span > 云媒体流量 < b > { { balance } } GB < / b > < / span > < / div >
< div > < span > 通信卡 < b > { { activeSimCount } } 张正常 / { { simTotal } } 张 < / b > < / span > < / div >
< div > < span > 接入设备 < b > { { resourceSummary ? ` ${ resourceSummary . dockCount } 个机巢 / ${ resourceSummary . droneCount } 架无人机 ` : deviceCount } } < / b > < / span > < / div >
< div > < span > 视频资产 < b > { { resourceSummary ? . videoCount ? ? '--' } } 个 / 本月下载 { { resourceSummary ? . monthDownloadCount ? ? '--' } } 次 < / b > < / span > < / div >
< div > < span > 本月媒体流量 < b > { { resourceSummary ? ` ${ Number ( resourceSummary . monthUsageGb ) . toFixed ( 2 ) } GB ` : '--' } } < / b > < / span > < / div >
< / div >
< div class = "account-card-actions" > < button class = "primary-command" @ click = "tab = 'traffic'" > 查看流量订单 < / button > < button class = "plain-command" @ click = "tab = 'sim'" > 管理通信卡 < / button > < / div >
< / section >
< / div >
< / div >
<!-- 云媒体流量 -- >
< div class = "business-panel account-panel" : class = "{ active: tab === 'traffic' }" style = "padding:18px" >
< div class = "traffic-balance-card" >
< span > 当前云媒体流量余额 < / span >
< strong > { { balance } } < small > GB < / small > < / strong >
< p > 流量用于直播推流 、 视频回放与原始素材下载 < / p >
< / div >
< div class = "traffic-balance-card" > < span > 当前云媒体流量余额 < / span > < strong > { { balance } } < small > GB < / small > < / strong > < p > 流量用于直播 、 回放与原始素材下载 < / p > < / div >
< div class = "traffic-purchase-layout" style = "margin-top:16px" >
< div class = "traffic-amount-block" >
< label > < span > 充值流量 ( GB ) < / span > < div > < input v -model = " trafficAmount " type = "number" min = "1" / > < b > GB < / b > < / div > < / label >
< div class = "traffic-quick-amounts" >
< button v-for ="a in [10, 50, 100, 500]" :key="a" :class="{ active: trafficAmount == a }" @click="trafficAmount = a" > {{ a }} GB < / button >
< / div >
< / div >
< div class = "traffic-order-summary" >
< span > 订单概要 < / span >
< strong > 云媒体流量充值 < / strong >
< small > 单价 < / small >
< b > ¥ { { ( trafficAmount * 0.8 ) . toFixed ( 2 ) } } < / b >
< small > 含税 , 充值后立即到账 < / small >
< button class = "primary-command" @click ="createOrder" > 立即下单 < / 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 > < span > 时间 < / span > < / div >
< div v-for ="o in orders" :key="o.id"><span>{{ o.id }}</span><strong>{{ o.type }}</strong><span>{{ o.desc }}</span><strong>¥{{ o.amount }}</strong><em :class="{ processing: o._processing }" > {{ o.status }} < / em > < span > { { o . time } } < / span > < / div >
< / div >
< div class = "account-subsection-title" > < h3 > 消费明细 < / h3 > < span > 直播 / 回放 / 下载 < / 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 > { { u . source } } < / span > < span > { { u . sourceId } } < / span > < strong > { { u . bytes } } < / strong > < span > { { u . before } } < / span > < span > { { u . after } } < / span > < span > { { u . time } } < / span > < / div >
< div class = "traffic-amount-block" > < label > < span > 购买流量 ( GB ) < / span > < div > < input v -model .number = " trafficAmount " type = "number" min = "1" / > < b > GB < / b > < / div > < / label > < / div >
< div class = "traffic-order-summary" > < span > 订单概要 < / span > < strong > 云媒体流量订单 < / strong > < small > 订单创建后由管理员人工确认 , 金额以服务端订单为准 。 < / small > < button class = "primary-command" @click ="createOrder" > 创建订单 < / button > < / div >
< / 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 >
< div class = "table-footer" v-if ="orderTotal" > < span > 第 { { orderPageNum } } / { { orderPages } } 页 < / span > < div > < button @ click = "goOrderPage(orderPageNum - 1)" > 上一页 < / button > < button @ click = "goOrderPage(orderPageNum + 1)" > 下一页 < / button > < / div > < / 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 >
< div class = "table-footer" v-if ="usageTotal" > < span > 第 { { usagePageNum } } / { { usagePages } } 页 < / span > < div > < button @ click = "goUsagePage(usagePageNum - 1)" > 上一页 < / button > < button @ click = "goUsagePage(usagePageNum + 1)" > 下一页 < / button > < / div > < / 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 >
< / div >
<!-- 通信卡 -- >
< div class = "business-panel account-panel" : class = "{ active: tab === 'sim' }" style = "padding:16px" >
< div class = "communication-status-grid" >
< div > < strong > { { simCards . length } } < / strong > < span > 通信卡总数 < / span > < / div >
< div > < strong > { { activeSimCount } } < / strong > < span > 正常 < / span > < / div >
< div > < strong > { { warnSimCount } } < / strong > < span > 异常 < / span > < / 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 >
< div class = "table-footer" v-if ="simTotal" > < span > 第 { { simPageNum } } / { { simPages } } 页 < / span > < div > < button @ click = "goSimPage(simPageNum - 1)" > 上一页 < / button > < button @ click = "goSimPage(simPageNum + 1)" > 下一页 < / button > < / div > < / 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 >
< div class = "table-footer" v-if ="simRechargeTotal" > < span > 第 { { simRechargePageNum } } / { { simRechargePages } } 页 < / span > < div > < button @ click = "goSimRechargePage(simRechargePageNum - 1)" > 上一页 < / button > < button @ click = "goSimRechargePage(simRechargePageNum + 1)" > 下一页 < / button > < / div > < / 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 >
< div class = "communication-usage" : class = "{ warning: c.usagePct > 80, danger: c.usagePct > 95 }" >
< span > { { c . usedGb } } / { { c . planGb } } GB < / span >
< i > < em : style = "{ width: c.usagePct + '%' }" > < / em > < / i >
< / div >
< / td >
< td >
< span class = "carrier-status" :class ="c.statusClass" > < i > < / i > { { c . carrierStatusName } } < / span >
< / td >
< td > { { c . expiredAt } } < / td >
< td > < button @click ="openRecharge(c)" > 充值 < / button > < / td >
< / tr >
< / tbody >
< / table >
< div v-if ="!simCards.length" class="table-empty" > 暂无通信卡 < / 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 > < button class = "plain-command" @click ="revokeOthers" > 退出其他设备 < / 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><button v-if="!session.current" class="table-text-action" @click="revokeSession(session.id)">退出</button><span v-else>当前会话</span></div></div><div v-if="!sessions.length" class="table-empty">暂无有效会话</div><div class="account-card-actions"><button class="primary-command" @click="passwordOpen = true" > 修改密码 < / button > < / div > < / div >
< div class = "account-subsection-title" > < h3 > 通信卡充值记录 < / h3 > < span > 本账户 SIM 卡套餐充值 < / span > < / div >
< div class = "asset-toolbar sim-recharge-toolbar" >
< div class = "toolbar-group" >
< select v-model ="simRechargeCardFilter" @change="reloadSimRechargeLogs" >
< option value = "" > 全部通信卡 < / option >
< option v-for ="c in simCards" :key="c.id" :value="c.id" > {{ c.carrier }} · {{ c.iccid }} < / option >
< / select >
< / div >
< / 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 > { { rechargeCardLabel ( r . simCardId ) } } < / span >
< strong > { { r . amountGb } } GB < / strong >
< strong > ¥ { { Number ( r . totalPrice ? ? 0 ) . toFixed ( 2 ) } } < / strong >
< em : class = "{ processing: r.payStatus !== 'paid' }" > { { payStatusName ( r . payStatus ) } } < / em >
< em : class = "{ processing: r.rechargeStatus !== 'success' }" > { { rechargeStatusName ( r . rechargeStatus ) } } < / em >
< span > { { fmtTime ( r . createdAt ) } } < / span >
< / div >
< / div >
< div v-if ="!simRechargeLogs.length" class="table-empty" > 暂无通信卡充值记录 < / div >
< div v-if ="simRechargeLogs.length" class="table-footer" >
< span > 共 { { simRechargeTotal } } 条记录 · 第 { { simRechargePageNum } } / { { simRechargePages } } 页 < / span >
< div >
< button @ click = "goSimRechargePage(simRechargePageNum - 1)" > < svg > < use href = "#i-chevron" / > < / svg > < / button >
< b > { { simRechargePageNum } } < / b >
< button @ click = "goSimRechargePage(simRechargePageNum + 1)" > < svg > < use href = "#i-chevron" / > < / svg > < / 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 > < input v -model .trim = " invoiceForm.title " maxlength = "128" / > < / label > < label > < span > 纳税人识别号 < / span > < input v -model .trim = " invoiceForm.taxpayerNumber " maxlength = "64" / > < / label > < label > < span > 接收邮箱 < / span > < input v -model .trim = " invoiceForm.email " type = "email" / > < / label > < button class = "primary-command" @click ="saveInvoiceProfile" > 保存默认发票资料 < / button > < / div > < div class = "traffic-order-summary" > < span > 可申请订单 < / span > < select v -model .number = " invoiceOrderId " > < option :value ="0" > 请选择已支付订单 < / option > < option v-for ="order in invoiceOrders" :key="order.id" :value="order.id">{{ order.id }} · {{ money(order.totalPrice) }}</option></select><button class="primary-command" :disabled="!invoiceOrderId" @click="createInvoiceRequest">提交人工申请</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 >
<!-- 账号安全 -- >
< 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" >
< button @click ="ui.toast('修改密码')" > < span > < strong > 登录密码 < / strong > < small > 定期更新可提升账号安全性 < / small > < / span > < b > 修改 < / b > < / button >
< button @click ="ui.toast('绑定手机号')" > < span > < strong > 绑定手机号 < / strong > < small > { { maskPhone ( profilePhone ) } } < / small > < / span > < b > 更换 < / b > < / button >
< button @click ="ui.toast('绑定邮箱')" > < span > < strong > 绑定邮箱 < / strong > < small > { { profileEmail } } < / small > < / span > < b > 更换 < / b > < / button >
< / div >
< / div >
< / div >
<!-- 通信卡充值弹窗 -- >
< div class = "modal-backdrop" : class = "{ open: rechargeOpen }" @ click.self = " rechargeOpen = false " >
< div class = "recharge-modal" >
< h3 > 通信卡充值 < / h3 >
< p class = "recharge-sub" v-if ="rechargeCard" > {{ rechargeCard.carrier }} · {{ rechargeCard.iccid }} · 当前套餐 {{ rechargeCard.planGb }} GB < / p >
< div class = "carrier-package-grid" >
< button v-for ="a in [10, 50, 100, 500]" :key="a" :class="{ active: rechargeAmount === a }" @click="rechargeAmount = a" >
< strong > { { a } } GB < / strong > < small > 流量包 < / small >
< / button >
< / div >
< div class = "carrier-recharge-total" >
< span > 合计 < / span >
< strong > ¥ { { ( rechargeAmount * 0.8 ) . toFixed ( 2 ) } } < / strong >
< / div >
< div class = "recharge-modal-footer" >
< button class = "cancel" @ click = "rechargeOpen = false" > 取消 < / button >
< button class = "confirm" :disabled ="recharging" @click ="submitRecharge" > {{ recharging ? ' 充值中 … ' : ' 确认充值 ' }} < / button >
< / div >
< / div >
< / div >
< div class = "modal-backdrop" : class = "{ open: editOpen }" @ click.self = " editOpen = false " > < div class = "recharge-modal" > < h3 > 编辑账户资料 < / h3 > < label > 账户名称 < input v -model .trim = " editForm.name " maxlength = "32" / > < / label > < label > 邮箱 < input v -model .trim = " editForm.email " type = "email" / > < / label > < div class = "recharge-modal-footer" > < button class = "cancel" @ click = "editOpen = false" > 取消 < / button > < button class = "confirm" :disabled ="saving" @click ="saveProfile" > {{ saving ? ' 保存中 … ' : ' 保存 ' }} < / button > < / div > < / div > < / div >
< div class = "modal-backdrop" : class = "{ open: passwordOpen }" @ click.self = " passwordOpen = false " > < div class = "recharge-modal" > < h3 > 修改密码 < / h3 > < label > 当前密码 < input v-model ="passwordForm.oldPassword" type="password" /></label><label>新密码<input v-model="passwordForm.newPassword" type="password" minlength="6" /></label><div class="recharge-modal-footer"><button class="cancel" @click="passwordOpen = false">取消</button><button class="confirm" @click="changePassword" > 确认修改 < / button > < / div > < / div > < / div >
< / section >
< / template >
< script setup >
import { computed , onMounted , ref } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/modules/userStore'
import { useUiStore } from '@/stores/modules/uiStore'
import request from '@/utils/http'
import { useDeviceStore } from '@/stores/modules/devicesStore'
import request from '@/utils/http'
import * as urls from '@/config/urls'
const userStore = useUserStore ( )
const router = useRouter ( )
const ui = useUiStore ( )
const deviceStore = useDeviceStore ( )
const tab = ref ( 'overview' )
const trafficAmount = ref ( 50 )
const tenantName = computed ( ( ) => userStore . user ? . name || '嘉谷科技' )
const profileName = computed ( ( ) => userStore . user ? . name || '平台管理员' )
const profilePhone = computed ( ( ) => userStore . user ? . phone || '--' )
const profileEmail = computed ( ( ) => userStore . user ? . email || '--' )
const tenantRole = computed ( ( ) => ( userStore . isAdmin ( ) ? '管理员' : '成员' ) )
const createdAt = computed ( ( ) => ( userStore . user ? . createdAt ? String ( userStore . user . createdAt ) . slice ( 0 , 10 ) : '--' ) )
const tenantInitial = computed ( ( ) => tenantName . value . charAt ( 0 ) )
const profile = ref ( null )
const balance = ref ( '--' )
const orders = ref ( [ ] )
const usage = ref ( [ ] )
const simCards = ref ( [ ] )
const simRechargeLogs = ref ( [ ] )
const simRechargeTotal = ref ( 0 )
const simRechargePages = ref ( 1 )
const simRechargePageNum = ref ( 1 )
const simRechargeCardFilter = ref ( '' )
const deviceCount = ref ( 0 )
const activeSimCount = computed ( ( ) => simCards . value . filter ( ( c ) => c . statusClass === 'normal' ) . length )
const warnSimCount = computed ( ( ) => simCards . value . length - activeSimCount . value )
const simUsagePct = computed ( ( ) => ( simCards . value . length ? Math . round ( ( activeSimCount . value / simCards . value . length ) * 100 ) : 0 ) )
const rechargeOpen = ref ( false )
const rechargeCard = ref ( null )
const rechargeAmount = ref ( 50 )
const recharging = ref ( false )
const PAY_STATUS = {
unpaid : { status : '待支付' , processing : true } ,
paid : { status : '已完成' , processing : false }
}
const SOURCE_NAME = { live : '直播' , replay : '回放' , download : '下载' }
const CARRIER_STATUS = {
normal : { name : '正常' , cls : 'normal' } ,
suspended : { name : '停机' , cls : 'danger' } ,
arrears : { name : '欠费' , cls : 'danger' } ,
cancelled : { name : '已注销' , cls : 'owned' }
}
function fmtTime ( t ) {
if ( ! t ) return '--'
const d = new Date ( t )
if ( Number . isNaN ( d . getTime ( ) ) ) return '--'
return d . toLocaleString ( 'zh-CN' , { hour12 : false } )
}
function fmtGb ( bytes ) {
if ( bytes == null ) return '--'
return ( Number ( bytes ) / 1024 / 1024 / 1024 ) . toFixed ( 2 ) + ' GB'
}
const deviceCount = ref ( '--' )
const deviceLoadFailed = ref ( false )
const resourceSummary = ref ( null )
const downloads = ref ( [ ] ) ; const downloadTotal = ref ( 0 )
const trafficAmount = ref ( 1 )
const sessions = ref ( [ ] )
const invoiceProfiles = ref ( [ ] ) ; const invoiceOrders = ref ( [ ] ) ; const invoiceRequests = ref ( [ ] ) ; const invoiceOrderId = ref ( 0 )
const invoiceForm = ref ( { title : '' , taxpayerNumber : '' , email : '' , isDefault : true } )
const passwordOpen = ref ( false ) ; const passwordForm = ref ( { oldPassword : '' , newPassword : '' } )
const orders = ref ( [ ] ) ; const orderTotal = ref ( 0 ) ; const orderPages = ref ( 1 ) ; const orderPageNum = ref ( 1 )
const usage = ref ( [ ] ) ; const usageTotal = ref ( 0 ) ; const usagePages = ref ( 1 ) ; const usagePageNum = ref ( 1 )
const simCards = ref ( [ ] ) ; const simTotal = ref ( 0 ) ; const simPages = ref ( 1 ) ; const simPageNum = 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 profileName = computed ( ( ) => profile . value ? . name || '--' )
const profilePhone = computed ( ( ) => profile . value ? . phone || '--' )
const profileEmail = computed ( ( ) => profile . value ? . email || '--' )
const profileRole = computed ( ( ) => profile . value ? . role === 'admin' ? '管理员' : profile . value ? . role === 'user' ? '普通用户' : '--' )
const accountStatus = computed ( ( ) => profile . value ? . status === 1 ? '正常' : profile . value ? . status === 0 ? '已禁用' : '--' )
const createdAt = computed ( ( ) => profile . value ? . createdAt ? fmtTime ( profile . value . createdAt ) : '--' )
const profileInitial = computed ( ( ) => profileName . value === '--' ? '-' : profileName . value . charAt ( 0 ) )
const activeSimCount = computed ( ( ) => simCards . value . filter ( ( card ) => simStatus ( card ) . cls === 'normal' ) . length )
const warnSimCount = computed ( ( ) => simTotal . value - activeSimCount . value )
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 money ( value ) { return value == null ? '--' : ` ¥ ${ Number ( value ) . toFixed ( 2 ) } ` }
function sourceName ( type ) { return ( { live : '直播' , replay : '回放' , download : '下载' } ) [ type ] || type || '--' }
function payStatusName ( status ) { return ( { unpaid : '待人工确认' , paid : '已入账' } ) [ status ] || status || '--' }
function rechargeStatusName ( status ) { return ( { pending : '处理中' , success : '充值成功' , failed : '充值失败' } ) [ status ] || status || '--' }
function invoiceStatusName ( status ) { return ( { pending : '待人工处理' , issued : '已开具' , rejected : '已驳回' , cancelled : '已取消' } ) [ status ] || status || '--' }
function maskPhone ( phone ) { const value = String ( phone || '' ) ; return value . length > 7 ? ` ${ value . slice ( 0 , 3 ) } **** ${ value . slice ( - 4 ) } ` : value || '--' }
function simStatus ( card ) { if ( card . status === 'expired' ) return { name : '已过期' , cls : 'danger' } ; return ( { normal : { name : '正常' , cls : 'normal' } , suspended : { name : '停机' , cls : 'danger' } , arrears : { name : '欠费' , cls : 'danger' } , cancelled : { name : '已注销' , cls : 'owned' } } ) [ card . carrierStatus ] || { name : card . lastSyncAt ? '状态未知' : '未同步' , cls : 'owned' } }
function cardLabel ( id ) { const card = simCards . value . find ( ( item ) => String ( item . id ) === String ( id ) ) ; return card ? ` ${ card . carrier || '--' } · ${ card . iccid || '--' } ` : String ( id || '--' ) }
async function loadProfile ( ) { profile . value = await request . get ( urls . ACCOUNT_PROFILE ) ; editForm . value = { name : profile . value ? . name || '' , email : profile . value ? . email || '' } ; userStore . user = { ... ( userStore . user || { } ) , ... profile . value } ; localStorage . setItem ( 'user' , JSON . stringify ( userStore . user ) ) }
async function loadAccountExtensions ( ) { const results = await Promise . allSettled ( [ request . get ( urls . ACCOUNT_RESOURCE_SUMMARY ) . then ( ( data ) => { resourceSummary . value = data } ) , request . get ( urls . ACCOUNT_DOWNLOADS , { params : { pageNum : 1 , pageSize : 10 } } ) . then ( ( data ) => { downloads . value = data ? . records || [ ] ; downloadTotal . value = data ? . total || 0 } ) , request . get ( urls . ACCOUNT_SESSIONS ) . then ( ( data ) => { sessions . value = data || [ ] } ) , request . get ( urls . INVOICE_PROFILES ) . then ( ( data ) => { invoiceProfiles . value = data || [ ] ; const profile = invoiceProfiles . value . find ( ( item ) => item . isDefault ) || invoiceProfiles . value [ 0 ] ; if ( profile ) invoiceForm . value = { ... profile , isDefault : true } } ) , request . get ( urls . INVOICE_ORDERS ) . then ( ( data ) => { invoiceOrders . value = data || [ ] } ) , request . get ( urls . INVOICE_REQUESTS ) . then ( ( data ) => { invoiceRequests . value = data || [ ] } ) ] ) ; if ( results . some ( ( result ) => result . status === 'rejected' ) ) ui . toast ( '部分账户扩展数据加载失败' ) }
async function changePassword ( ) { if ( ! passwordForm . value . oldPassword || passwordForm . value . newPassword . length < 6 ) { ui . toast ( '请填写正确的密码' ) ; return } try { await request . post ( urls . ACCOUNT_CHANGE_PASSWORD , passwordForm . value ) ; userStore . logout ( ) ; router . push ( '/login' ) } catch { } }
async function revokeSession ( id ) { try { await request . delete ( ` ${ urls . ACCOUNT_SESSIONS } / ${ id } ` ) ; await loadAccountExtensions ( ) } catch { } }
async function revokeOthers ( ) { try { await request . delete ( urls . ACCOUNT_SESSIONS ) ; await loadAccountExtensions ( ) } catch { } }
async function saveInvoiceProfile ( ) { try { await request . post ( urls . INVOICE_PROFILES , invoiceForm . value ) ; ui . toast ( '默认发票资料已保存' ) ; await loadAccountExtensions ( ) } catch { } }
async function createInvoiceRequest ( ) { try { await request . post ( urls . INVOICE_REQUESTS , { orderId : invoiceOrderId . value } ) ; ui . toast ( '发票申请已提交,等待人工处理' ) ; invoiceOrderId . value = 0 ; await loadAccountExtensions ( ) } catch { } }
async function loadOrders ( ) { try { const page = await request . get ( urls . TRAFFIC_ORDERS , { params : { pageNum : orderPageNum . value , pageSize : 10 } } ) ; const meta = applyPage ( orders , page ) ; orderTotal . value = meta . total ; orderPages . value = meta . pages } catch { orders . value = [ ] } }
async function loadUsage ( ) { try { const page = await request . get ( urls . TRAFFIC_USAGE , { params : { pageNum : usagePageNum . value , pageSize : 10 } } ) ; const meta = applyPage ( usage , page ) ; usageTotal . value = meta . total ; usagePages . value = meta . pages } catch { usage . value = [ ] } }
async function loadSim ( ) { const [ cards , logs ] = await Promise . all ( [ request . get ( urls . SIM_CARDS , { params : { pageNum : simPageNum . value , pageSize : 10 } } ) , request . get ( urls . SIM_RECHARGE_LOGS , { params : { pageNum : simRechargePageNum . value , pageSize : 10 } } ) ] ) ; const cardMeta = applyPage ( simCards , cards ) ; const logMeta = applyPage ( simRechargeLogs , logs ) ; simTotal . value = cardMeta . total ; simPages . value = cardMeta . pages ; simRechargeTotal . value = logMeta . total ; simRechargePages . value = logMeta . pages }
async function loadDeviceCount ( ) { try { await deviceStore . load ( ) ; deviceCount . value = deviceStore . docks . length + deviceStore . drones . length } catch { deviceLoadFailed . value = true } }
async function load ( ) {
try {
await deviceStore . load ( )
deviceCount . value = deviceStore . docks . length + deviceStore . drones . length
const simRechargeParams = { pageNum : simRechargePageNum . value , pageSize : 10 }
if ( simRechargeCardFilter . value ) simRechargeParams . simCardId = simRechargeCardFilter . value
const [ bal , orderPage , usagePage , simPage , simRechargePage ] = await Promise . all ( [
request . get ( urls . TRAFFIC_BALANCE ) ,
request . get ( urls . TRAFFIC_ORDERS , { params : { pageNum : 1 , pageSize : 100 } } ) ,
request . get ( urls . TRAFFIC_USAGE , { params : { pageNum : 1 , pageSize : 100 } } ) ,
request . get ( urls . SIM_CARDS , { params : { pageNum : 1 , pageSize : 100 } } ) ,
request . get ( urls . SIM_RECHARGE_LOGS , { params : simRechargeParams } )
const results = await Promise . allSettled ( [
loadProfile ( ) ,
request . get ( urls . TRAFFIC_BALANCE ) . then ( ( data ) => { balance . value = data ? . balanceGb == null ? '--' : Number ( data . balanceGb ) . toFixed ( 1 ) } ) ,
loadOrders ( ) ,
loadUsage ( ) ,
loadSim ( ) ,
loadDeviceCount ( ) ,
loadAccountExtensions ( )
] )
if ( bal ? . balanceGb != null ) balance . value = Number ( bal . balanceGb ) . toFixed ( 1 )
else balance . value = Number ( userStore . user ? . trafficBalance ? ? 0 ) . toFixed ( 1 )
orders . value = ( orderPage ? . records || [ ] ) . map ( ( o ) => {
const st = PAY_STATUS [ o . payStatus ] || { status : o . payStatus , processing : false }
return {
id : String ( o . id ) ,
type : '云媒体流量' ,
desc : ` ${ o . amountGb } GB 流量 ` ,
amount : Number ( o . totalPrice ? ? 0 ) . toFixed ( 2 ) ,
status : st . status ,
_processing : st . processing ,
time : fmtTime ( o . createdAt )
}
} )
usage . value = ( usagePage ? . records || [ ] ) . map ( ( u ) => ( {
id : String ( u . id ) ,
source : SOURCE_NAME [ u . sourceType ] || u . sourceType || '--' ,
sourceId : String ( u . sourceId ? ? '--' ) ,
bytes : fmtGb ( u . bytesUsed ) ,
before : fmtGb ( u . balanceBefore ) ,
after : fmtGb ( u . balanceAfter ) ,
time : fmtTime ( u . createdAt )
} ) )
simCards . value = ( simPage ? . records || [ ] ) . map ( ( c ) => {
const planGb = Number ( c . planGb ? ? 0 )
const usedGb = Number ( c . usedGb ? ? 0 )
const usagePct = planGb > 0 ? Math . min ( ( usedGb / planGb ) * 100 , 100 ) : 0
const st = CARRIER_STATUS [ c . carrierStatus ] || { name : c . carrierStatus || '--' , cls : 'normal' }
return {
id : String ( c . id ) ,
carrier : c . carrier || '--' ,
iccid : c . iccid || '--' ,
phone : c . phone || '--' ,
dockId : c . dockId || '--' ,
planGb ,
usedGb : usedGb . toFixed ( 1 ) ,
usagePct ,
statusClass : st . cls ,
carrierStatusName : st . name ,
expiredAt : c . expiredAt ? String ( c . expiredAt ) . slice ( 0 , 10 ) : '--'
}
} )
simRechargeTotal . value = simRechargePage ? . total || 0
simRechargePages . value = simRechargePage ? . pages || 1
simRechargeLogs . value = simRechargePage ? . records || [ ]
} catch ( e ) {
ui . toast ( e . message || '加载账户数据失败' )
}
if ( results . some ( ( result ) => result . status === 'rejected' ) ) ui . toast ( '部分账户数据加载失败' )
}
function goOrderPage ( page ) { if ( page < 1 || page > orderPages . value ) return ; orderPageNum . value = page ; loadOrders ( ) }
function goUsagePage ( page ) { if ( page < 1 || page > usagePages . value ) return ; usagePageNum . value = page ; loadUsage ( ) }
function goSimPage ( page ) { if ( page < 1 || page > simPages . value ) return ; simPageNum . value = page ; loadSim ( ) }
function goSimRechargePage ( page ) { if ( page < 1 || page > simRechargePages . value ) return ; simRechargePageNum . value = page ; loadSim ( ) }
async function createOrder ( ) { try { const order = await request . post ( urls . TRAFFIC_ORDERS , { amountGb : Number ( trafficAmount . value ) || 1 } ) ; ui . toast ( ` 订单已创建,金额 ${ money ( order . totalPrice ) } ,请等待管理员确认 ` ) ; orderPageNum . value = 1 ; await loadOrders ( ) } catch ( error ) { ui . toast ( error . message || '创建订单失败' ) } }
async function saveProfile ( ) { saving . value = true ; try { const updated = await request . put ( urls . ACCOUNT_PROFILE , editForm . value ) ; profile . value = updated ; userStore . user = { ... ( userStore . user || { } ) , ... updated } ; localStorage . setItem ( 'user' , JSON . stringify ( userStore . user ) ) ; editOpen . value = false ; ui . toast ( '账户资料已保存' ) } catch ( error ) { ui . toast ( error . message || '保存账户资料失败' ) } finally { saving . value = false } }
onMounted ( load )
function payStatusName ( status ) {
return PAY_STATUS [ status ] ? . status || status || '--'
}
function rechargeStatusName ( status ) {
return { pending : '处理中' , success : '充值成功' , failed : '充值失败' } [ status ] || status || '--'
}
function rechargeCardLabel ( id ) {
const card = simCards . value . find ( ( c ) => c . id === String ( id ) )
return card ? ` ${ card . carrier } · ${ card . iccid } ` : String ( id || '--' )
}
async function reloadSimRechargeLogs ( ) {
simRechargePageNum . value = 1
await loadSimRechargeLogs ( )
}
async function loadSimRechargeLogs ( ) {
try {
const params = { pageNum : simRechargePageNum . value , pageSize : 10 }
if ( simRechargeCardFilter . value ) params . simCardId = simRechargeCardFilter . value
const page = await request . get ( urls . SIM_RECHARGE_LOGS , { params } )
simRechargeTotal . value = page ? . total || 0
simRechargePages . value = page ? . pages || 1
simRechargeLogs . value = page ? . records || [ ]
} catch ( e ) {
ui . toast ( e . message || '加载通信卡充值记录失败' )
}
}
function goSimRechargePage ( page ) {
if ( page < 1 || page > simRechargePages . value ) return
simRechargePageNum . value = page
loadSimRechargeLogs ( )
}
function maskPhone ( p ) {
if ( ! p ) return ''
const s = String ( p )
return s . length > 7 ? s . slice ( 0 , 3 ) + '****' + s . slice ( - 4 ) : s
}
async function createOrder ( ) {
try {
await request . post ( urls . TRAFFIC_ORDERS , { amountGb : Number ( trafficAmount . value ) || 1 } )
ui . toast ( '订单已创建,请联系管理员完成支付' )
await load ( )
} catch ( e ) {
ui . toast ( e . message || '下单失败' )
}
}
function openRecharge ( card ) {
rechargeCard . value = card
rechargeAmount . value = 50
rechargeOpen . value = true
}
async function submitRecharge ( ) {
if ( ! rechargeCard . value ) return
recharging . value = true
try {
await request . post ( urls . SIM_CARD_RECHARGE ( rechargeCard . value . id ) , { amountGb : Number ( rechargeAmount . value ) || 1 } )
ui . toast ( '充值成功' )
rechargeOpen . value = false
await load ( )
await loadSimRechargeLogs ( )
} catch ( e ) {
ui . toast ( e . message || '充值失败' )
} finally {
recharging . value = false
}
}
< / script >