feat: 更新许可证管理功能,添加手动创建许可证的接口,增强授权信息展示,更新相关国际化文本
This commit is contained in:
1 parent
49e5952e1d
commit
2f683fa22b
11 files changed
+516
-61
No files matched your search
@@ -58,10 +58,11 @@ func (r *authorizationCodeRepository) GetAuthorizationCodeList(ctx context.Conte
|
||||
order = "desc"
|
||||
}
|
||||
|
||||
// 构建查询,包含状态计算
|
||||
// 构建查询,包含状态计算与当前激活数量
|
||||
query := r.db.WithContext(ctx).Table("authorization_codes ac").
|
||||
Select(`ac.id, ac.code, ac.customer_id, c.customer_name,
|
||||
ac.start_date, ac.end_date, ac.max_activations,
|
||||
COALESCE(l.active_count, 0) AS current_activations,
|
||||
ac.deployment_type, ac.is_locked, ac.description, ac.created_at,
|
||||
CASE
|
||||
WHEN ac.is_locked = true THEN 'locked'
|
||||
@@ -69,7 +70,13 @@ func (r *authorizationCodeRepository) GetAuthorizationCodeList(ctx context.Conte
|
||||
WHEN ac.start_date <= NOW() AND ac.end_date >= NOW() THEN 'normal'
|
||||
ELSE 'expired'
|
||||
END AS status`).
|
||||
Joins("LEFT JOIN customers c ON ac.customer_id = c.id AND c.deleted_at IS NULL")
|
||||
Joins("LEFT JOIN customers c ON ac.customer_id = c.id AND c.deleted_at IS NULL").
|
||||
Joins(`LEFT JOIN (
|
||||
SELECT authorization_code_id, COUNT(*) AS active_count
|
||||
FROM licenses
|
||||
WHERE status = 'active' AND deleted_at IS NULL
|
||||
GROUP BY authorization_code_id
|
||||
) l ON ac.id = l.authorization_code_id`)
|
||||
|
||||
// 添加筛选条件
|
||||
if req.CustomerID != "" {
|
||||
@@ -110,6 +117,7 @@ func (r *authorizationCodeRepository) GetAuthorizationCodeList(ctx context.Conte
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
MaxActivations int `json:"max_activations"`
|
||||
CurrentActivations int `json:"current_activations" gorm:"column:current_activations"`
|
||||
DeploymentType string `json:"deployment_type"`
|
||||
IsLocked bool `json:"is_locked"`
|
||||
Description *string `json:"description"`
|
||||
@@ -140,7 +148,7 @@ func (r *authorizationCodeRepository) GetAuthorizationCodeList(ctx context.Conte
|
||||
StartDate: result.StartDate,
|
||||
EndDate: result.EndDate,
|
||||
MaxActivations: result.MaxActivations,
|
||||
CurrentActivations: 0, // TODO: 从licenses表统计
|
||||
CurrentActivations: result.CurrentActivations,
|
||||
DeploymentType: result.DeploymentType,
|
||||
IsLocked: result.IsLocked,
|
||||
Description: result.Description,
|
||||
|
||||
@@ -125,14 +125,17 @@ func (s *licenseService) CreateLicense(ctx context.Context, req *models.LicenseC
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 构建许可证实体
|
||||
now := time.Now()
|
||||
|
||||
// 构建许可证实体,手动添加的离线许可证直接视为已激活
|
||||
license := &models.License{
|
||||
LicenseKey: licenseKey,
|
||||
AuthorizationCodeID: req.AuthorizationCodeID,
|
||||
CustomerID: authCode.CustomerID, // 从授权码获取客户ID
|
||||
HardwareFingerprint: req.HardwareFingerprint,
|
||||
ActivationIP: req.ActivationIP,
|
||||
Status: "inactive", // 手动创建的许可证默认为未激活状态
|
||||
Status: "active",
|
||||
ActivatedAt: &now,
|
||||
}
|
||||
|
||||
// 设置设备信息
|
||||
|
||||
@@ -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,8 +1,74 @@
|
||||
<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>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
@@ -11,38 +77,40 @@
|
||||
<!-- 设备头部 -->
|
||||
<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 class="info-label">
|
||||
{{ t('pages.licenses.detail.licenseInfo.hardwareFingerprint') }}
|
||||
</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-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, () => {
|
||||
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;
|
||||
|
||||
|
||||
@@ -69,14 +69,14 @@
|
||||
<template #default="scope">
|
||||
<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