feat: 实现许可证撤销功能,更新相关国际化文本,优化许可证信息展示

This commit is contained in:
wood911010 committed 2025-11-21 22:36:25 +08:00
1 parent 1f25df980d
commit 0478c201db
6 files changed
+119 -33

No files matched your search

+7
View File
@@ -215,6 +215,13 @@ export function deactivateLicense(id: string): Promise<ApiResponse<Authorization
return Axios.patch(`/api/v1/authorization-codes/${id}/deactivate`)
}
/**
* 撤销许可证
*/
export function revokeLicense(id: string, reason: string): Promise<ApiResponse> {
return Axios.put(`/api/v1/licenses/${id}/revoke`, { reason })
}
// 许可证设备信息类型(与后端 LicenseListItem 对应)
export interface LicenseDevice {
id: string
+5
View File
@@ -405,6 +405,11 @@
"revokeTitle": "Warning",
"revokeConfirmButton": "Confirm Revoke",
"revokeCancelButton": "Cancel",
"revokeReasonPlaceholder": "Please enter revocation reason",
"revokeReasonRequired": "Revocation reason is required",
"revokeReasonMax": "Revocation reason cannot exceed 500 characters",
"revokeSuccess": "License revoked successfully",
"revokeError": "Failed to revoke the license, please try again later",
"loadError": "Failed to load license details",
"missingId": "Missing license ID"
}
+5
View File
@@ -405,6 +405,11 @@
"revokeTitle": "警告",
"revokeConfirmButton": "取り消し確認",
"revokeCancelButton": "キャンセル",
"revokeReasonPlaceholder": "取り消し理由を入力してください",
"revokeReasonRequired": "取り消し理由は必須です",
"revokeReasonMax": "取り消し理由は500文字以内で入力してください",
"revokeSuccess": "ライセンスを正常に取り消しました",
"revokeError": "ライセンスの取り消しに失敗しました。後でもう一度お試しください",
"loadError": "ライセンス詳細の読み込みに失敗しました",
"missingId": "ライセンスIDが不足しています"
}
+9 -4
View File
@@ -390,7 +390,7 @@
"copyCode": "复制授权码",
"updateLicense": "变更授权",
"renewLicense": "延期授权",
"revokeLicense": "吊销授权",
"revokeLicense": "撤销许可证",
"downloadCertificate": "下载部署包"
},
"messages": {
@@ -399,12 +399,17 @@
"codeEmpty": "授权码为空",
"updateComingSoon": "变更授权功能开发中...",
"renewComingSoon": "延期授权功能开发中...",
"revokeComingSoon": "吊销授权功能开发中...",
"revokeComingSoon": "撤销许可证功能开发中...",
"downloadComingSoon": "下载凭单功能开发中...",
"revokeConfirm": "确定要吊销此授权吗?此操作不可撤销!",
"revokeConfirm": "确定要撤销此许可证吗?此操作不可逆!",
"revokeTitle": "警告",
"revokeConfirmButton": "确定吊销",
"revokeConfirmButton": "确定撤销",
"revokeCancelButton": "取消",
"revokeReasonPlaceholder": "请输入撤销原因",
"revokeReasonRequired": "请填写撤销原因",
"revokeReasonMax": "撤销原因不能超过500个字符",
"revokeSuccess": "许可证已成功撤销",
"revokeError": "撤销许可证失败,请稍后重试",
"loadError": "加载授权详情失败",
"missingId": "缺少授权ID"
}
@@ -157,6 +157,16 @@
>
{{ t('pages.licenses.detail.licenseInfo.downloadButton') }}
</el-button>
<el-button
class="revoke-license-btn"
type="danger"
plain
:loading="isRevoking(device.id)"
:disabled="device.status === 'revoked'"
@click="handleRevokeLicense(device)"
>
{{ t('pages.licenses.detail.actions.revokeLicense') }}
</el-button>
</div>
</div>
</div>
@@ -174,16 +184,20 @@
<script setup lang="ts">
import { ref, onMounted, watch, reactive, computed, nextTick } from 'vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { useI18n } from 'vue-i18n'
import type { AuthorizationCode, LicenseDevice, LicenseDeviceCreateRequest } from '@/api/license'
import { getLicenseDevices, getLicenseDeviceDetail, createLicenseDevice, downloadLicenseFile } from '@/api/license'
import { getLicenseDevices, getLicenseDeviceDetail, createLicenseDevice, downloadLicenseFile, revokeLicense } from '@/api/license'
import { formatDate } from '@/utils/date'
interface Props {
licenseData: AuthorizationCode | null
}
const emit = defineEmits<{
(e: 'licenseRevoked'): void
}>()
const props = defineProps<Props>()
const { t } = useI18n()
@@ -197,6 +211,7 @@ const addForm = reactive({
remark: ''
})
const downloadingMap = reactive<Record<string, boolean>>({})
const revokingMap = reactive<Record<string, boolean>>({})
const authorizationCodeId = computed(() => props.licenseData?.id || '')
const authorizationCodeValue = computed(() => props.licenseData?.code || '--')
@@ -339,6 +354,10 @@ const isDownloading = (licenseId: string) => {
return Boolean(licenseId && downloadingMap[licenseId])
}
const isRevoking = (licenseId: string) => {
return Boolean(licenseId && revokingMap[licenseId])
}
const extractFilename = (contentDisposition?: string) => {
if (!contentDisposition) return ''
const utfMatch = /filename\*=utf-8''([^;]+)/i.exec(contentDisposition)
@@ -390,6 +409,57 @@ const handleDownloadLicense = async (device: LicenseDevice) => {
}
}
const handleRevokeLicense = async (device: LicenseDevice) => {
if (!device?.id) {
ElMessage.error(t('pages.licenses.detail.messages.missingId'))
return
}
if (device.status === 'revoked') {
return
}
let reason = ''
try {
const { value } = await ElMessageBox.prompt(
t('pages.licenses.detail.messages.revokeConfirm'),
t('pages.licenses.detail.messages.revokeTitle'),
{
confirmButtonText: t('pages.licenses.detail.messages.revokeConfirmButton'),
cancelButtonText: t('pages.licenses.detail.messages.revokeCancelButton'),
type: 'warning',
inputType: 'textarea',
inputPlaceholder: t('pages.licenses.detail.messages.revokeReasonPlaceholder'),
inputValidator: (val: string) => {
if (!val || !val.trim()) {
return t('pages.licenses.detail.messages.revokeReasonRequired')
}
if (val.trim().length > 500) {
return t('pages.licenses.detail.messages.revokeReasonMax')
}
return true
},
inputErrorMessage: t('pages.licenses.detail.messages.revokeReasonRequired')
}
)
reason = value.trim()
} catch {
return
}
revokingMap[device.id] = true
try {
await revokeLicense(device.id, reason)
ElMessage.success(t('pages.licenses.detail.messages.revokeSuccess'))
await fetchDevices()
emit('licenseRevoked')
} catch (error: any) {
ElMessage.error(error?.message || t('pages.licenses.detail.messages.revokeError'))
} finally {
revokingMap[device.id] = false
}
}
const submitAddLicense = async () => {
if (!addFormRef.value) return
@@ -710,6 +780,21 @@ const submitAddLicense = async () => {
.download-section {
display: flex;
justify-content: flex-start;
gap: 12px;
flex-wrap: wrap;
}
.revoke-license-btn {
min-width: 148px;
border: 1px solid #F0142F;
color: #F0142F;
background: transparent;
&:hover:not(.is-disabled) {
background: rgba(240, 20, 47, 0.08);
color: #d01228;
border-color: #d01228;
}
}
.empty-state {
@@ -62,9 +62,6 @@
<el-button class="action-btn-inline renew-btn" @click="handleRenewLicense">
{{ $t('pages.licenses.detail.actions.renewLicense') }}
</el-button>
<el-button class="action-btn-inline revoke-btn" @click="handleRevokeLicense">
{{ $t('pages.licenses.detail.actions.revokeLicense') }}
</el-button>
<el-button class="action-btn-inline download-btn" @click="handleDownloadCertificate">
{{ $t('pages.licenses.detail.actions.downloadCertificate') }}
</el-button>
@@ -75,7 +72,11 @@
<div class="tab-content-area">
<BasicInfo v-if="activeTab === 'basic'" :license-data="licenseData" />
<AuthorizationInfo v-if="activeTab === 'authorization'" :license-data="licenseData" />
<LicenseInfo v-if="activeTab === 'license'" :license-data="licenseData" />
<LicenseInfo
v-if="activeTab === 'license'"
:license-data="licenseData"
@license-revoked="loadLicenseData"
/>
<ChangeHistory v-if="activeTab === 'history'" :license-data="licenseData" />
</div>
</div>
@@ -86,7 +87,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n'
import { getLicenseDetail, type AuthorizationCode } from '@/api/license'
import LicenseTabIcon from '@/components/common/icons/LicenseTabIcon.vue'
@@ -141,18 +142,6 @@ const handleRenewLicense = () => {
ElMessage.info(t('pages.licenses.detail.messages.renewComingSoon'))
}
const handleRevokeLicense = () => {
ElMessageBox.confirm(t('pages.licenses.detail.messages.revokeConfirm'), t('pages.licenses.detail.messages.revokeTitle'), {
confirmButtonText: t('pages.licenses.detail.messages.revokeConfirmButton'),
cancelButtonText: t('pages.licenses.detail.messages.revokeCancelButton'),
type: 'warning'
}).then(() => {
ElMessage.info(t('pages.licenses.detail.messages.revokeComingSoon'))
}).catch(() => {
// 取消操作
})
}
const handleDownloadCertificate = () => {
ElMessage.info(t('pages.licenses.detail.messages.downloadComingSoon'))
}
@@ -399,16 +388,6 @@ onMounted(() => {
}
}
&.revoke-btn {
background: #F0142F;
color: white;
border: none;
&:hover {
background: #d01228;
}
}
&.download-btn {
background: #00C27C;
color: white;