feat: 更新授权变更类型,添加起止时间支持,优化许可证更新表单和国际化文本
This commit is contained in:
1 parent
53e3449849
commit
146eb2c44d
13 files changed
+682
-42
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "認証ファイルのダウンロードに失敗しました。後でもう一度お試しください",
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user