feat(license): 在线异常浮告警卡+离线SN/MAC标签化,统一两Tab排版

- 在线:正常态只显示核心信息,提醒/过期/设备不匹配时浮出告警卡,SN/MAC 收进折叠诊断;frozen 状态文案统一为「已过期」
- 离线:SN/MAC 改行尾标签+失配整行标红,IP/域名/创建时间折叠进「更多」
- 统一在线/离线信息行的字号/行高/间距/label
- 清理死样式 .information 与死变量 onlineIng
- 同步 ai-kb online.md 状态措辞并登记新增文案
This commit is contained in:
kuaifan 2026-07-02 07:45:16 +00:00
parent f45f86601e
commit c9f5296b73
3 changed files with 402 additions and 186 deletions

View File

@ -2499,3 +2499,26 @@ AI任务分析
(*)秒后重发
请输入邮箱和验证码
刷新中
诊断详情
授权 SN
当前 SN
授权 MAC
当前 MAC
授权与当前设备不匹配
检测到设备标识SN已变更在线授权可能已失效。请重新登录授权或先在原设备退出以释放座位。
在线授权已过期
新增用户已受限,请尽快联网以自动续期恢复。
检测到网卡MAC变化
系统会在下次续期时自动恢复授权,通常无需处理。
续期失败,请检查网络
授权仍然有效,联网后会自动续期恢复。
授权即将到期
请保持联网,系统会自动为你续期。
重新登录授权
匹配
与本机不一致
收起
更多信息
提示
将释放当前设备占用的授权座位并回到登录,确定继续?
已过期

View File

@ -19,6 +19,11 @@ aliases:
- 在线授权冻结
- 在线授权被吊销
- 换机 deactivate
- SN 与 License 不匹配
- MAC 与 License 不匹配
- 终端SN与License不匹配
- 终端MAC与License不匹配
- 换机后在线授权失效
related_tools: []
related_pages: []
prerequisites:
@ -30,7 +35,7 @@ negative:
- 离线授权(粘贴 License 原文)完全不受影响,没有自动续期
- 一个账号同一时刻只占用一个实例座位,换机需先在原实例「退出在线授权」释放
- 试用每个账号仅一次,时长由 App Store 管理员配置且硬上限 60 天
last_verified: v1.7.91
last_verified: v1.8.45
---
# 在线授权(邮箱验证码登录 / 申请试用 / 自动续期)
@ -53,15 +58,14 @@ DooTask 的 License 页提供两种授权方式,可在页面顶部 Tab 切换
### 已有授权 → 登录授权
- 填好邮箱与验证码后点击「登录授权」
- 终端把自身指纹doo_sn、网卡 MAC、版本上报授权中心签发一张租约 License 并落地
- 成功后页面显示套餐、使用人数、租约到期、当前状态
- 成功后页面显示账号、套餐、使用人数、授权有效期、SN/MAC与本机是否匹配、当前状态
### 没有正式授权 → 申请试用
- 填好邮箱与验证码后点击「申请试用」即开通试用授权并签发
- 试用默认 14 天 / 不限人数(具体以 App Store 管理员配置为准,时长硬上限 60 天),每个账号仅能申请一次
### 日常维护
- **自动续期**:终端每小时检查,租约将尽时自动向授权中心续期,无需人工干预(需保持联网)
- **立即续期**:状态页「立即续期」按钮可手动触发一次
- **自动续期**:终端每小时检查,租约将尽时自动向授权中心续期,无需人工干预(需保持联网);进入授权页时也会静默刷新一次
- **换机 / 退出**:「退出在线授权」会释放该账号在授权中心占用的座位,并把终端回落到基础版;换机时先在原实例退出,再到新实例登录
## 状态与提醒(断网/欠费时的分级降级)
@ -71,11 +75,21 @@ DooTask 的 License 页提供两种授权方式,可在页面顶部 Tab 切换
| --- | --- | --- |
| 生效中 | 续期正常 | 无 |
| 即将到期 | 续期失败或租约临近 | 仅提醒 |
| 已冻结 | 租约已过期 | 限制新增用户(沿用离线过期的既有行为) |
| 已吊销 | 冻结超过宽限期或授权被收回 | 回落基础版(最多 3 人,超出的账号按既有规则禁用) |
| 已过期 | 租约已到期 | 限制新增用户(沿用离线过期的既有行为) |
| 已吊销 | 过期超过宽限期或授权被收回 | 回落基础版(最多 3 人,超出的账号按既有规则禁用) |
只要在租约窗口内恢复联网并成功续期一次,即可回到「生效中」。
## SN / MAC 与本机匹配
在线授权签发的 License 内嵌了签发时的终端 SN 与网卡 MAC。正常情况下二者与本机一致「在线授权」Tab 保持极简,只显示账号、套餐、使用人数、有效期与状态(带颜色圆点),不常驻展示 SN/MAC 这类技术细节。
一旦 SN 或 MAC 与本机不一致,页面顶部会浮出告警卡,并展开「诊断详情」逐项列出「授权 SN / 当前 SN / 授权 MAC / 当前 MAC」及匹配标记✓/✕同一批告警也会出现在仪表盘顶部横幅。「离线授权」Tab手动粘贴场景则始终以列表展示 SN/MAC用行尾标签直陈匹配与否、不一致时整行标红IP/域名/创建时间等低频字段收纳在「更多信息」里。
授权成功后若终端的 SN 或 MAC 发生变化,行为不同:
- **MAC 变化SN 不变)**:属于同一实例,下一次自动续期会用新的 MAC 重新签发 License 自动恢复,通常无感;重签前的短暂窗口可能出现 MAC 不匹配告警。
- **SN 变化**:视为换机。续期会被授权中心判为座位被占用而拒绝重签,原 License 因 SN 对不上而失效,终端将按状态机走向「已过期 → 已吊销」并回落基础版。需在原实例(或让管理员在 App Store 授权中心)释放座位后,在新实例重新登录授权。
## 怎么自检
- 接口 `POST api/license/status` 返回当前在线授权状态mode/plan/people/lease_expired_at/status
- 提醒文案同样并入 `POST api/system/license``error` 数组,便于脚本巡检

View File

@ -1,173 +1,166 @@
<template>
<div class="setting-item submit license-setting">
<Tabs v-model="mode">
<TabPane :label="$L('在线授权')" name="online">
<div class="setting-component-item">
<div class="setting-scroll">
<!-- 首次进入且无缓存骨架占位 + 加载中有缓存则直接渲染下方真实数据 -->
<div v-if="firstLoading" class="license-box">
<div class="online-refreshing"><i class="online-spin"></i>{{$L('加载中...')}}</div>
<ul class="online-info">
<li><em>{{$L('账号')}}:</em><span class="online-skeleton"></span></li>
<li><em>{{$L('套餐')}}:</em><span class="online-skeleton sk-sm"></span></li>
<li><em>{{$L('使用人数')}}:</em><span class="online-skeleton sk-xs"></span></li>
<li><em>{{$L('授权有效期')}}:</em><span class="online-skeleton"></span></li>
<li><em>{{$L('当前状态')}}:</em><span class="online-skeleton sk-sm"></span></li>
</ul>
</div>
<template v-else>
<div v-if="onlineActive" class="license-box">
<!-- 后台刷新中指示缓存秒开后仍刷新最新数据 -->
<div v-if="onlineRefreshing" class="online-refreshing"><i class="online-spin"></i>{{$L('刷新中')}}</div>
<ul class="online-info">
<li><em>{{$L('账号')}}:</em><span>{{online.account}}</span></li>
<li><em>{{$L('套餐')}}:</em><span>{{online.plan || '-'}}</span></li>
<li><em>{{$L('使用人数')}}:</em><span>{{online.people || $L('无限制')}}</span></li>
<li><em>{{$L('授权有效期')}}:</em><span>{{online.valid_until ? fmt(online.valid_until) : $L('永久')}}</span></li>
<li>
<em>{{$L('当前状态')}}:</em>
<span :class="{warning: online.status !== 'active'}">{{stageText(online.status)}}</span>
</li>
</ul>
<TabPane :label="$L('在线授权')" name="online">
<div class="setting-component-item">
<div class="setting-scroll">
<!-- 首次进入且无缓存骨架占位 + 加载中有缓存则直接渲染下方真实数据 -->
<div v-if="firstLoading" class="license-box">
<div class="online-refreshing"><i class="online-spin"></i>{{$L('加载中...')}}</div>
<ul class="online-info">
<li><em>{{$L('账号')}}:</em><span class="online-skeleton"></span></li>
<li><em>{{$L('套餐')}}:</em><span class="online-skeleton sk-sm"></span></li>
<li><em>{{$L('使用人数')}}:</em><span class="online-skeleton sk-xs"></span></li>
<li><em>{{$L('授权有效期')}}:</em><span class="online-skeleton"></span></li>
<li><em>{{$L('当前状态')}}:</em><span class="online-skeleton sk-sm"></span></li>
</ul>
</div>
<template v-else>
<div v-if="onlineActive" class="license-box">
<!-- 后台刷新中指示缓存秒开后仍刷新最新数据 -->
<div v-if="onlineRefreshing" class="online-refreshing"><i class="online-spin"></i>{{$L('刷新中')}}</div>
<!-- 异常告警卡仅提醒/冻结/设备不匹配时浮现正常态不显示避免噪音 -->
<div v-if="onlineAlert" class="online-alert" :class="onlineAlert.type">
<i class="online-alert-ico">{{onlineAlert.type === 'error' ? '✕' : '!'}}</i>
<div class="online-alert-main">
<div class="online-alert-title">{{onlineAlert.title}}</div>
<div class="online-alert-desc">{{onlineAlert.desc}}</div>
</div>
</div>
<!-- 核心信息正常态只看结果不常驻 SN/MAC -->
<ul class="online-info">
<li><em>{{$L('账号')}}:</em><span>{{online.account}}</span></li>
<li><em>{{$L('套餐')}}:</em><span>{{online.plan || '-'}}</span></li>
<li><em>{{$L('使用人数')}}:</em><span>{{online.people || $L('无限制')}}</span></li>
<li><em>{{$L('授权有效期')}}:</em><span>{{online.valid_until ? fmt(online.valid_until) : $L('永久')}}</span></li>
<li>
<em>{{$L('当前状态')}}:</em>
<span class="online-status" :class="'is-' + onlineHealth"><i class="online-status-dot"></i>{{stageText(online.status)}}</span>
</li>
</ul>
<!-- 诊断详情仅设备SN/MAC不匹配时展开普通用户不受打扰 -->
<div v-if="onlineMismatch" class="online-diag">
<div class="online-diag-title">{{$L('诊断详情')}}</div>
<div class="online-diag-row" :class="{bad: !snMatch}"><em>{{$L('授权 SN')}}:</em><span>{{formData.info.sn}}</span></div>
<div class="online-diag-row" :class="{bad: !snMatch}"><em>{{$L('当前 SN')}}:</em><span>{{formData.doo_sn}}<b>{{snMatch ? ' ✓' : ' ✕'}}</b></span></div>
<div class="online-diag-row" :class="{bad: !macMatch}"><em>{{$L('授权 MAC')}}:</em><span>{{infoJoin(formData.info.mac)}}</span></div>
<div class="online-diag-row" :class="{bad: !macMatch}"><em>{{$L('当前 MAC')}}:</em><span>{{infoJoin(formData.macs)}}<b>{{macMatch ? ' ✓' : ' ✕'}}</b></span></div>
</div>
</div>
<Form v-else :model="onlineForm" v-bind="formOptions" @submit.native.prevent>
<FormItem :label="$L('邮箱')">
<Input
v-model="onlineForm.email"
:class="codeCountdown > 0 ? 'setting-send-input' : 'setting-input'"
search @on-search="emailSend"
:enter-button="sendBtnText"
:disabled="onlineBusy"
:placeholder="$L('请输入邮箱')"/>
</FormItem>
<FormItem v-if="codeSent" :label="$L('邮箱验证码')">
<Input v-model="onlineForm.code" class="setting-input" :placeholder="$L('请输入验证码')"/>
<div class="online-tip">{{$L('验证码已发送至(*)', maskedEmail)}}</div>
</FormItem>
</Form>
</template>
</div>
<div v-if="!firstLoading" class="setting-footer">
<template v-if="onlineActive">
<Button v-if="onlineAlert && onlineAlert.relogin" :loading="onlineAction === 'relogin'" :disabled="onlineBusy && onlineAction !== 'relogin'" type="primary" @click="onlineRelogin">{{$L('重新登录授权')}}</Button>
<Button :loading="onlineAction === 'logout'" :disabled="onlineBusy && onlineAction !== 'logout'" :type="onlineAlert && onlineAlert.relogin ? 'default' : 'primary'" @click="onlineLogout">{{$L('退出在线授权')}}</Button>
</template>
<template v-else>
<Button :loading="onlineAction === 'login'" :disabled="onlineBusy && onlineAction !== 'login'" type="primary" @click="onlineLogin">{{$L('登录授权')}}</Button>
<Button :loading="onlineAction === 'trial'" :disabled="onlineBusy && onlineAction !== 'trial'" @click="trialSubmit">{{$L('申请试用')}}</Button>
</template>
</div>
<Form v-else :model="onlineForm" v-bind="formOptions" @submit.native.prevent>
<FormItem :label="$L('邮箱')">
<Input
v-model="onlineForm.email"
:class="codeCountdown > 0 ? 'setting-send-input' : 'setting-input'"
search @on-search="emailSend"
:enter-button="sendBtnText"
:disabled="onlineBusy"
:placeholder="$L('请输入邮箱')"/>
</FormItem>
<FormItem v-if="codeSent" :label="$L('邮箱验证码')">
<Input v-model="onlineForm.code" class="setting-input" :placeholder="$L('请输入验证码')"/>
<div class="online-tip">{{$L('验证码已发送至(*)', maskedEmail)}}</div>
</FormItem>
</Form>
</template>
</div>
<div v-if="!firstLoading" class="setting-footer">
<template v-if="onlineActive">
<Button :loading="onlineAction === 'logout'" :disabled="onlineBusy && onlineAction !== 'logout'" type="primary" @click="onlineLogout">{{$L('退出在线授权')}}</Button>
</template>
<template v-else>
<Button :loading="onlineAction === 'login'" :disabled="onlineBusy && onlineAction !== 'login'" type="primary" @click="onlineLogin">{{$L('登录授权')}}</Button>
<Button :loading="onlineAction === 'trial'" :disabled="onlineBusy && onlineAction !== 'trial'" @click="trialSubmit">{{$L('申请试用')}}</Button>
</template>
</div>
</div>
</TabPane>
<TabPane :label="$L('离线授权')" name="offline">
<div class="setting-component-item">
<div class="setting-scroll">
<template v-if="onlineActive">
<div class="license-box">
<ul class="online-info">
<li><em>{{$L('当前状态')}}:</em><span class="online-link" @click="mode = 'online'">{{$L('已绑定在线授权')}}</span></li>
<li><em>SN:</em><span>{{formData.doo_sn}}</span></li>
<li><em>MAC:</em><span>{{infoJoin(formData.macs)}}</span></li>
</ul>
</div>
<Form v-if="offlineRebindShow" :model="formData" v-bind="formOptions" @submit.native.prevent>
<FormItem label="License">
<Input v-model="offlineRebindLicense" type="textarea" :autosize="{minRows: 2,maxRows: 5}" :placeholder="$L('请输入License...')" />
</FormItem>
</Form>
</template>
<template v-else>
<Form ref="formData" :model="formData" v-bind="formOptions" @submit.native.prevent>
<FormItem label="License" prop="license">
<Input v-model="formData.license" type="textarea" :autosize="{minRows: 2,maxRows: 5}" :placeholder="$L('请输入License...')" />
</FormItem>
<FormItem>
<div class="license-box">
<ul v-if="formData.info.sn">
<li>
<em>SN:</em>
<span>{{formData.info.sn}}</span>
<ETooltip max-width="auto" placement="right">
<div slot="content">{{$L('当前环境')}}: {{formData.doo_sn}}</div>
<Icon class="information" :class="{error: !existIntersection(formData.doo_sn, formData.info.sn)}" type="ios-information-circle-outline" />
</ETooltip>
</li>
<li>
<em>IP:</em>
<span>{{infoJoin(formData.info.ip)}}</span>
</li>
<li>
<em>{{$L('域名')}}:</em>
<span>{{infoJoin(formData.info.domain)}}</span>
</li>
<li>
<em>MAC:</em>
<span>{{infoJoin(formData.info.mac)}}</span>
<ETooltip max-width="auto" placement="right">
<div slot="content">{{$L('当前环境')}}: {{infoJoin(formData.macs, '-')}}</div>
<Icon class="information" :class="{error: !existIntersection(formData.macs, formData.info.mac)}" type="ios-information-circle-outline" />
</ETooltip>
</li>
<li>
<em>{{$L('使用人数')}}:</em>
<span>{{formData.info.people || $L('无限制')}} ({{$L('已使用')}}: {{formData.user_count}})</span>
<ETooltip max-width="auto" placement="right">
<div slot="content">{{$L('限制注册人数')}}</div>
<Icon class="information" type="ios-information-circle-outline" />
</ETooltip>
</li>
<li>
<em>{{$L('创建时间')}}:</em>
<span>{{formData.info.created_at}}</span>
</li>
<li>
<em>{{$L('到期时间')}}:</em>
<span>{{formData.info.expired_at || $L('永久')}}</span>
<ETooltip v-if="formData.info.expired_at" max-width="auto" placement="right">
<div slot="content">{{$L('到期后限制注册帐号')}}</div>
<Icon class="information" type="ios-information-circle-outline" />
</ETooltip>
</li>
</ul>
<ul v-else>
<li>
{{$L('加载中...')}}
</li>
</ul>
</div>
</FormItem>
<FormItem :label="$L('当前环境')" v-if="formData.error?.length > 0">
<div class="license-box">
<ul>
<li>
<em>SN:</em>
<span>{{formData.doo_sn}}</span>
</li>
<li>
<em>MAC:</em>
<span>{{infoJoin(formData.macs)}}</span>
</li>
<li v-for="(tip, ti) in formData.error" :key="ti" class="warning">{{tip}}</li>
</ul>
</TabPane>
<TabPane :label="$L('离线授权')" name="offline">
<div class="setting-component-item">
<div class="setting-scroll">
<template v-if="onlineActive">
<div class="license-box">
<ul class="online-info">
<li><em>{{$L('当前状态')}}:</em><span class="online-link" @click="mode = 'online'">{{$L('已绑定在线授权')}}</span></li>
<li><em>SN:</em><span>{{formData.doo_sn}}</span></li>
<li><em>MAC:</em><span>{{infoJoin(formData.macs)}}</span></li>
</ul>
</div>
<Form v-if="offlineRebindShow" :model="formData" v-bind="formOptions" @submit.native.prevent>
<FormItem label="License">
<Input v-model="offlineRebindLicense" type="textarea" :autosize="{minRows: 2,maxRows: 5}" :placeholder="$L('请输入License...')" />
</FormItem>
</Form>
</template>
<template v-else>
<Form ref="formData" :model="formData" v-bind="formOptions" @submit.native.prevent class="license-form">
<FormItem label="License" prop="license">
<Input v-model="formData.license" type="textarea" :autosize="{minRows: 2,maxRows: 5}" :placeholder="$L('请输入License...')" />
</FormItem>
<FormItem>
<div class="license-box">
<ul v-if="formData.info.sn" class="offline-detail">
<!-- SN/MAC行尾标签直陈匹配与否失配整行标红无需悬停 -->
<li class="offline-row" :class="{bad: !snMatch}">
<em>SN:</em>
<span class="v">{{formData.info.sn}}</span>
<span class="offline-flag">{{snMatch ? $L('匹配') : $L('与本机不一致')}}</span>
</li>
<li class="offline-row" :class="{bad: !macMatch}">
<em>MAC:</em>
<span class="v">{{infoJoin(formData.info.mac)}}</span>
<span class="offline-flag">{{macMatch ? $L('匹配') : $L('与本机不一致')}}</span>
</li>
<li class="offline-row">
<em>{{$L('使用人数')}}:</em>
<span class="v">{{formData.info.people || $L('无限制')}}{{$L('已使用')}} {{formData.user_count}}</span>
</li>
<li class="offline-row">
<em>{{$L('到期时间')}}:</em>
<span class="v">{{formData.info.expired_at || $L('永久')}}</span>
</li>
<!-- 低频字段折叠默认更干净 -->
<template v-if="offlineMore">
<li class="offline-row"><em>IP:</em><span class="v">{{infoJoin(formData.info.ip)}}</span></li>
<li class="offline-row"><em>{{$L('域名')}}:</em><span class="v">{{infoJoin(formData.info.domain)}}</span></li>
<li class="offline-row"><em>{{$L('创建时间')}}:</em><span class="v">{{formData.info.created_at}}</span></li>
<li class="offline-row"><em>{{$L('当前环境')}}:</em><span class="v">SN: {{formData.doo_sn}}, MAC: {{infoJoin(formData.macs)}}</span></li>
</template>
<li class="offline-more"><a @click="offlineMore = !offlineMore">{{offlineMore ? $L('收起') : $L('更多信息')}}</a></li>
</ul>
<ul v-else>
<li>
{{$L('加载中...')}}
</li>
</ul>
</div>
</FormItem>
<FormItem :label="$L('提示')" v-if="formData.error?.length > 0">
<div class="license-box">
<ul>
<li v-for="(tip, ti) in formData.error" :key="ti" class="warning">{{tip}}</li>
</ul>
</div>
</FormItem>
</Form>
</template>
</div>
<div class="setting-footer">
<template v-if="onlineActive">
<Button v-if="!offlineRebindShow" type="primary" @click="offlineRebindShow = true">{{$L('绑定离线 License')}}</Button>
<template v-else>
<Button :loading="loadIng > 0" type="primary" @click="offlineRebindSubmit">{{$L('提交')}}</Button>
<Button :loading="loadIng > 0" @click="offlineRebindCancel">{{$L('取消')}}</Button>
</template>
</template>
<template v-else>
<Button :loading="loadIng > 0" type="primary" @click="submitForm">{{$L('提交')}}</Button>
<Button :loading="loadIng > 0" @click="resetForm">{{$L('重置')}}</Button>
</template>
</div>
</div>
</FormItem>
</Form>
</template>
</div>
<div class="setting-footer">
<template v-if="onlineActive">
<Button v-if="!offlineRebindShow" type="primary" @click="offlineRebindShow = true">{{$L('绑定离线 License')}}</Button>
<template v-else>
<Button :loading="loadIng > 0" type="primary" @click="offlineRebindSubmit">{{$L('提交')}}</Button>
<Button :loading="loadIng > 0" @click="offlineRebindCancel">{{$L('取消')}}</Button>
</template>
</template>
<template v-else>
<Button :loading="loadIng > 0" type="primary" @click="submitForm">{{$L('提交')}}</Button>
<Button :loading="loadIng > 0" @click="resetForm">{{$L('重置')}}</Button>
</template>
</div>
</div>
</TabPane>
</TabPane>
</Tabs>
</div>
</template>
@ -177,6 +170,11 @@
flex: 1;
overflow-y: auto;
}
.license-form {
.ivu-form-item {
margin-bottom: 12px;
}
}
.license-box {
position: relative;
padding-top: 6px;
@ -194,8 +192,8 @@
> li {
list-style: none;
font-size: 14px;
line-height: 22px;
padding-bottom: 6px;
line-height: 24px;
padding-bottom: 4px;
display: flex;
align-items: center;
gap: 6px;
@ -208,15 +206,6 @@
font-style: normal;
opacity: 0.8;
}
.information {
display: flex;
align-items: center;
justify-content: center;
margin-left: 6px;
&.error {
color: #ed4014;
}
}
}
}
}
@ -264,6 +253,105 @@
margin-top: 4px;
opacity: 0.6;
}
/* 在线授权:异常告警卡(正常态不渲染) */
.online-alert {
display: flex;
gap: 10px;
max-width: calc(100vw - 20px);
margin: 0 auto 14px;
padding: 11px 13px;
border-radius: 6px;
&.warning { background: #fdf6ec; border: 1px solid #faecd8; }
&.error { background: #fef0ef; border: 1px solid #fcd7d3; }
.online-alert-ico {
flex-shrink: 0;
width: 18px;
height: 18px;
border-radius: 50%;
color: #fff;
font-size: 12px;
font-style: normal;
display: flex;
align-items: center;
justify-content: center;
}
&.warning .online-alert-ico { background: #ff9900; }
&.error .online-alert-ico { background: #ed4014; }
.online-alert-title { font-size: 13px; font-weight: 600; margin-bottom: 2px; }
&.warning .online-alert-title { color: #b8791b; }
&.error .online-alert-title { color: #c0341a; }
.online-alert-desc { font-size: 12px; line-height: 18px; }
&.warning .online-alert-desc { color: #8a7455; }
&.error .online-alert-desc { color: #97544a; }
}
/* 在线授权:状态圆点 */
.online-status {
display: inline-flex;
align-items: center;
.online-status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
margin-right: 6px;
background: #c5c8ce;
}
&.is-ok { color: #19be6b; .online-status-dot { background: #19be6b; } }
&.is-warning { color: #ff9900; .online-status-dot { background: #ff9900; } }
&.is-error { color: #ed4014; font-weight: 500; .online-status-dot { background: #ed4014; } }
}
/* 在线授权:诊断详情(仅 SN/MAC 不匹配时出现) */
.online-diag {
max-width: calc(100vw - 20px);
margin: 10px auto 0;
margin-top: 10px;
padding: 10px 12px;
background: #f8f9fb;
border-radius: 6px;
.online-diag-title { font-size: 12px; color: #808695; margin-bottom: 6px; }
.online-diag-row {
font-size: 12.5px;
line-height: 20px;
display: flex;
> em { font-style: normal; opacity: 0.55; width: 72px; flex-shrink: 0; }
b { font-weight: 500; }
&.bad { color: #ed4014; > em { opacity: 0.7; } }
}
}
/* 离线授权:详情行 + 匹配标签 */
.offline-detail {
.offline-row {
/* 文字排版(字号/行高/label/gap沿用默认信息行只保留标红块所需的内边距与圆角 */
padding: 2px 0;
border-radius: 5px;
> .v { flex: 1; word-break: break-all; }
.offline-flag {
flex-shrink: 0;
font-size: 12px;
padding: 1px 8px;
border-radius: 9px;
background: #e8f7f0;
color: #19be6b;
}
&.bad {
color: #ed4014;
.offline-flag { background: #fdecea; color: #ed4014; }
}
}
.offline-more {
padding: 4px 0;
a {
font-size: 13px;
color: #2d8cf0;
cursor: pointer;
&:hover { text-decoration: underline; }
}
}
}
body.window-portrait {
.license-box {
padding-top: 16px;
}
}
</style>
<script>
import {mapState} from "vuex";
@ -287,7 +375,7 @@ export default {
tabInited: false,
offlineRebindShow: false,
offlineRebindLicense: '',
onlineIng: 0,
offlineMore: false, // 线 IP///
onlineAction: '', // 线'' | 'login' | 'trial' | 'logout' loading/
onlineForm: {
email: '',
@ -351,6 +439,82 @@ export default {
const macOk = this.existIntersection(this.formData.macs, info.mac);
return !isTrialThree && snOk && macOk;
},
// license SN/MAC 线/线info
snMatch() {
const info = this.formData.info || {};
return !info.sn || this.existIntersection(this.formData.doo_sn, info.sn);
},
macMatch() {
const info = this.formData.info || {};
return !info.mac || this.existIntersection(this.formData.macs, info.mac);
},
// 线license SN MAC /
onlineMismatch() {
const info = this.formData.info || {};
return !!info.sn && (!this.snMatch || !this.macMatch);
},
// 线ok绿/ warning/ error
onlineHealth() {
const s = this.online.status;
if (s === 'revoked' || s === 'frozen' || !this.snMatch) {
return 'error';
}
if (s === 'reminder' || !this.macMatch) {
return 'warning';
}
return 'ok';
},
// 线 null
onlineAlert() {
if (!this.onlineActive) {
return null;
}
const s = this.online.status;
// SN
if (this.onlineMismatch && !this.snMatch) {
return {
type: 'error',
title: this.$L('授权与当前设备不匹配'),
desc: this.$L('检测到设备标识SN已变更在线授权可能已失效。请重新登录授权或先在原设备退出以释放座位。'),
relogin: true,
};
}
if (s === 'frozen') {
return {
type: 'error',
title: this.$L('在线授权已过期'),
desc: this.$L('新增用户已受限,请尽快联网以自动续期恢复。'),
relogin: false,
};
}
// MAC
if (this.onlineMismatch && !this.macMatch) {
return {
type: 'warning',
title: this.$L('检测到网卡MAC变化'),
desc: this.$L('系统会在下次续期时自动恢复授权,通常无需处理。'),
relogin: false,
};
}
if (s === 'reminder') {
if (this.online.error_count > 0) {
return {
type: 'warning',
title: this.$L('续期失败,请检查网络'),
desc: this.$L('授权仍然有效,联网后会自动续期恢复。'),
relogin: false,
};
}
return {
type: 'warning',
title: this.$L('授权即将到期'),
desc: this.$L('请保持联网,系统会自动为你续期。'),
relogin: false,
};
}
return null;
},
},
methods: {
submitForm() {
@ -510,13 +674,12 @@ export default {
return {
active: this.$L('生效中'),
reminder: this.$L('即将到期'),
frozen: this.$L('已冻结'),
frozen: this.$L('已过期'),
revoked: this.$L('已吊销'),
}[status] || status || '-';
},
onlineCall(url, data, successMsg) {
this.onlineIng++;
return this.$store.dispatch("call", {
url,
data,
@ -529,8 +692,6 @@ export default {
}).catch(({msg}) => {
$A.modalError(msg);
return Promise.reject(msg);
}).finally(_ => {
this.onlineIng--;
});
},
@ -636,6 +797,24 @@ export default {
});
},
// /
onlineRelogin() {
$A.modalConfirm({
title: '重新登录授权',
content: '将释放当前设备占用的授权座位并回到登录,确定继续?',
onOk: () => {
this.onlineAction = 'relogin';
this.onlineCall('license/logout', {}, '').then(_ => {
this.systemSetting();
}).catch(() => {
// onlineCall
}).finally(() => {
this.onlineAction = '';
});
}
});
},
startCodeCountdown() {
this.clearCodeTimer();
this.codeCountdown = 60;