feat: 实现许可证撤销功能,更新相关国际化文本,优化许可证信息展示
This commit is contained in:
1 parent
1f25df980d
commit
0478c201db
6 files changed
+119
-33
No files matched your search
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -405,6 +405,11 @@
|
||||
"revokeTitle": "警告",
|
||||
"revokeConfirmButton": "取り消し確認",
|
||||
"revokeCancelButton": "キャンセル",
|
||||
"revokeReasonPlaceholder": "取り消し理由を入力してください",
|
||||
"revokeReasonRequired": "取り消し理由は必須です",
|
||||
"revokeReasonMax": "取り消し理由は500文字以内で入力してください",
|
||||
"revokeSuccess": "ライセンスを正常に取り消しました",
|
||||
"revokeError": "ライセンスの取り消しに失敗しました。後でもう一度お試しください",
|
||||
"loadError": "ライセンス詳細の読み込みに失敗しました",
|
||||
"missingId": "ライセンスIDが不足しています"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user