Browse Source

feat: update authentication and account flows

Add account endpoints and refresh login, session, and account UI integration.
main
刘浩东 4 weeks ago
parent
commit
c0ab3a48cd
  1. 11
      src/config/urls.js
  2. 7
      src/layout/components/SideMenu.vue
  3. 4
      src/stores/modules/devicesStore.js
  4. 10
      src/stores/modules/userStore.js
  5. 3
      src/utils/http.js
  6. 467
      src/views/AccountView/AccountView.vue
  7. 88
      src/views/LoginView/LoginView.vue

11
src/config/urls.js

@ -2,10 +2,12 @@
export const BASE_URL = '/api'
// Auth
export const AUTH_CAPTCHA = '/v1/auth/captcha'
export const AUTH_LOGIN = '/v1/auth/login'
export const AUTH_REGISTER = '/v1/auth/register'
export const AUTH_SMS_CODE = '/v1/auth/sms-code'
export const AUTH_RESET_PASSWORD = '/v1/auth/reset-password'
export const AUTH_LOGOUT = '/v1/auth/logout'
// Docks
export const DOCKS = '/v1/docks'
@ -46,9 +48,18 @@ export const ALARM_RESOLVE = (id) => `/v1/alarms/${id}/resolve`
export const COMMANDS = '/v1/commands'
// Account
export const ACCOUNT_PROFILE = '/v1/account/profile'
export const TRAFFIC_BALANCE = '/v1/account/traffic/balance'
export const TRAFFIC_ORDERS = '/v1/account/traffic/orders'
export const TRAFFIC_USAGE = '/v1/account/traffic/usage'
export const ACCOUNT_RESOURCE_SUMMARY = '/v1/account/resource-summary'
export const ACCOUNT_DOWNLOADS = '/v1/account/downloads'
export const ACCOUNT_CHANGE_PASSWORD = '/v1/account/security/change-password'
export const ACCOUNT_SESSIONS = '/v1/account/security/sessions'
export const INVOICE_PROFILES = '/v1/account/invoice-profiles'
export const INVOICE_PROFILE = (id) => `/v1/account/invoice-profiles/${id}`
export const INVOICE_ORDERS = '/v1/account/invoice-orders'
export const INVOICE_REQUESTS = '/v1/account/invoice-requests'
export const SIM_CARDS = '/v1/account/sim-cards'
export const SIM_RECHARGE_LOGS = '/v1/account/sim-recharge-logs'
export const SIM_CARD_RECHARGE = (id) => `/v1/account/sim-cards/${id}/recharge`

7
src/layout/components/SideMenu.vue

@ -3,6 +3,8 @@ import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/stores/modules/userStore'
import { useUiStore } from '@/stores/modules/uiStore'
import request from '@/utils/http'
import * as urls from '@/config/urls'
const route = useRoute()
const router = useRouter()
@ -64,6 +66,11 @@ async function logout() {
'清除本地凭证',
])
if (ok) {
try {
await request.post(urls.AUTH_LOGOUT, { refreshToken: userStore.refreshToken })
} catch {
//
}
userStore.logout()
router.push('/login')
ui.toast('已退出登录')

4
src/stores/modules/devicesStore.js

@ -184,7 +184,7 @@ export const useDeviceStore = defineStore('devices', () => {
const id = String(d.id)
const detail = dockDetails[i] || {}
const rt = detail.realtime || {}
const online = detail.online ?? d.status === 'online'
const online = Boolean(detail.online)
const registerStatus = d.registerStatus || 'registered'
const pending = registerStatus === 'pending' && !online
const st = pending
@ -228,7 +228,7 @@ export const useDeviceStore = defineStore('devices', () => {
const armed = ['1', 'true', 1, true].includes(rt.armed)
const altitude = coordinate(rt.altitude, -1000, 100000)
const flying = armed || (altitude != null && altitude > 0)
const online = detail.online ?? d.status !== 'offline'
const online = Boolean(detail.online)
const st = !online
? { status: '离线', statusClass: 'offline' }
: flying

10
src/stores/modules/userStore.js

@ -3,23 +3,29 @@ import { ref } from 'vue'
export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token') || '')
const refreshToken = ref(localStorage.getItem('refreshToken') || '')
const user = ref(JSON.parse(localStorage.getItem('user') || 'null'))
function setLogin(tokenVal, userVal) {
function setLogin(tokenVal, userVal, refreshTokenVal = '') {
const nextToken = tokenVal == null ? '' : String(tokenVal)
if (!nextToken) {
throw new Error('登录凭证为空')
}
token.value = nextToken
refreshToken.value = refreshTokenVal == null ? '' : String(refreshTokenVal)
user.value = userVal
localStorage.setItem('token', nextToken)
if (refreshToken.value) localStorage.setItem('refreshToken', refreshToken.value)
else localStorage.removeItem('refreshToken')
localStorage.setItem('user', JSON.stringify(userVal))
}
function logout() {
token.value = ''
refreshToken.value = ''
user.value = null
localStorage.removeItem('token')
localStorage.removeItem('refreshToken')
localStorage.removeItem('user')
}
@ -27,5 +33,5 @@ export const useUserStore = defineStore('user', () => {
return !!user.value && user.value.role === 'admin'
}
return { token, user, setLogin, logout, isAdmin }
return { token, refreshToken, user, setLogin, logout, isAdmin }
})

3
src/utils/http.js

@ -18,6 +18,9 @@ request.interceptors.request.use((config) => {
request.interceptors.response.use(
(response) => {
if (response.config.responseType === 'blob') {
return response.data
}
const res = response.data
if (res && res.code === 200) {
return res.data

467
src/views/AccountView/AccountView.vue

@ -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>{{ tenantName }}</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>{{ tenantInitial }}</span>
<div><strong>{{ tenantName }}</strong><small>企业账号 · {{ tenantRole }}</small></div>
<button class="table-text-action" @click="ui.toast('编辑企业信息')">编辑</button>
<span>{{ profileInitial }}</span>
<div><strong>{{ profileName }}</strong><small>账户角色 · {{ profileRole }}</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>

88
src/views/LoginView/LoginView.vue

@ -1,5 +1,5 @@
<script setup>
import { reactive, ref } from 'vue'
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import request from '@/utils/http'
import * as urls from '@/config/urls'
@ -21,7 +21,8 @@ const password = ref('')
const smsPhone = ref('')
const smsCode = ref('')
const captcha = ref('')
const captchaValue = ref('7K4P')
const captchaImage = ref('')
const captchaLoading = ref(false)
const loginError = ref('')
const loginCodeTimer = ref(0)
@ -29,6 +30,7 @@ const reg = reactive({
company: '',
contact: '',
phone: '',
smsCode: '',
email: '',
password: '',
confirm: '',
@ -40,41 +42,46 @@ const forgotVisible = ref(false)
const forgot = reactive({ account: '', phone: '', code: '', password: '' })
const forgotError = ref('')
function refreshCaptcha() {
const chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'
captchaValue.value = Array.from({ length: 4 }, () => chars[Math.floor(Math.random() * chars.length)]).join('')
async function refreshCaptcha() {
captchaLoading.value = true
try {
const response = await request.get(urls.AUTH_CAPTCHA, { responseType: 'blob' })
if (captchaImage.value) URL.revokeObjectURL(captchaImage.value)
captchaImage.value = URL.createObjectURL(response)
captcha.value = ''
} finally {
captchaLoading.value = false
}
}
async function submitLogin() {
loginError.value = ''
if (!captcha.value) {
if (method.value === 'password' && !captcha.value) {
loginError.value = '请输入图形验证码。'
return
}
if (captcha.value.toUpperCase() !== captchaValue.value) {
loginError.value = '图形验证码不正确,请重新输入。'
return
}
if (method.value === 'password' && (!account.value || !password.value)) {
loginError.value = '请输入账号和密码。'
return
}
if (method.value === 'sms') {
loginError.value = '短信登录暂未开放,请使用账号密码登录。'
if (method.value === 'sms' && (!smsPhone.value || !smsCode.value)) {
loginError.value = '请输入手机号和短信验证码。'
return
}
loading.value = true
try {
const data = await request.post(urls.AUTH_LOGIN, {
phone: account.value,
password: password.value,
phone: method.value === 'sms' ? smsPhone.value : account.value,
password: method.value === 'password' ? password.value : '',
captcha: captcha.value,
smsCode: method.value === 'sms' ? smsCode.value : '',
})
const token = data?.accessToken || data?.token || data?.jToken
if (!token) {
loginError.value = '登录成功但未返回 token,请检查后端响应字段。'
return
}
userStore.setLogin(token, data.user || data.userInfo || null)
userStore.setLogin(token, data.user || data.userInfo || null, data.refreshToken || '')
ui.toast('登录成功')
router.push('/monitor')
} catch (e) {
@ -86,7 +93,7 @@ async function submitLogin() {
async function submitRegister() {
registerError.value = ''
if (!reg.company || !reg.contact || !reg.phone || !reg.email || !reg.password || !reg.confirm) {
if (!reg.company || !reg.contact || !reg.phone || !reg.smsCode || !reg.email || !reg.password || !reg.confirm) {
registerError.value = '请完整填写注册信息。'
return
}
@ -105,10 +112,11 @@ async function submitRegister() {
phone: reg.phone,
email: reg.email,
password: reg.password,
smsCode: reg.smsCode,
})
const token = data?.accessToken || data?.token || data?.jToken
if (token) {
userStore.setLogin(token, data.user || data.userInfo || null)
userStore.setLogin(token, data.user || data.userInfo || null, data.refreshToken || '')
ui.toast('注册成功')
router.push('/monitor')
} else {
@ -124,6 +132,14 @@ async function submitRegister() {
}
}
async function sendSmsCode(phone, scene, onError) {
if (!phone) { onError('请先输入手机号。'); return }
try {
await request.post(urls.AUTH_SMS_CODE, { phone, scene })
ui.toast('短信验证码已发送')
} catch {}
}
function sendLoginCode() {
if (!smsPhone.value) {
loginError.value = '请先输入手机号。'
@ -135,8 +151,7 @@ function sendLoginCode() {
loginCodeTimer.value -= 1
if (loginCodeTimer.value <= 0) clearInterval(timer)
}, 1000)
request.post(urls.AUTH_SMS_CODE, { phone: smsPhone.value }).catch(() => {})
ui.toast('短信验证码已发送')
sendSmsCode(smsPhone.value, 'login', (message) => { loginError.value = message })
}
function sendForgotCode() {
@ -144,10 +159,15 @@ function sendForgotCode() {
forgotError.value = '请先输入手机号。'
return
}
request.post(urls.AUTH_SMS_CODE, { phone: forgot.phone }).catch(() => {})
ui.toast('短信验证码已发送')
sendSmsCode(forgot.phone, 'reset', (message) => { forgotError.value = message })
}
function sendRegisterCode() {
sendSmsCode(reg.phone, 'register', (message) => { registerError.value = message })
}
onMounted(refreshCaptcha)
async function submitForgot() {
forgotError.value = ''
if (!forgot.account || !forgot.phone || !forgot.code || !forgot.password) {
@ -198,6 +218,11 @@ async function submitForgot() {
<div :class="s.formSide">
<div :class="s.formWrap">
<div v-show="mode === 'login'" :class="s.panel">
<div :class="s.heading">
<span>嘉谷低空智控平台</span>
<h2>欢迎回来</h2>
<p>登录平台继续管理您的飞行任务</p>
</div>
<div :class="s.methodTabs">
<button
type="button"
@ -293,7 +318,7 @@ async function submitForgot() {
</t-form-item>
</template>
<t-form-item>
<t-form-item v-if="method === 'password'">
<div :class="s.codeRow">
<t-input
v-model="captcha"
@ -314,7 +339,8 @@ async function submitForgot() {
title="刷新验证码"
@click="refreshCaptcha"
>
{{ captchaValue }}
<img v-if="captchaImage" :src="captchaImage" alt="图形验证码" />
<span v-else>加载中</span>
</t-button>
</div>
</t-form-item>
@ -399,6 +425,13 @@ async function submitForgot() {
</t-input>
</t-form-item>
<t-form-item>
<div :class="s.codeRow">
<t-input v-model="reg.smsCode" size="large" clearable :maxlength="6" placeholder="请输入短信验证码" />
<t-button theme="default" variant="outline" size="large" @click="sendRegisterCode">获取验证码</t-button>
</div>
</t-form-item>
<div :class="s.row">
<t-form-item>
<t-input
@ -691,8 +724,17 @@ async function submitForgot() {
.captchaBtn {
min-width: 96px;
padding: 0 4px;
overflow: hidden;
letter-spacing: 0.18em;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
img {
display: block;
width: 88px;
height: 32px;
object-fit: contain;
}
}
.options {

Loading…
Cancel
Save