feat: 添加许可证撤销和下载功能,更新国际化文本,优化许可证信息展示
This commit is contained in:
1 parent
bed116fcb7
commit
1f25df980d
8 files changed
+171
-19
No files matched your search
@@ -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
|
||||
|
||||
@@ -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<ApiResponse<LicenseD
|
||||
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 {
|
||||
authorization_code_id: string
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}日"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}天"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,6 @@
|
||||
<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>
|
||||
<div class="info-value">{{ licenseData?.created_by || '-' }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ t('pages.licenses.detail.authorizationInfo.description') }}:</div>
|
||||
<div class="info-value">{{ licenseData?.description || '-' }}</div>
|
||||
|
||||
@@ -38,10 +38,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 剩余时间(天) -->
|
||||
<!-- 剩余时间 -->
|
||||
<div class="stat-card stat-card-4">
|
||||
<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>
|
||||
</div>
|
||||
@@ -52,12 +52,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { AuthorizationCode } from '@/api/license'
|
||||
import { formatDateShort as formatDateUtil } from '@/utils/date'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
interface Props {
|
||||
licenseData: AuthorizationCode | null
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const getActivatedLicenses = (license?: AuthorizationCode | null) => {
|
||||
if (!license) return 0
|
||||
@@ -75,13 +77,34 @@ const formatDateShort = (date?: string) => {
|
||||
return formatDateUtil(date)
|
||||
}
|
||||
|
||||
const getRemainingDays = (endDate?: string) => {
|
||||
const formatRemainingTime = (endDate?: string) => {
|
||||
if (!endDate) return '-'
|
||||
const now = new Date()
|
||||
const end = new Date(endDate)
|
||||
const diff = end.getTime() - now.getTime()
|
||||
const days = Math.ceil(diff / (1000 * 60 * 60 * 24))
|
||||
return days > 0 ? days : 0
|
||||
if (Number.isNaN(end.getTime())) return '-'
|
||||
const now = new Date()
|
||||
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>
|
||||
|
||||
|
||||
@@ -146,6 +146,18 @@
|
||||
</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>
|
||||
|
||||
@@ -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<Record<string, boolean>>({})
|
||||
|
||||
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 {
|
||||
|
||||
Reference in new issue
Block a user