diff --git a/frontend/src/api/https/index.ts b/frontend/src/api/https/index.ts index 52352cb..cc727db 100644 --- a/frontend/src/api/https/index.ts +++ b/frontend/src/api/https/index.ts @@ -74,7 +74,11 @@ Axios.interceptors.request.use(config => { }) // 添加响应拦截器 Axios.interceptors.response.use(response => { - // console.log(response) + // 处理文件下载等二进制响应 + if (response.config?.responseType === 'blob') { + return response + } + const apiData = response.data as any // 这个 Code 是和后端约定的业务 Code const code = apiData.code diff --git a/frontend/src/api/license.ts b/frontend/src/api/license.ts index b32ef19..a4d393a 100644 --- a/frontend/src/api/license.ts +++ b/frontend/src/api/license.ts @@ -1,3 +1,4 @@ +import type { AxiosResponse } from 'axios' import Axios from './https' // 授权码(AuthorizationCode)类型 - 供授权管理页面使用 export interface AuthorizationCode { @@ -303,6 +304,15 @@ export function getLicenseDeviceDetail(id: string): Promise> { + return Axios.get(`/api/v1/licenses/${id}/download`, { + responseType: 'blob' + }) +} + // 创建许可证请求(手动绑定设备) export interface LicenseDeviceCreateRequest { authorization_code_id: string diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 608016f..62e1b7d 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -348,8 +348,12 @@ "licenseKey": "License key", "licenseStatus": "License status", "statusDescription": "This license is generated and bound to the specified hardware for runtime validation.", + "downloadButton": "Download License File", "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": { @@ -627,7 +631,13 @@ "activatedDevices": "Activated Devices Count", "creationTime": "Creation 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)" + } } } } \ No newline at end of file diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json index 71e0c3f..8fc7bb4 100644 --- a/frontend/src/i18n/locales/ja.json +++ b/frontend/src/i18n/locales/ja.json @@ -348,8 +348,12 @@ "licenseKey": "ライセンスキー", "licenseStatus": "ライセンス状態", "statusDescription": "このライセンスは指定ハードウェアにバインドされ、システム検証に利用されます", + "downloadButton": "ライセンスファイルをダウンロード", "messages": { - "loadError": "デバイスライセンスの取得に失敗しました" + "loadError": "デバイスライセンスの取得に失敗しました", + "downloadSuccess": "ライセンスファイルのダウンロードを開始しました", + "downloadError": "ライセンスファイルのダウンロードに失敗しました", + "missingLicense": "ライセンス情報が見つかりません" } }, "licenseModal": { @@ -627,7 +631,13 @@ "activatedDevices": "アクティベート済みデバイス数", "creationTime": "作成日時", "expiryTime": "有効期限", - "remainingDays": "残り日数" + "remainingDays": "残り時間", + "remainingTime": { + "permanent": "永久", + "years": "{years}年", + "yearMonth": "{years}年{months}ヶ月", + "days": "{days}日" + } } } } \ No newline at end of file diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json index f45d0da..fc7eb8b 100644 --- a/frontend/src/i18n/locales/zh.json +++ b/frontend/src/i18n/locales/zh.json @@ -348,8 +348,12 @@ "licenseKey": "许可证密钥", "licenseStatus": "许可证状态", "statusDescription": "此许可证已生成并绑定到指定硬件,可用于系统运行验证", + "downloadButton": "许可证文件下载", "messages": { - "loadError": "获取设备列表失败" + "loadError": "获取设备列表失败", + "downloadSuccess": "许可证文件已开始下载", + "downloadError": "下载许可证文件失败", + "missingLicense": "未找到许可证信息" } }, "licenseModal": { @@ -627,7 +631,13 @@ "activatedDevices": "已激活设备数量", "creationTime": "创建时间", "expiryTime": "到期时间", - "remainingDays": "剩余时间(天)" + "remainingDays": "剩余时间", + "remainingTime": { + "permanent": "永久", + "years": "{years}年", + "yearMonth": "{years}年{months}个月", + "days": "{days}天" + } } } } \ No newline at end of file diff --git a/frontend/src/views/Licenses/LicensesView/components/AuthorizationInfo.vue b/frontend/src/views/Licenses/LicensesView/components/AuthorizationInfo.vue index c337f82..9788607 100644 --- a/frontend/src/views/Licenses/LicensesView/components/AuthorizationInfo.vue +++ b/frontend/src/views/Licenses/LicensesView/components/AuthorizationInfo.vue @@ -12,10 +12,6 @@
{{ t('pages.licenses.detail.authorizationInfo.customerCode') }}:
{{ licenseData?.customer_info?.customer_code || '-' }}
-
-
{{ t('pages.licenses.detail.authorizationInfo.createdBy') }}:
-
{{ licenseData?.created_by || '-' }}
-
{{ t('pages.licenses.detail.authorizationInfo.description') }}:
{{ licenseData?.description || '-' }}
diff --git a/frontend/src/views/Licenses/LicensesView/components/BasicInfo.vue b/frontend/src/views/Licenses/LicensesView/components/BasicInfo.vue index 53ce023..a5076fc 100644 --- a/frontend/src/views/Licenses/LicensesView/components/BasicInfo.vue +++ b/frontend/src/views/Licenses/LicensesView/components/BasicInfo.vue @@ -38,10 +38,10 @@
- +
-
{{ getRemainingDays(licenseData?.end_date) }}
+
{{ formatRemainingTime(licenseData?.end_date) }}
{{ $t('customers.basicInfo.remainingDays') }}
@@ -52,12 +52,14 @@ diff --git a/frontend/src/views/Licenses/LicensesView/components/LicenseInfo.vue b/frontend/src/views/Licenses/LicensesView/components/LicenseInfo.vue index f130737..8ba4eef 100644 --- a/frontend/src/views/Licenses/LicensesView/components/LicenseInfo.vue +++ b/frontend/src/views/Licenses/LicensesView/components/LicenseInfo.vue @@ -146,6 +146,18 @@ + + +
+ + {{ t('pages.licenses.detail.licenseInfo.downloadButton') }} + +
@@ -165,7 +177,7 @@ 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 { getLicenseDevices, getLicenseDeviceDetail, createLicenseDevice, downloadLicenseFile } from '@/api/license' import { formatDate } from '@/utils/date' interface Props { @@ -184,6 +196,7 @@ const addForm = reactive({ hardware_fingerprint: '', remark: '' }) +const downloadingMap = reactive>({}) const authorizationCodeId = computed(() => props.licenseData?.id || '') 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 () => { 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 {