feat: 更新授权变更类型,添加起止时间支持,优化许可证更新表单和国际化文本

This commit is contained in:
wood911010 committed 2025-12-21 13:31:00 +08:00
1 parent 53e3449849
commit 146eb2c44d
13 files changed
+682 -42

No files matched your search

+2
View File
@@ -93,6 +93,8 @@ export interface LicenseUpdateRequest {
software_id?: string;
description?: string;
validity_days?: number;
start_date?: string;
end_date?: string;
deployment_type?: string;
encryption_type?: string;
software_version?: string;
+39 -1
View File
@@ -88,6 +88,10 @@
"unknown": "Unknown Page"
}
},
"common": {
"cancel": "Cancel",
"confirm": "Confirm"
},
"chart": {
"licenseTrend": {
"title": "License Trend",
@@ -407,16 +411,50 @@
"actions": {
"copyCode": "Copy License Code",
"updateLicense": "Update License",
"renewLicense": "Renew License",
"changeValidity": "Change Validity Period",
"revokeLicense": "Revoke License",
"downloadCertificate": "Download Authorization File"
},
"updateDialog": {
"title": "Update Authorization",
"fields": {
"changeType": "Change Type",
"maxActivations": "Max Activations",
"featureConfig": "Feature Config (JSON)",
"usageLimits": "Usage Limits (JSON)",
"customParameters": "Custom Parameters (JSON)",
"reason": "Change Reason"
},
"validation": {
"changeTypeRequired": "Please select change type",
"reasonRequired": "Please enter change reason",
"featureConfigInvalid": "Feature config JSON is invalid",
"usageLimitsInvalid": "Usage limits JSON is invalid",
"customParametersInvalid": "Custom parameters JSON is invalid"
},
"messages": {
"updateSuccess": "Update submitted",
"updateError": "Failed to submit update"
}
},
"dialogs": {
"changeValidityTitle": "Change Validity Period",
"validityType": "Validity Type",
"limited": "Limited",
"permanent": "Permanent",
"dateRange": "Date Range",
"reason": "Change Reason",
"reasonPlaceholder": "Please enter change reason (max 500 chars)"
},
"messages": {
"copySuccess": "License code copied to clipboard",
"copyError": "Copy failed, please copy manually",
"codeEmpty": "License code is empty",
"updateComingSoon": "Update license feature is under development...",
"renewComingSoon": "Renew license feature is under development...",
"changeValiditySuccess": "Validity period changed successfully",
"changeValidityError": "Failed to change validity period",
"changeValidityReasonRequired": "Please provide a reason for the change",
"revokeComingSoon": "Revoke license feature is under development...",
"downloadSuccess": "Authorization file download started",
"downloadError": "Failed to download authorization file, please try again later",
+39 -1
View File
@@ -88,6 +88,10 @@
"unknown": "未知ページ"
}
},
"common": {
"cancel": "キャンセル",
"confirm": "確認"
},
"chart": {
"licenseTrend": {
"title": "ライセンストレンド",
@@ -407,16 +411,50 @@
"actions": {
"copyCode": "ライセンスコードをコピー",
"updateLicense": "ライセンス更新",
"renewLicense": "ライセンス延長",
"changeValidity": "有効期限を変更",
"revokeLicense": "ライセンス取り消し",
"downloadCertificate": "認証ファイルをダウンロード"
},
"updateDialog": {
"title": "ライセンスを変更",
"fields": {
"changeType": "変更タイプ",
"maxActivations": "最大アクティベーション数",
"featureConfig": "機能設定 (JSON)",
"usageLimits": "利用制限 (JSON)",
"customParameters": "カスタムパラメータ (JSON)",
"reason": "変更理由"
},
"validation": {
"changeTypeRequired": "変更タイプを選択してください",
"reasonRequired": "変更理由を入力してください",
"featureConfigInvalid": "機能設定のJSONが不正です",
"usageLimitsInvalid": "利用制限のJSONが不正です",
"customParametersInvalid": "カスタムパラメータのJSONが不正です"
},
"messages": {
"updateSuccess": "変更が送信されました",
"updateError": "変更の送信に失敗しました"
}
},
"dialogs": {
"changeValidityTitle": "有効期限の変更",
"validityType": "期限タイプ",
"limited": "限定",
"permanent": "永久",
"dateRange": "期間",
"reason": "変更理由",
"reasonPlaceholder": "変更理由を入力してください(最大500文字)"
},
"messages": {
"copySuccess": "ライセンスコードがクリップボードにコピーされました",
"copyError": "コピーに失敗しました。手動でコピーしてください",
"codeEmpty": "ライセンスコードが空です",
"updateComingSoon": "ライセンス更新機能は開発中です...",
"renewComingSoon": "ライセンス延長機能は開発中です...",
"changeValiditySuccess": "有効期限の変更が完了しました",
"changeValidityError": "有効期限の変更に失敗しました",
"changeValidityReasonRequired": "変更理由を入力してください",
"revokeComingSoon": "ライセンス取り消し機能は開発中です...",
"downloadSuccess": "認証ファイルのダウンロードを開始しました",
"downloadError": "認証ファイルのダウンロードに失敗しました。後でもう一度お試しください",
+39 -1
View File
@@ -88,6 +88,10 @@
"unknown": "未知页面"
}
},
"common": {
"cancel": "取消",
"confirm": "确定"
},
"chart": {
"licenseTrend": {
"title": "授权趋势",
@@ -407,16 +411,50 @@
"actions": {
"copyCode": "复制授权码",
"updateLicense": "变更授权",
"renewLicense": "延期授权",
"changeValidity": "变更授权期限",
"revokeLicense": "撤销许可证",
"downloadCertificate": "授权文件下载"
},
"updateDialog": {
"title": "变更授权",
"fields": {
"changeType": "变更类型",
"maxActivations": "最大激活设备数",
"featureConfig": "功能配置 (JSON)",
"usageLimits": "使用限制 (JSON)",
"customParameters": "自定义参数 (JSON)",
"reason": "变更原因"
},
"validation": {
"changeTypeRequired": "请选择变更类型",
"reasonRequired": "请输入变更原因",
"featureConfigInvalid": "功能配置 JSON 格式错误",
"usageLimitsInvalid": "使用限制 JSON 格式错误",
"customParametersInvalid": "自定义参数 JSON 格式错误"
},
"messages": {
"updateSuccess": "变更已提交",
"updateError": "变更提交失败"
}
},
"dialogs": {
"changeValidityTitle": "变更授权期限",
"validityType": "期限类型",
"limited": "有限",
"permanent": "永久",
"dateRange": "起止时间",
"reason": "变更原因",
"reasonPlaceholder": "请输入变更原因(最多500字符)"
},
"messages": {
"copySuccess": "授权码已复制到剪贴板",
"copyError": "复制失败,请手动复制",
"codeEmpty": "授权码为空",
"updateComingSoon": "变更授权功能开发中...",
"renewComingSoon": "延期授权功能开发中...",
"changeValiditySuccess": "授权期限变更成功",
"changeValidityError": "变更授权期限失败",
"changeValidityReasonRequired": "请填写变更原因",
"revokeComingSoon": "撤销许可证功能开发中...",
"downloadSuccess": "授权文件已开始下载",
"downloadError": "下载授权文件失败,请稍后重试",
@@ -56,18 +56,146 @@
<el-button class="action-btn-inline copy-btn" @click="handleCopyCode">
{{ $t('pages.licenses.detail.actions.copyCode') }}
</el-button>
<el-button class="action-btn-inline update-btn" @click="handleUpdateLicense">
<el-button class="action-btn-inline update-btn" @click="openUpdateDialog">
{{ $t('pages.licenses.detail.actions.updateLicense') }}
</el-button>
<el-button class="action-btn-inline renew-btn" @click="handleRenewLicense">
{{ $t('pages.licenses.detail.actions.renewLicense') }}
<el-button class="action-btn-inline renew-btn" @click="openChangeValidityDialog">
{{ $t('pages.licenses.detail.actions.changeValidity') }}
</el-button>
<!-- update button moved -->
<el-button class="action-btn-inline download-btn" @click="handleDownloadCertificate">
{{ $t('pages.licenses.detail.actions.downloadCertificate') }}
</el-button>
</div>
</div>
<!-- 变更授权期限弹窗 -->
<el-dialog
:title="t('pages.licenses.detail.dialogs.changeValidityTitle')"
v-model="changeDialogVisible"
width="520px"
>
<div>
<el-form label-width="100px" :model="{ validity_type: changeValidityType, date_range: changeDateRange, reason: changeReason }">
<el-form-item :label="t('pages.licenses.detail.dialogs.validityType')">
<el-radio-group v-model="changeValidityType">
<el-radio label="limited">{{ t('pages.licenses.detail.dialogs.limited') }}</el-radio>
<el-radio label="permanent">{{ t('pages.licenses.detail.dialogs.permanent') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="changeValidityType === 'limited'" :label="t('pages.licenses.detail.dialogs.dateRange')">
<el-date-picker
v-model="changeDateRange"
type="daterange"
range-separator=" - "
start-placeholder="Start date"
end-placeholder="End date"
:disabled-date="disabledDate"
value-format="YYYY-MM-DD"
@change="handleChangeDateRange"
/>
</el-form-item>
<el-form-item :label="t('pages.licenses.detail.dialogs.reason')">
<el-input
type="textarea"
v-model="changeReason"
:placeholder="t('pages.licenses.detail.dialogs.reasonPlaceholder')"
:rows="3"
maxlength="500"
/>
</el-form-item>
</el-form>
</div>
<template #footer>
<el-button @click="changeDialogVisible = false">{{ t('common.cancel') }}</el-button>
<el-button type="primary" :loading="changeSubmitting" @click="handleConfirmChangeValidity">{{ t('common.confirm') }}</el-button>
</template>
</el-dialog>
<!-- 变更授权弹窗(编辑激活数/配置等) -->
<el-dialog
:title="t('pages.licenses.detail.updateDialog.title')"
v-model="updateDialogVisible"
width="720px"
>
<div>
<el-form label-width="140px" class="update-dialog-form">
<el-form-item :label="t('pages.licenses.detail.updateDialog.fields.changeType')">
<el-select v-model="updateChangeType" placeholder="Select" style="width:100%">
<el-option
v-for="opt in changeTypeOptions"
:key="opt.key"
:label="opt.display"
:value="opt.key"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('pages.licenses.detail.updateDialog.fields.maxActivations')">
<el-input-number v-model="updateMaxActivations" :min="1" :controls="true" />
</el-form-item>
<el-form-item :label="t('pages.licenses.detail.updateDialog.fields.featureConfig')">
<div class="kv-entries">
<div v-for="(entry, idx) in featureConfigEntries" :key="entry.id" class="kv-row">
<el-input v-model="entry.key" placeholder="key" style="width:40%" />
<el-select v-model="entry.type" placeholder="type" style="width:14%;margin:0 8px">
<el-option label="string" value="string" />
<el-option label="number" value="number" />
<el-option label="bool" value="bool" />
</el-select>
<el-input v-model="entry.value" placeholder="value" style="width:40%" />
<el-button type="text" icon="el-icon-delete" @click="removeFeatureConfigEntry(idx)" />
</div>
<el-button size="small" @click="addFeatureConfigEntry">{{ t('pages.licenses.form.keyValue.addItem') }}</el-button>
</div>
</el-form-item>
<el-form-item :label="t('pages.licenses.detail.updateDialog.fields.usageLimits')">
<div class="kv-entries">
<div v-for="(entry, idx) in usageLimitsEntries" :key="entry.id" class="kv-row">
<el-input v-model="entry.key" placeholder="key" style="width:40%" />
<el-select v-model="entry.type" placeholder="type" style="width:14%;margin:0 8px">
<el-option label="string" value="string" />
<el-option label="number" value="number" />
<el-option label="bool" value="bool" />
</el-select>
<el-input v-model="entry.value" placeholder="value" style="width:40%" />
<el-button type="text" icon="el-icon-delete" @click="removeUsageLimitEntry(idx)" />
</div>
<el-button size="small" @click="addUsageLimitEntry">{{ t('pages.licenses.form.keyValue.addItem') }}</el-button>
</div>
</el-form-item>
<el-form-item :label="t('pages.licenses.detail.updateDialog.fields.customParameters')">
<div class="kv-entries">
<div v-for="(entry, idx) in customParametersEntries" :key="entry.id" class="kv-row">
<el-input v-model="entry.key" placeholder="key" style="width:40%" />
<el-select v-model="entry.type" placeholder="type" style="width:14%;margin:0 8px">
<el-option label="string" value="string" />
<el-option label="number" value="number" />
<el-option label="bool" value="bool" />
</el-select>
<el-input v-model="entry.value" placeholder="value" style="width:40%" />
<el-button type="text" icon="el-icon-delete" @click="removeCustomParamEntry(idx)" />
</div>
<el-button size="small" @click="addCustomParamEntry">{{ t('pages.licenses.form.keyValue.addItem') }}</el-button>
</div>
</el-form-item>
<el-form-item :label="t('pages.licenses.detail.updateDialog.fields.reason')">
<el-input type="textarea" v-model="updateReason" :rows="3" maxlength="500" />
</el-form-item>
</el-form>
</div>
<template #footer>
<el-button @click="updateDialogVisible = false">{{ t('common.cancel') }}</el-button>
<el-button type="primary" :loading="updateSubmitting" @click="handleConfirmUpdate">{{ t('common.confirm') }}</el-button>
</template>
</el-dialog>
<!-- Tab 内容区域 -->
<div class="tab-content-area">
<BasicInfo v-if="activeTab === 'basic'" :license-data="licenseData" />
@@ -85,7 +213,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, watch, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n'
@@ -134,12 +262,360 @@ const handleCopyCode = () => {
})
}
const handleUpdateLicense = () => {
ElMessage.info(t('pages.licenses.detail.messages.updateComingSoon'))
import { updateLicense } from '@/api/license'
import { getEnumOptions } from '@/api/enum'
// --- change validity dialog state & handlers ---
const changeDialogVisible = ref(false)
const changeValidityType = ref<'permanent' | 'limited'>('limited')
const changeDateRange = ref<[string, string] | null>(null)
const changeValidityDays = ref<number>(365)
const changeReason = ref<string>('')
const changeSubmitting = ref(false)
const disabledDate = (time: Date) => {
const today = new Date()
today.setHours(0, 0, 0, 0)
return time.getTime() < today.getTime()
}
const handleRenewLicense = () => {
ElMessage.info(t('pages.licenses.detail.messages.renewComingSoon'))
const openChangeValidityDialog = () => {
// initialize with current license data
console.log('[Licenses] openChangeValidityDialog called')
if (licenseData.value && licenseData.value.start_date && licenseData.value.end_date) {
changeDateRange.value = [licenseData.value.start_date, licenseData.value.end_date]
const startDate = new Date(changeDateRange.value[0])
const endDate = new Date(changeDateRange.value[1])
const diffTime = endDate.getTime() - startDate.getTime()
changeValidityDays.value = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
changeValidityType.value = changeValidityDays.value >= 365000 ? 'permanent' : 'limited'
} else {
changeDateRange.value = null
changeValidityDays.value = 365
changeValidityType.value = 'limited'
}
changeReason.value = ''
changeDialogVisible.value = true
}
// watch for dialog visibility changes and verify DOM mount
watch(changeDialogVisible, async (val) => {
console.log('[Licenses] changeDialogVisible ->', val)
if (val) {
await nextTick()
const wrapper = document.querySelector('.el-dialog__wrapper') || document.querySelector('.el-dialog')
console.log('[Licenses] dialog DOM element:', wrapper)
if (!wrapper) {
ElMessage.error(t('pages.licenses.detail.messages.changeValidityError') + ' (dialog not mounted)')
}
}
})
const handleChangeDateRange = (dates: [string, string] | null) => {
if (dates && dates.length === 2) {
const startDate = new Date(dates[0])
const endDate = new Date(dates[1])
const diffTime = endDate.getTime() - startDate.getTime()
changeValidityDays.value = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
}
}
const handleConfirmChangeValidity = async () => {
if (!licenseData.value?.id) {
ElMessage.error(t('pages.licenses.detail.messages.missingId'))
return
}
if (!changeReason.value || !changeReason.value.trim()) {
ElMessage.warning(t('pages.licenses.detail.messages.changeValidityReasonRequired'))
return
}
changeSubmitting.value = true
try {
// build start_date / end_date payload (preferred). Format YYYY-MM-DD.
const formatDateYMD = (d: Date) => {
const y = d.getFullYear()
const m = `${d.getMonth() + 1}`.padStart(2, '0')
const day = `${d.getDate()}`.padStart(2, '0')
return `${y}-${m}-${day}`
}
let startDateStr: string | null = null
let endDateStr: string | null = null
if (changeValidityType.value === 'permanent') {
const today = new Date()
startDateStr = formatDateYMD(today)
const endDate = new Date(today)
// permanent maps to 365000 days (back-end convention)
endDate.setDate(endDate.getDate() + 365000 - 1)
endDateStr = formatDateYMD(endDate)
} else {
if (!changeDateRange.value || changeDateRange.value.length !== 2 || !changeDateRange.value[0] || !changeDateRange.value[1]) {
ElMessage.warning(t('pages.licenses.detail.form.validation.dateRangeRequired'))
changeSubmitting.value = false
return
}
// date picker returns strings when value-format is set; handle both and avoid timezone drift by parsing Y-M-D explicitly
const rawStart = changeDateRange.value[0]
const rawEnd = changeDateRange.value[1]
const parseYMD = (s: string) => {
const parts = s.split('-')
return new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]))
}
const start = typeof rawStart === 'string' ? parseYMD(rawStart) : (rawStart as any as Date)
const end = typeof rawEnd === 'string' ? parseYMD(rawEnd) : (rawEnd as any as Date)
startDateStr = formatDateYMD(start)
endDateStr = formatDateYMD(end)
}
console.log('[Licenses] submitting changeValidity', { changeValidityType: changeValidityType.value, changeDateRange: changeDateRange.value, startDateStr, endDateStr })
const payload: any = {
change_type: 'renewal',
reason: changeReason.value
}
if (startDateStr && endDateStr) {
payload.start_date = startDateStr
payload.end_date = endDateStr
}
// include update dialog fields if dialog visible
if (updateDialogVisible.value) {
if (updateMaxActivations.value !== null && updateMaxActivations.value !== undefined) {
payload.max_activations = updateMaxActivations.value
}
const fc = buildJsonRecord(featureConfigEntries.value)
if (fc) payload.feature_config = fc
const ul = buildJsonRecord(usageLimitsEntries.value)
if (ul) payload.usage_limits = ul
const cp = buildJsonRecord(customParametersEntries.value)
if (cp) payload.custom_parameters = cp
payload.change_type = updateChangeType.value || payload.change_type
// if reason in update dialog provided, override
if (updateReason.value && updateReason.value.trim()) {
payload.reason = updateReason.value
}
}
const res = await updateLicense(licenseData.value.id, payload)
if (res.code === '000000') {
ElMessage.success(res.message || t('pages.licenses.detail.messages.changeValiditySuccess'))
changeDialogVisible.value = false
await loadLicenseData()
} else {
throw new Error(res.message || t('pages.licenses.detail.messages.changeValidityError'))
}
} catch (err: any) {
console.error('change validity error', err)
const backendMsg = err?.response?.data?.message
ElMessage.error(backendMsg || err.message || t('pages.licenses.detail.messages.changeValidityError'))
} finally {
changeSubmitting.value = false
}
}
// --- update authorization dialog ---
import type { RawEnumItem } from '@/api/enum'
type KeyValueType = 'string' | 'number' | 'bool'
interface KeyValueItem {
id: string
key: string
value: string
type: KeyValueType
}
const updateDialogVisible = ref(false)
const changeTypeOptions = ref<RawEnumItem[]>([])
const updateChangeType = ref<string>('other')
const updateMaxActivations = ref<number | null>(null)
const featureConfigEntries = ref<KeyValueItem[]>([])
const usageLimitsEntries = ref<KeyValueItem[]>([])
const customParametersEntries = ref<KeyValueItem[]>([])
const updateReason = ref<string>('')
const updateSubmitting = ref(false)
const createEntryId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const createEmptyEntry = (): KeyValueItem => ({
id: createEntryId(),
key: '',
value: '',
type: 'string'
})
const addFeatureConfigEntry = () => featureConfigEntries.value.push(createEmptyEntry())
const addUsageLimitEntry = () => usageLimitsEntries.value.push(createEmptyEntry())
const addCustomParamEntry = () => customParametersEntries.value.push(createEmptyEntry())
const removeFeatureConfigEntry = (i: number) => featureConfigEntries.value.splice(i, 1)
const removeUsageLimitEntry = (i: number) => usageLimitsEntries.value.splice(i, 1)
const removeCustomParamEntry = (i: number) => customParametersEntries.value.splice(i, 1)
const convertValueByType = (value: string, type: KeyValueType) => {
if (type === 'number') {
const num = Number(value)
if (Number.isNaN(num)) throw new Error('invalid number')
return num
}
if (type === 'bool') return value === 'true'
return value
}
const buildJsonRecord = (entries: KeyValueItem[]) => {
if (!entries.length) return null
const result: Record<string, any> = {}
entries.forEach(item => {
const k = item.key.trim()
if (!k) return
try {
result[k] = convertValueByType(item.value, item.type)
} catch {
// ignore here; validation will surface
}
})
return Object.keys(result).length ? result : null
}
// Validate key-value entries: ensure key present and value matches selected type
const validateKeyValueEntries = (entries: KeyValueItem[]): { ok: boolean; msg?: string } => {
for (const item of entries) {
const key = item.key?.trim()
const val = item.value
if (!key) {
return { ok: false, msg: t('pages.licenses.form.keyValue.keyRequired') }
}
if (item.type === 'number') {
if (val === '' || val === null || val === undefined || Number.isNaN(Number(val))) {
return { ok: false, msg: t('pages.licenses.form.keyValue.numberRequired') }
}
}
if (item.type === 'bool') {
const lower = String(val).toLowerCase()
if (lower !== 'true' && lower !== 'false') {
return { ok: false, msg: t('pages.licenses.form.keyValue.boolRequired') }
}
}
}
return { ok: true }
}
const openUpdateDialog = async () => {
updateChangeType.value = 'other'
updateReason.value = ''
updateMaxActivations.value = licenseData.value?.max_activations || null
// populate entries from existing JSON if present
featureConfigEntries.value = []
usageLimitsEntries.value = []
customParametersEntries.value = []
try {
if (licenseData.value?.feature_config) {
const obj = typeof licenseData.value.feature_config === 'string' ? JSON.parse(licenseData.value.feature_config) : licenseData.value.feature_config
for (const [k, v] of Object.entries(obj || {})) {
featureConfigEntries.value.push({ id: createEntryId(), key: k, value: String(v), type: typeof v === 'number' ? 'number' : typeof v === 'boolean' ? 'bool' : 'string' })
}
}
} catch {
// ignore parse errors
}
try {
if (licenseData.value?.usage_limits) {
const obj = typeof licenseData.value.usage_limits === 'string' ? JSON.parse(licenseData.value.usage_limits) : licenseData.value.usage_limits
for (const [k, v] of Object.entries(obj || {})) {
usageLimitsEntries.value.push({ id: createEntryId(), key: k, value: String(v), type: typeof v === 'number' ? 'number' : typeof v === 'boolean' ? 'bool' : 'string' })
}
}
} catch {}
try {
if (licenseData.value?.custom_parameters) {
const obj = typeof licenseData.value.custom_parameters === 'string' ? JSON.parse(licenseData.value.custom_parameters) : licenseData.value.custom_parameters
for (const [k, v] of Object.entries(obj || {})) {
customParametersEntries.value.push({ id: createEntryId(), key: k, value: String(v), type: typeof v === 'number' ? 'number' : typeof v === 'boolean' ? 'bool' : 'string' })
}
}
} catch {}
// load change type options from backend enum
try {
const res = await getEnumOptions('authorization_change_type')
if (res.code === '000000' && res.data?.items) {
changeTypeOptions.value = res.data.items
if (!changeTypeOptions.value.find(i => i.key === updateChangeType.value) && changeTypeOptions.value.length) {
updateChangeType.value = changeTypeOptions.value[0].key
}
}
} catch (err) {
console.error('load change type enums failed', err)
}
updateDialogVisible.value = true
}
const handleConfirmUpdate = async () => {
if (!licenseData.value?.id) {
ElMessage.error(t('pages.licenses.detail.messages.missingId'))
return
}
if (!updateChangeType.value || !updateChangeType.value.trim()) {
ElMessage.warning(t('pages.licenses.detail.updateDialog.validation.changeTypeRequired'))
return
}
if (!updateReason.value || !updateReason.value.trim()) {
ElMessage.warning(t('pages.licenses.detail.updateDialog.validation.reasonRequired'))
return
}
// build payload
const payload: any = {
change_type: updateChangeType.value,
reason: updateReason.value
}
if (updateMaxActivations.value !== null && updateMaxActivations.value !== undefined) {
payload.max_activations = updateMaxActivations.value
}
// build JSON object from key-value entries
const fc = buildJsonRecord(featureConfigEntries.value)
if (fc) payload.feature_config = fc
const ul = buildJsonRecord(usageLimitsEntries.value)
if (ul) payload.usage_limits = ul
const cp = buildJsonRecord(customParametersEntries.value)
if (cp) payload.custom_parameters = cp
// validate KV entries before sending
const v1 = validateKeyValueEntries(featureConfigEntries.value)
if (!v1.ok) {
ElMessage.error(v1.msg || t('pages.licenses.detail.updateDialog.validation.featureConfigInvalid'))
changeSubmitting.value = false
return
}
const v2 = validateKeyValueEntries(usageLimitsEntries.value)
if (!v2.ok) {
ElMessage.error(v2.msg || t('pages.licenses.detail.updateDialog.validation.usageLimitsInvalid'))
changeSubmitting.value = false
return
}
const v3 = validateKeyValueEntries(customParametersEntries.value)
if (!v3.ok) {
ElMessage.error(v3.msg || t('pages.licenses.detail.updateDialog.validation.customParametersInvalid'))
changeSubmitting.value = false
return
}
updateSubmitting.value = true
try {
const res = await updateLicense(licenseData.value.id, payload)
if (res.code === '000000') {
ElMessage.success(res.message || t('pages.licenses.detail.updateDialog.messages.updateSuccess'))
updateDialogVisible.value = false
await loadLicenseData()
} else {
throw new Error(res.message || t('pages.licenses.detail.updateDialog.messages.updateError'))
}
} catch (err: any) {
console.error('update authorization error', err)
const backendMsg = err?.response?.data?.message
ElMessage.error(backendMsg || err.message || t('pages.licenses.detail.updateDialog.messages.updateError'))
} finally {
updateSubmitting.value = false
}
}
const getFileNameFromDisposition = (disposition?: string | null) => {
@@ -229,6 +705,20 @@ const loadLicenseData = async () => {
onMounted(() => {
loadLicenseData()
})
// load change type enums on mount for dropdown
const loadChangeTypeEnums = async () => {
try {
const res = await getEnumOptions('authorization_change_type')
if (res.code === '000000' && res.data?.items) {
changeTypeOptions.value = res.data.items
}
} catch (err) {
console.error('load change type enums failed', err)
}
}
onMounted(() => {
loadChangeTypeEnums()
})
</script>
<style lang="scss" scoped>
@@ -505,3 +995,4 @@ onMounted(() => {
}
}
</style>