feat: 更新许可证管理功能,添加手动创建许可证的接口,增强授权信息展示,更新相关国际化文本
This commit is contained in:
1 parent
49e5952e1d
commit
2f683fa22b
11 files changed
+532
-77
No files matched your search
@@ -20,6 +20,7 @@ export interface AuthorizationCode {
|
||||
encryption_type_display?: string;
|
||||
software_version?: string;
|
||||
max_activations: number;
|
||||
current_activations?: number;
|
||||
activated_licenses_count?: number;
|
||||
is_locked?: boolean;
|
||||
feature_config?: any;
|
||||
@@ -301,6 +302,23 @@ export function getLicenseDeviceDetail(id: string): Promise<ApiResponse<LicenseD
|
||||
return Axios.get(`/api/v1/licenses/${id}`)
|
||||
}
|
||||
|
||||
// 创建许可证请求(手动绑定设备)
|
||||
export interface LicenseDeviceCreateRequest {
|
||||
authorization_code_id: string
|
||||
hardware_fingerprint: string
|
||||
device_info?: Record<string, any>
|
||||
activation_ip?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动创建许可证
|
||||
*/
|
||||
export function createLicenseDevice(
|
||||
data: LicenseDeviceCreateRequest
|
||||
): Promise<ApiResponse<LicenseDevice>> {
|
||||
return Axios.post('/api/v1/licenses', data)
|
||||
}
|
||||
|
||||
// 授权变更历史相关类型定义
|
||||
export interface AuthorizationChangeItem {
|
||||
id: string
|
||||
|
||||
@@ -64,9 +64,10 @@ const { t } = useI18n()
|
||||
const defaultNavItems = computed(() => [
|
||||
{ id: "dashboard", label: t('navigation.menu.dashboard'), href: "/dashboard", icon: "dashboard" },
|
||||
{ id: "customers", label: t('navigation.menu.customers'), href: "/customers", icon: "customers" },
|
||||
{ id: "licenses", label: t('navigation.menu.licenses'), href: "/licenses", icon: "licenses" },
|
||||
{ id: "roles", label: t('navigation.menu.roles'), href: "/roles", icon: "roles" },
|
||||
{ id: "users", label: t('navigation.menu.users'), href: "/users", icon: "users" }
|
||||
{ id: "licenses", label: t('navigation.menu.licenses'), href: "/licenses", icon: "licenses" }
|
||||
// 暂时隐藏角色和系统用户菜单,后续版本再开启
|
||||
// { id: "roles", label: t('navigation.menu.roles'), href: "/roles", icon: "roles" },
|
||||
// { id: "users", label: t('navigation.menu.users'), href: "/users", icon: "users" }
|
||||
])
|
||||
|
||||
// 使用 store 和路由
|
||||
|
||||
@@ -316,7 +316,7 @@
|
||||
"authorizationInfo": {
|
||||
"basicInfo": "Basic Information",
|
||||
"customerName": "Customer Name",
|
||||
"customerId": "Customer ID",
|
||||
"customerCode": "Customer Code",
|
||||
"createdBy": "Created By",
|
||||
"description": "Description",
|
||||
"title": "Authorization Information",
|
||||
@@ -334,6 +334,54 @@
|
||||
"code": "Authorization Code",
|
||||
"tipText": "Provide this authorization code to the customer, who can automatically obtain the software license after configuring it in the software product"
|
||||
},
|
||||
"licenseInfo": {
|
||||
"addButton": "Add License",
|
||||
"emptyDescription": "No device licenses yet",
|
||||
"deviceTitle": "Device {index}",
|
||||
"deviceAddress": "Device address",
|
||||
"lastActiveTime": "Last active time",
|
||||
"hardwareSectionTitle": "Hardware Information",
|
||||
"hardwareSectionSubtitle": "(Binding info)",
|
||||
"hardwareFingerprint": "Hardware fingerprint",
|
||||
"softwareSectionTitle": "Software License",
|
||||
"softwareSectionSubtitle": "(Used for validation)",
|
||||
"licenseKey": "License key",
|
||||
"licenseStatus": "License status",
|
||||
"statusDescription": "This license is generated and bound to the specified hardware for runtime validation.",
|
||||
"messages": {
|
||||
"loadError": "Failed to load license devices"
|
||||
}
|
||||
},
|
||||
"licenseModal": {
|
||||
"title": "Add License",
|
||||
"description": "Bind this authorization code to a hardware fingerprint.",
|
||||
"fields": {
|
||||
"authorizationCode": "Authorization Code",
|
||||
"hardwareFingerprint": "Hardware Fingerprint",
|
||||
"remark": "Remark"
|
||||
},
|
||||
"placeholders": {
|
||||
"hardwareFingerprint": "Enter hardware fingerprint",
|
||||
"remark": "Add a remark (optional)"
|
||||
},
|
||||
"validation": {
|
||||
"hardwareRequired": "Hardware fingerprint is required",
|
||||
"hardwareMax": "Hardware fingerprint cannot exceed 200 characters",
|
||||
"remarkMax": "Remark cannot exceed 3000 characters"
|
||||
},
|
||||
"messages": {
|
||||
"success": "License created successfully",
|
||||
"error": "Failed to create license",
|
||||
"missingAuthorization": "Missing authorization code information",
|
||||
"copySuccess": "Authorization code copied",
|
||||
"copyError": "Failed to copy authorization code"
|
||||
},
|
||||
"buttons": {
|
||||
"cancel": "Cancel",
|
||||
"submit": "Create",
|
||||
"copy": "Copy"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"copyCode": "Copy License Code",
|
||||
"updateLicense": "Update License",
|
||||
@@ -574,7 +622,7 @@
|
||||
"customerNotExist": "Customer data does not exist"
|
||||
},
|
||||
"basicInfo": {
|
||||
"customerId": "Customer ID",
|
||||
"customerCode": "Customer Code",
|
||||
"customerName": "Customer Name",
|
||||
"activatedDevices": "Activated Devices Count",
|
||||
"creationTime": "Creation Time",
|
||||
|
||||
@@ -316,7 +316,7 @@
|
||||
"authorizationInfo": {
|
||||
"basicInfo": "基本情報",
|
||||
"customerName": "顧客名",
|
||||
"customerId": "顧客ID",
|
||||
"customerCode": "顧客コード",
|
||||
"createdBy": "作成者",
|
||||
"description": "備考",
|
||||
"title": "認証情報",
|
||||
@@ -334,6 +334,54 @@
|
||||
"code": "認証コード",
|
||||
"tipText": "この認証コードを顧客に提供してください。顧客がソフトウェア製品で設定すると、ソフトウェアライセンスを自動的に取得できます"
|
||||
},
|
||||
"licenseInfo": {
|
||||
"addButton": "ライセンスを追加",
|
||||
"emptyDescription": "デバイスに紐づくライセンスはありません",
|
||||
"deviceTitle": "デバイス{index}",
|
||||
"deviceAddress": "デバイスアドレス",
|
||||
"lastActiveTime": "最終アクティブ時間",
|
||||
"hardwareSectionTitle": "ハードウェア情報",
|
||||
"hardwareSectionSubtitle": "(バインド情報)",
|
||||
"hardwareFingerprint": "ハードウェア指紋",
|
||||
"softwareSectionTitle": "ソフトウェアライセンス",
|
||||
"softwareSectionSubtitle": "(システム検証用)",
|
||||
"licenseKey": "ライセンスキー",
|
||||
"licenseStatus": "ライセンス状態",
|
||||
"statusDescription": "このライセンスは指定ハードウェアにバインドされ、システム検証に利用されます",
|
||||
"messages": {
|
||||
"loadError": "デバイスライセンスの取得に失敗しました"
|
||||
}
|
||||
},
|
||||
"licenseModal": {
|
||||
"title": "ライセンスを追加",
|
||||
"description": "この認証コードをハードウェア指紋に紐づけてください。",
|
||||
"fields": {
|
||||
"authorizationCode": "認証コード",
|
||||
"hardwareFingerprint": "ハードウェア指紋",
|
||||
"remark": "備考"
|
||||
},
|
||||
"placeholders": {
|
||||
"hardwareFingerprint": "ハードウェア指紋を入力してください",
|
||||
"remark": "備考(任意)を入力してください"
|
||||
},
|
||||
"validation": {
|
||||
"hardwareRequired": "ハードウェア指紋を入力してください",
|
||||
"hardwareMax": "ハードウェア指紋は200文字以内で入力してください",
|
||||
"remarkMax": "備考は3000文字以内で入力してください"
|
||||
},
|
||||
"messages": {
|
||||
"success": "ライセンスを作成しました",
|
||||
"error": "ライセンスの作成に失敗しました",
|
||||
"missingAuthorization": "認証コード情報が見つかりません",
|
||||
"copySuccess": "認証コードをコピーしました",
|
||||
"copyError": "認証コードのコピーに失敗しました"
|
||||
},
|
||||
"buttons": {
|
||||
"cancel": "キャンセル",
|
||||
"submit": "作成",
|
||||
"copy": "コピー"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"copyCode": "ライセンスコードをコピー",
|
||||
"updateLicense": "ライセンス更新",
|
||||
@@ -574,7 +622,7 @@
|
||||
"customerNotExist": "顧客データが存在しません"
|
||||
},
|
||||
"basicInfo": {
|
||||
"customerId": "顧客ID",
|
||||
"customerCode": "顧客コード",
|
||||
"customerName": "顧客名",
|
||||
"activatedDevices": "アクティベート済みデバイス数",
|
||||
"creationTime": "作成日時",
|
||||
|
||||
@@ -316,7 +316,7 @@
|
||||
"authorizationInfo": {
|
||||
"basicInfo": "基本信息",
|
||||
"customerName": "客户名称",
|
||||
"customerId": "客户ID",
|
||||
"customerCode": "客户编码",
|
||||
"createdBy": "创建人",
|
||||
"description": "备注",
|
||||
"title": "授权信息",
|
||||
@@ -334,6 +334,54 @@
|
||||
"code": "授权码",
|
||||
"tipText": "将此授权码提供给客户,客户在软件产品中配置后可自动获取软件许可证"
|
||||
},
|
||||
"licenseInfo": {
|
||||
"addButton": "新增许可证",
|
||||
"emptyDescription": "暂无设备许可证信息",
|
||||
"deviceTitle": "设备{index}",
|
||||
"deviceAddress": "设备地址",
|
||||
"lastActiveTime": "最后活跃时间",
|
||||
"hardwareSectionTitle": "硬件信息",
|
||||
"hardwareSectionSubtitle": "(绑定信息)",
|
||||
"hardwareFingerprint": "硬件指纹",
|
||||
"softwareSectionTitle": "软件许可证",
|
||||
"softwareSectionSubtitle": "(用于系统验证)",
|
||||
"licenseKey": "许可证密钥",
|
||||
"licenseStatus": "许可证状态",
|
||||
"statusDescription": "此许可证已生成并绑定到指定硬件,可用于系统运行验证",
|
||||
"messages": {
|
||||
"loadError": "获取设备列表失败"
|
||||
}
|
||||
},
|
||||
"licenseModal": {
|
||||
"title": "新增许可证",
|
||||
"description": "请填写硬件指纹信息,将授权码绑定到指定设备。",
|
||||
"fields": {
|
||||
"authorizationCode": "授权码",
|
||||
"hardwareFingerprint": "硬件指纹",
|
||||
"remark": "备注"
|
||||
},
|
||||
"placeholders": {
|
||||
"hardwareFingerprint": "请输入硬件指纹信息",
|
||||
"remark": "请输入备注信息(可选)"
|
||||
},
|
||||
"validation": {
|
||||
"hardwareRequired": "请输入硬件指纹",
|
||||
"hardwareMax": "硬件指纹长度不能超过200个字符",
|
||||
"remarkMax": "备注长度不能超过3000个字符"
|
||||
},
|
||||
"messages": {
|
||||
"success": "许可证创建成功",
|
||||
"error": "许可证创建失败",
|
||||
"missingAuthorization": "缺少授权码信息",
|
||||
"copySuccess": "授权码已复制",
|
||||
"copyError": "复制授权码失败"
|
||||
},
|
||||
"buttons": {
|
||||
"cancel": "取消",
|
||||
"submit": "创建",
|
||||
"copy": "复制"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"copyCode": "复制授权码",
|
||||
"updateLicense": "变更授权",
|
||||
@@ -574,7 +622,7 @@
|
||||
"customerNotExist": "客户数据不存在"
|
||||
},
|
||||
"basicInfo": {
|
||||
"customerId": "客户ID",
|
||||
"customerCode": "客户编码",
|
||||
"customerName": "客户名称",
|
||||
"activatedDevices": "已激活设备数量",
|
||||
"creationTime": "创建时间",
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<div class="info-value">{{ licenseData?.customer_info.customer_name|| '-' }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ t('pages.licenses.detail.authorizationInfo.customerId') }}:</div>
|
||||
<div class="info-value">{{ licenseData?.customer_id || '-' }}</div>
|
||||
<div class="info-label">{{ t('pages.licenses.detail.authorizationInfo.customerCode') }}:</div>
|
||||
<div class="info-value">{{ licenseData?.customer_info?.customer_code || '-' }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ t('pages.licenses.detail.authorizationInfo.createdBy') }}:</div>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<!-- 基础信息部分 -->
|
||||
<div class="info-section">
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('customers.basicInfo.customerId') }}</div>
|
||||
<div class="info-value">{{ licenseData?.customer_id || '-' }}</div>
|
||||
<div class="info-label">{{ $t('customers.basicInfo.customerCode') }}</div>
|
||||
<div class="info-value">{{ licenseData?.customer_info?.customer_code || '-' }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('customers.basicInfo.customerName') }}</div>
|
||||
@@ -17,7 +17,7 @@
|
||||
<!-- 已激活设备数量 -->
|
||||
<div class="stat-card stat-card-1">
|
||||
<div class="card-content">
|
||||
<div class="stat-value">{{ licenseData?.activated_licenses_count || 0 }}</div>
|
||||
<div class="stat-value">{{ getActivatedLicenses(licenseData) }}</div>
|
||||
<div class="stat-label">{{ $t('customers.basicInfo.activatedDevices') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,6 +59,17 @@ interface Props {
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const getActivatedLicenses = (license?: AuthorizationCode | null) => {
|
||||
if (!license) return 0
|
||||
if (typeof license.current_activations === 'number') {
|
||||
return license.current_activations
|
||||
}
|
||||
if (typeof license.activated_licenses_count === 'number') {
|
||||
return license.activated_licenses_count
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const formatDateShort = (date?: string) => {
|
||||
if (!date) return '-'
|
||||
return formatDateUtil(date)
|
||||
|
||||
@@ -1,48 +1,116 @@
|
||||
<template>
|
||||
<div class="license-info-container" v-loading="loading">
|
||||
<el-dialog
|
||||
v-model="addLicenseVisible"
|
||||
class="license-add-dialog"
|
||||
:title="t('pages.licenses.detail.licenseModal.title')"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
align-center
|
||||
@closed="resetAddForm"
|
||||
>
|
||||
<p class="dialog-description">
|
||||
{{ t('pages.licenses.detail.licenseModal.description') }}
|
||||
</p>
|
||||
<el-form
|
||||
ref="addFormRef"
|
||||
:model="addForm"
|
||||
:rules="addFormRules"
|
||||
label-position="top"
|
||||
class="license-form-dialog"
|
||||
>
|
||||
<el-form-item :label="t('pages.licenses.detail.licenseModal.fields.authorizationCode')">
|
||||
<div class="auth-code-row">
|
||||
<el-input :model-value="authorizationCodeValue" readonly />
|
||||
<el-button text type="primary" class="copy-btn" @click="handleCopyAuthorizationCode">
|
||||
{{ t('pages.licenses.detail.licenseModal.buttons.copy') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="t('pages.licenses.detail.licenseModal.fields.hardwareFingerprint')"
|
||||
prop="hardware_fingerprint"
|
||||
>
|
||||
<el-input
|
||||
v-model="addForm.hardware_fingerprint"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
:placeholder="t('pages.licenses.detail.licenseModal.placeholders.hardwareFingerprint')"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('pages.licenses.detail.licenseModal.fields.remark')" prop="remark">
|
||||
<el-input
|
||||
v-model="addForm.remark"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
:placeholder="t('pages.licenses.detail.licenseModal.placeholders.remark')"
|
||||
maxlength="3000"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleDialogCancel" :disabled="submitting">
|
||||
{{ t('pages.licenses.detail.licenseModal.buttons.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submitAddLicense" :loading="submitting">
|
||||
{{ t('pages.licenses.detail.licenseModal.buttons.submit') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新增许可证按钮 -->
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="handleAddLicense">新增许可证</el-button>
|
||||
<el-button type="primary" @click="handleAddLicense">{{
|
||||
t('pages.licenses.detail.licenseInfo.addButton')
|
||||
}}</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
<div v-if="!loading && devices.length > 0" class="devices-list">
|
||||
<div v-for="(device, index) in devices" :key="device.id" class="device-card">
|
||||
<div v-for="(device, index) in devices" :key="device.id" class="device-card">
|
||||
<!-- 设备头部 -->
|
||||
<div class="device-header">
|
||||
<div class="device-title-row">
|
||||
<span class="device-title">设备{{ index + 1 }}</span>
|
||||
<span class="device-title">
|
||||
{{ t('pages.licenses.detail.licenseInfo.deviceTitle', { index: index + 1 }) }}
|
||||
</span>
|
||||
<el-tag :type="device.is_online ? 'success' : 'danger'" size="small">
|
||||
{{ device.is_online_display}}
|
||||
{{ device.is_online_display }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="device-info-row">
|
||||
<span class="device-info-item">设备地址:{{ device.last_online_ip || device.activation_ip || '-' }}</span>
|
||||
<span class="device-info-item">最后活跃时间:{{ formatDateTime(device.last_heartbeat || '') }}</span>
|
||||
<span class="device-info-item">
|
||||
{{ t('pages.licenses.detail.licenseInfo.deviceAddress') }}:
|
||||
{{ device.last_online_ip || device.activation_ip || '-' }}
|
||||
</span>
|
||||
<span class="device-info-item">
|
||||
{{ t('pages.licenses.detail.licenseInfo.lastActiveTime') }}:
|
||||
{{ formatDateTime(device.last_heartbeat || '') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 硬件信息 -->
|
||||
<div class="info-section hardware-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">硬件信息</span>
|
||||
<span class="section-subtitle">(绑定信息)</span>
|
||||
<span class="section-title">
|
||||
{{ t('pages.licenses.detail.licenseInfo.hardwareSectionTitle') }}
|
||||
</span>
|
||||
<span class="section-subtitle">
|
||||
{{ t('pages.licenses.detail.licenseInfo.hardwareSectionSubtitle') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-row">
|
||||
<div class="info-label">机器序列号</div>
|
||||
<div class="info-value">{{ device.device_info?.machine_code || '-' }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">MAC地址</div>
|
||||
<div class="info-value">{{ device.device_info?.mac_address || '-' }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">CPU ID</div>
|
||||
<div class="info-value">{{ device.device_info?.cpu_id || '-' }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">硬件哈希</div>
|
||||
<div class="info-label">
|
||||
{{ t('pages.licenses.detail.licenseInfo.hardwareFingerprint') }}
|
||||
</div>
|
||||
<div class="info-value">{{ device.hardware_fingerprint || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,17 +119,30 @@
|
||||
<!-- 软件许可证 -->
|
||||
<div class="info-section license-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">软件许可证</span>
|
||||
<span class="section-subtitle">(用于系统验证)</span>
|
||||
<span class="section-title">
|
||||
{{ t('pages.licenses.detail.licenseInfo.softwareSectionTitle') }}
|
||||
</span>
|
||||
<span class="section-subtitle">
|
||||
{{ t('pages.licenses.detail.licenseInfo.softwareSectionSubtitle') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="license-content">
|
||||
<div class="license-row">
|
||||
<div class="license-label">许可证密钥</div>
|
||||
<div class="license-label">
|
||||
{{ t('pages.licenses.detail.licenseInfo.licenseKey') }}
|
||||
</div>
|
||||
<div class="license-value">{{ device.license_key || '-' }}</div>
|
||||
</div>
|
||||
<div class="license-row">
|
||||
<div class="license-label">许可证状态</div>
|
||||
<div class="license-value status-text">{{ device.status_display || '此许可证已生成并绑定到指定硬件,可用于系统运行验证' }}</div>
|
||||
<div class="license-label">
|
||||
{{ t('pages.licenses.detail.licenseInfo.licenseStatus') }}
|
||||
</div>
|
||||
<div class="license-value status-text">
|
||||
{{
|
||||
device.status_display ||
|
||||
t('pages.licenses.detail.licenseInfo.statusDescription')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,18 +151,21 @@
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!loading && devices.length === 0" class="empty-state">
|
||||
<el-empty description="暂无设备许可证信息">
|
||||
<el-button type="primary" @click="handleAddLicense">新增许可证</el-button>
|
||||
<el-empty :description="t('pages.licenses.detail.licenseInfo.emptyDescription')">
|
||||
<el-button type="primary" @click="handleAddLicense">
|
||||
{{ t('pages.licenses.detail.licenseInfo.addButton') }}
|
||||
</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { AuthorizationCode, LicenseDevice } from '@/api/license'
|
||||
import { getLicenseDevices, getLicenseDeviceDetail } from '@/api/license'
|
||||
import { ref, onMounted, watch, reactive, computed, nextTick } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { AuthorizationCode, LicenseDevice, LicenseDeviceCreateRequest } from '@/api/license'
|
||||
import { getLicenseDevices, getLicenseDeviceDetail, createLicenseDevice } from '@/api/license'
|
||||
import { formatDate } from '@/utils/date'
|
||||
|
||||
interface Props {
|
||||
@@ -89,13 +173,53 @@ interface Props {
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const devices = ref<LicenseDevice[]>([])
|
||||
const loading = ref(false)
|
||||
const addLicenseVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const addFormRef = ref<FormInstance>()
|
||||
const addForm = reactive({
|
||||
hardware_fingerprint: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const authorizationCodeId = computed(() => props.licenseData?.id || '')
|
||||
const authorizationCodeValue = computed(() => props.licenseData?.code || '--')
|
||||
|
||||
const validateHardware = (_rule: any, value: string, callback: (error?: Error) => void) => {
|
||||
const trimmed = value?.trim()
|
||||
if (!trimmed) {
|
||||
callback(new Error(t('pages.licenses.detail.licenseModal.validation.hardwareRequired')))
|
||||
return
|
||||
}
|
||||
if (trimmed.length > 200) {
|
||||
callback(new Error(t('pages.licenses.detail.licenseModal.validation.hardwareMax')))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
|
||||
const validateRemark = (_rule: any, value: string, callback: (error?: Error) => void) => {
|
||||
const trimmed = value?.trim()
|
||||
if (trimmed && trimmed.length > 3000) {
|
||||
callback(new Error(t('pages.licenses.detail.licenseModal.validation.remarkMax')))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
|
||||
const addFormRules: FormRules = {
|
||||
hardware_fingerprint: [
|
||||
{ validator: validateHardware, trigger: ['blur', 'change'] }
|
||||
],
|
||||
remark: [{ validator: validateRemark, trigger: ['blur', 'change'] }]
|
||||
}
|
||||
|
||||
// 获取设备列表
|
||||
const fetchDevices = async () => {
|
||||
if (!props.licenseData?.id) {
|
||||
if (!authorizationCodeId.value) {
|
||||
devices.value = []
|
||||
return
|
||||
}
|
||||
@@ -103,7 +227,7 @@ const fetchDevices = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getLicenseDevices({
|
||||
authorization_code_id: props.licenseData.id,
|
||||
authorization_code_id: authorizationCodeId.value,
|
||||
page: 1,
|
||||
page_size: 100
|
||||
})
|
||||
@@ -133,7 +257,7 @@ const fetchDevices = async () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取设备列表失败:', error)
|
||||
ElMessage.error('获取设备列表失败')
|
||||
ElMessage.error(t('pages.licenses.detail.licenseInfo.messages.loadError'))
|
||||
devices.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -146,9 +270,12 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
// 监听licenseData变化,重新获取数据
|
||||
watch(() => props.licenseData?.id, () => {
|
||||
fetchDevices()
|
||||
})
|
||||
watch(
|
||||
() => authorizationCodeId.value,
|
||||
() => {
|
||||
fetchDevices()
|
||||
}
|
||||
)
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (dateTime: string) => {
|
||||
@@ -156,10 +283,81 @@ const formatDateTime = (dateTime: string) => {
|
||||
return formatDate(dateTime)
|
||||
}
|
||||
|
||||
// 处理新增许可证
|
||||
const resetAddForm = () => {
|
||||
addForm.hardware_fingerprint = ''
|
||||
addForm.remark = ''
|
||||
nextTick(() => {
|
||||
addFormRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
const handleDialogCancel = () => {
|
||||
addLicenseVisible.value = false
|
||||
resetAddForm()
|
||||
}
|
||||
|
||||
const handleAddLicense = () => {
|
||||
// TODO: 实现新增许可证逻辑
|
||||
console.log('新增许可证')
|
||||
if (!authorizationCodeId.value) {
|
||||
ElMessage.warning(t('pages.licenses.detail.licenseModal.messages.missingAuthorization'))
|
||||
return
|
||||
}
|
||||
addLicenseVisible.value = true
|
||||
nextTick(() => {
|
||||
addFormRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
const handleCopyAuthorizationCode = async () => {
|
||||
if (!authorizationCodeValue.value || authorizationCodeValue.value === '--') {
|
||||
ElMessage.warning(t('pages.licenses.detail.licenseModal.messages.missingAuthorization'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(authorizationCodeValue.value)
|
||||
ElMessage.success(t('pages.licenses.detail.licenseModal.messages.copySuccess'))
|
||||
} catch (error) {
|
||||
console.error('复制授权码失败:', error)
|
||||
ElMessage.error(t('pages.licenses.detail.licenseModal.messages.copyError'))
|
||||
}
|
||||
}
|
||||
|
||||
const submitAddLicense = async () => {
|
||||
if (!addFormRef.value) return
|
||||
|
||||
try {
|
||||
await addFormRef.value.validate()
|
||||
if (!authorizationCodeId.value) {
|
||||
throw new Error(t('pages.licenses.detail.licenseModal.messages.missingAuthorization'))
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
const payload: LicenseDeviceCreateRequest = {
|
||||
authorization_code_id: authorizationCodeId.value,
|
||||
hardware_fingerprint: addForm.hardware_fingerprint.trim()
|
||||
}
|
||||
|
||||
if (addForm.remark.trim()) {
|
||||
payload.device_info = { remark: addForm.remark.trim() }
|
||||
}
|
||||
|
||||
const response = await createLicenseDevice(payload)
|
||||
if (response.code === '000000') {
|
||||
ElMessage.success(
|
||||
response.message || t('pages.licenses.detail.licenseModal.messages.success')
|
||||
)
|
||||
addLicenseVisible.value = false
|
||||
resetAddForm()
|
||||
await fetchDevices()
|
||||
} else {
|
||||
throw new Error(response.message || t('pages.licenses.detail.licenseModal.messages.error'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('创建许可证失败:', error)
|
||||
ElMessage.error(error.message || t('pages.licenses.detail.licenseModal.messages.error'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -168,6 +366,68 @@ const handleAddLicense = () => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.license-add-dialog {
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__header) {
|
||||
margin-right: 0;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__title) {
|
||||
font-family: 'Source Han Sans CN', sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1d1d1d;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 16px 24px 0;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__footer) {
|
||||
padding: 0 24px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-description {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 22px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.license-form-dialog {
|
||||
:deep(.el-form-item__label) {
|
||||
font-weight: 500;
|
||||
color: #1d1d1d;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-code-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
background: rgba(247, 248, 250, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
margin-bottom: 16px;
|
||||
|
||||
|
||||
@@ -67,16 +67,16 @@
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<div
|
||||
<div
|
||||
class="progress-container"
|
||||
:class="{ 'zero-activated': (scope.row.activated_licenses_count || 0) === 0 }"
|
||||
:class="{ 'zero-activated': getActivatedCount(scope.row) === 0 }"
|
||||
>
|
||||
<el-progress
|
||||
:percentage="getActivationProgress(scope.row)"
|
||||
:stroke-width="8"
|
||||
:show-text="true"
|
||||
:text-inside="true"
|
||||
:format="() => `${scope.row.activated_licenses_count || 0}/${scope.row.max_activations}`"
|
||||
:format="() => `${getActivatedCount(scope.row)}/${scope.row.max_activations}`"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -185,11 +185,21 @@ const getStatusClass = (status: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getActivatedCount = (row: AuthorizationCode) => {
|
||||
if (typeof row.current_activations === 'number') {
|
||||
return row.current_activations
|
||||
}
|
||||
if (typeof row.activated_licenses_count === 'number') {
|
||||
return row.activated_licenses_count
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const getActivationProgress = (row: AuthorizationCode) => {
|
||||
if (!row.max_activations || row.max_activations === 0) {
|
||||
return 0
|
||||
}
|
||||
const activated = row.activated_licenses_count || 0
|
||||
const activated = getActivatedCount(row)
|
||||
return Math.round((activated / row.max_activations) * 100)
|
||||
}
|
||||
|
||||
@@ -613,7 +623,7 @@ onMounted(async () => {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
// 当 activated_licenses_count 为 0 时,文字显示为红色
|
||||
// 当激活数量为 0 时,文字显示为红色
|
||||
&.zero-activated {
|
||||
:deep(.el-progress-bar__innerText),
|
||||
:deep(.el-progress__text) {
|
||||
|
||||
Reference in new issue
Block a user