feat: 添加许可证撤销和下载功能,更新国际化文本,优化许可证信息展示

This commit is contained in:
wood911010 committed 2025-11-21 22:21:45 +08:00
1 parent bed116fcb7
commit 1f25df980d
8 files changed
+171 -19

No files matched your search

+5 -1
View File
@@ -74,7 +74,11 @@ Axios.interceptors.request.use(config => {
}) })
// 添加响应拦截器 // 添加响应拦截器
Axios.interceptors.response.use(response => { Axios.interceptors.response.use(response => {
// console.log(response) // 处理文件下载等二进制响应
if (response.config?.responseType === 'blob') {
return response
}
const apiData = response.data as any const apiData = response.data as any
// 这个 Code 是和后端约定的业务 Code // 这个 Code 是和后端约定的业务 Code
const code = apiData.code const code = apiData.code
+10
View File
@@ -1,3 +1,4 @@
import type { AxiosResponse } from 'axios'
import Axios from './https' import Axios from './https'
// 授权码(AuthorizationCode)类型 - 供授权管理页面使用 // 授权码(AuthorizationCode)类型 - 供授权管理页面使用
export interface AuthorizationCode { export interface AuthorizationCode {
@@ -303,6 +304,15 @@ export function getLicenseDeviceDetail(id: string): Promise<ApiResponse<LicenseD
return Axios.get(`/api/v1/licenses/${id}`) return Axios.get(`/api/v1/licenses/${id}`)
} }
/**
* 下载许可证文件
*/
export function downloadLicenseFile(id: string): Promise<AxiosResponse<Blob>> {
return Axios.get(`/api/v1/licenses/${id}/download`, {
responseType: 'blob'
})
}
// 创建许可证请求(手动绑定设备) // 创建许可证请求(手动绑定设备)
export interface LicenseDeviceCreateRequest { export interface LicenseDeviceCreateRequest {
authorization_code_id: string authorization_code_id: string
+12 -2
View File
@@ -348,8 +348,12 @@
"licenseKey": "License key", "licenseKey": "License key",
"licenseStatus": "License status", "licenseStatus": "License status",
"statusDescription": "This license is generated and bound to the specified hardware for runtime validation.", "statusDescription": "This license is generated and bound to the specified hardware for runtime validation.",
"downloadButton": "Download License File",
"messages": { "messages": {
"loadError": "Failed to load license devices" "loadError": "Failed to load license devices",
"downloadSuccess": "License file download started",
"downloadError": "Failed to download license file",
"missingLicense": "License information not found"
} }
}, },
"licenseModal": { "licenseModal": {
@@ -627,7 +631,13 @@
"activatedDevices": "Activated Devices Count", "activatedDevices": "Activated Devices Count",
"creationTime": "Creation Time", "creationTime": "Creation Time",
"expiryTime": "Expiry Time", "expiryTime": "Expiry Time",
"remainingDays": "Remaining Days" "remainingDays": "Remaining Time",
"remainingTime": {
"permanent": "Permanent",
"years": "{years} year(s)",
"yearMonth": "{years} year(s) {months} month(s)",
"days": "{days} day(s)"
}
} }
} }
} }
+12 -2
View File
@@ -348,8 +348,12 @@
"licenseKey": "ライセンスキー", "licenseKey": "ライセンスキー",
"licenseStatus": "ライセンス状態", "licenseStatus": "ライセンス状態",
"statusDescription": "このライセンスは指定ハードウェアにバインドされ、システム検証に利用されます", "statusDescription": "このライセンスは指定ハードウェアにバインドされ、システム検証に利用されます",
"downloadButton": "ライセンスファイルをダウンロード",
"messages": { "messages": {
"loadError": "デバイスライセンスの取得に失敗しました" "loadError": "デバイスライセンスの取得に失敗しました",
"downloadSuccess": "ライセンスファイルのダウンロードを開始しました",
"downloadError": "ライセンスファイルのダウンロードに失敗しました",
"missingLicense": "ライセンス情報が見つかりません"
} }
}, },
"licenseModal": { "licenseModal": {
@@ -627,7 +631,13 @@
"activatedDevices": "アクティベート済みデバイス数", "activatedDevices": "アクティベート済みデバイス数",
"creationTime": "作成日時", "creationTime": "作成日時",
"expiryTime": "有効期限", "expiryTime": "有効期限",
"remainingDays": "残り日数" "remainingDays": "残り時間",
"remainingTime": {
"permanent": "永久",
"years": "{years}年",
"yearMonth": "{years}年{months}ヶ月",
"days": "{days}日"
}
} }
} }
} }
+12 -2
View File
@@ -348,8 +348,12 @@
"licenseKey": "许可证密钥", "licenseKey": "许可证密钥",
"licenseStatus": "许可证状态", "licenseStatus": "许可证状态",
"statusDescription": "此许可证已生成并绑定到指定硬件,可用于系统运行验证", "statusDescription": "此许可证已生成并绑定到指定硬件,可用于系统运行验证",
"downloadButton": "许可证文件下载",
"messages": { "messages": {
"loadError": "获取设备列表失败" "loadError": "获取设备列表失败",
"downloadSuccess": "许可证文件已开始下载",
"downloadError": "下载许可证文件失败",
"missingLicense": "未找到许可证信息"
} }
}, },
"licenseModal": { "licenseModal": {
@@ -627,7 +631,13 @@
"activatedDevices": "已激活设备数量", "activatedDevices": "已激活设备数量",
"creationTime": "创建时间", "creationTime": "创建时间",
"expiryTime": "到期时间", "expiryTime": "到期时间",
"remainingDays": "剩余时间(天)" "remainingDays": "剩余时间",
"remainingTime": {
"permanent": "永久",
"years": "{years}年",
"yearMonth": "{years}年{months}个月",
"days": "{days}天"
}
} }
} }
} }
@@ -12,10 +12,6 @@
<div class="info-label">{{ t('pages.licenses.detail.authorizationInfo.customerCode') }}:</div> <div class="info-label">{{ t('pages.licenses.detail.authorizationInfo.customerCode') }}:</div>
<div class="info-value">{{ licenseData?.customer_info?.customer_code || '-' }}</div> <div class="info-value">{{ licenseData?.customer_info?.customer_code || '-' }}</div>
</div> </div>
<div class="info-row">
<div class="info-label">{{ t('pages.licenses.detail.authorizationInfo.createdBy') }}:</div>
<div class="info-value">{{ licenseData?.created_by || '-' }}</div>
</div>
<div class="info-row"> <div class="info-row">
<div class="info-label">{{ t('pages.licenses.detail.authorizationInfo.description') }}:</div> <div class="info-label">{{ t('pages.licenses.detail.authorizationInfo.description') }}:</div>
<div class="info-value">{{ licenseData?.description || '-' }}</div> <div class="info-value">{{ licenseData?.description || '-' }}</div>
@@ -38,10 +38,10 @@
</div> </div>
</div> </div>
<!-- 剩余时间(天) --> <!-- 剩余时间 -->
<div class="stat-card stat-card-4"> <div class="stat-card stat-card-4">
<div class="card-content"> <div class="card-content">
<div class="stat-value">{{ getRemainingDays(licenseData?.end_date) }}</div> <div class="stat-value">{{ formatRemainingTime(licenseData?.end_date) }}</div>
<div class="stat-label">{{ $t('customers.basicInfo.remainingDays') }}</div> <div class="stat-label">{{ $t('customers.basicInfo.remainingDays') }}</div>
</div> </div>
</div> </div>
@@ -52,12 +52,14 @@
<script setup lang="ts"> <script setup lang="ts">
import type { AuthorizationCode } from '@/api/license' import type { AuthorizationCode } from '@/api/license'
import { formatDateShort as formatDateUtil } from '@/utils/date' import { formatDateShort as formatDateUtil } from '@/utils/date'
import { useI18n } from 'vue-i18n'
interface Props { interface Props {
licenseData: AuthorizationCode | null licenseData: AuthorizationCode | null
} }
defineProps<Props>() defineProps<Props>()
const { t } = useI18n()
const getActivatedLicenses = (license?: AuthorizationCode | null) => { const getActivatedLicenses = (license?: AuthorizationCode | null) => {
if (!license) return 0 if (!license) return 0
@@ -75,13 +77,34 @@ const formatDateShort = (date?: string) => {
return formatDateUtil(date) return formatDateUtil(date)
} }
const getRemainingDays = (endDate?: string) => { const formatRemainingTime = (endDate?: string) => {
if (!endDate) return '-' if (!endDate) return '-'
const now = new Date()
const end = new Date(endDate) const end = new Date(endDate)
const diff = end.getTime() - now.getTime() if (Number.isNaN(end.getTime())) return '-'
const days = Math.ceil(diff / (1000 * 60 * 60 * 24)) const now = new Date()
return days > 0 ? days : 0 const diffMs = end.getTime() - now.getTime()
const days = Math.max(0, Math.ceil(diffMs / (1000 * 60 * 60 * 24)))
if (days > 36500) {
return t('customers.basicInfo.remainingTime.permanent')
}
if (days >= 731) {
const years = Math.floor(days / 365)
return t('customers.basicInfo.remainingTime.years', { years })
}
if (days >= 366) {
const years = Math.floor(days / 365)
const remainingDays = days - years * 365
if (remainingDays === 0) {
return t('customers.basicInfo.remainingTime.years', { years })
}
const months = Math.max(1, Math.round(remainingDays / 30))
return t('customers.basicInfo.remainingTime.yearMonth', { years, months })
}
return t('customers.basicInfo.remainingTime.days', { days })
} }
</script> </script>
@@ -146,6 +146,18 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 下载按钮 -->
<div class="download-section">
<el-button
class="download-license-btn"
type="primary"
:loading="isDownloading(device.id)"
@click="handleDownloadLicense(device)"
>
{{ t('pages.licenses.detail.licenseInfo.downloadButton') }}
</el-button>
</div>
</div> </div>
</div> </div>
@@ -165,7 +177,7 @@ import { ref, onMounted, watch, reactive, computed, nextTick } from 'vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus' import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { AuthorizationCode, LicenseDevice, LicenseDeviceCreateRequest } from '@/api/license' import type { AuthorizationCode, LicenseDevice, LicenseDeviceCreateRequest } from '@/api/license'
import { getLicenseDevices, getLicenseDeviceDetail, createLicenseDevice } from '@/api/license' import { getLicenseDevices, getLicenseDeviceDetail, createLicenseDevice, downloadLicenseFile } from '@/api/license'
import { formatDate } from '@/utils/date' import { formatDate } from '@/utils/date'
interface Props { interface Props {
@@ -184,6 +196,7 @@ const addForm = reactive({
hardware_fingerprint: '', hardware_fingerprint: '',
remark: '' remark: ''
}) })
const downloadingMap = reactive<Record<string, boolean>>({})
const authorizationCodeId = computed(() => props.licenseData?.id || '') const authorizationCodeId = computed(() => props.licenseData?.id || '')
const authorizationCodeValue = computed(() => props.licenseData?.code || '--') const authorizationCodeValue = computed(() => props.licenseData?.code || '--')
@@ -322,6 +335,61 @@ const handleCopyAuthorizationCode = async () => {
} }
} }
const isDownloading = (licenseId: string) => {
return Boolean(licenseId && downloadingMap[licenseId])
}
const extractFilename = (contentDisposition?: string) => {
if (!contentDisposition) return ''
const utfMatch = /filename\*=utf-8''([^;]+)/i.exec(contentDisposition)
if (utfMatch?.[1]) {
try {
return decodeURIComponent(utfMatch[1])
} catch (error) {
console.warn('Failed to decode filename from header:', error)
}
}
const asciiMatch = /filename="?([^"]+)"?/i.exec(contentDisposition)
return asciiMatch?.[1] || ''
}
const handleDownloadLicense = async (device: LicenseDevice) => {
if (!device?.id) {
ElMessage.warning(t('pages.licenses.detail.licenseInfo.messages.missingLicense'))
return
}
downloadingMap[device.id] = true
try {
const response = await downloadLicenseFile(device.id)
const disposition =
response.headers?.['content-disposition'] || response.headers?.['Content-Disposition']
const filename =
extractFilename(disposition) ||
`${device.customer_name || 'license'}-${device.id}.lic`
const blobUrl = window.URL.createObjectURL(response.data)
const link = document.createElement('a')
link.href = blobUrl
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(blobUrl)
ElMessage.success(t('pages.licenses.detail.licenseInfo.messages.downloadSuccess'))
} catch (error: any) {
console.error('下载许可证失败:', error)
const backendMessage = error?.backendMessage || error?.message
ElMessage.error(
backendMessage || t('pages.licenses.detail.licenseInfo.messages.downloadError')
)
} finally {
downloadingMap[device.id] = false
}
}
const submitAddLicense = async () => { const submitAddLicense = async () => {
if (!addFormRef.value) return if (!addFormRef.value) return
@@ -621,6 +689,27 @@ const submitAddLicense = async () => {
} }
} }
} }
.download-license-btn {
min-width: 148px;
background: #00C27C;
border: none;
color: #FFFFFF;
font-family: 'Source Han Sans CN', sans-serif;
font-size: 14px;
font-weight: 500;
line-height: 21px;
border-radius: 4px;
&:hover {
background: #019C7C;
}
}
}
.download-section {
display: flex;
justify-content: flex-start;
} }
.empty-state { .empty-state {