feat(license): 租户平台新增最大设备数编辑,ops 彻底移除 license 管理入口
ops-platform:
- AppDetailView: 移除整个 License 授权管理卡片(含设备数查看和 maxDevices 编辑)
- ops.ts: 移除 LicenseStatusInfo 类型、getAppLicense / updateMaxDevices API
tenant-platform:
- license.ts: 新增 updateAppLicense() 调用 PATCH /api/license/admin/apps/{appKey}
- LicenseManagementView: 「最大设备数」旁新增「修改」按钮,
弹出行内 InputNumber 编辑,保存后刷新显示
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
290a6999fe
当前提交
66129cb89d
@ -76,14 +76,6 @@ export interface ServiceRequestPage {
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export interface LicenseStatusInfo {
|
||||
exists: boolean
|
||||
active?: boolean
|
||||
maxDevices?: number
|
||||
registeredDevices?: number
|
||||
expiresAt?: string | null
|
||||
}
|
||||
|
||||
export interface AppItem {
|
||||
id: string
|
||||
appKey: string
|
||||
@ -287,10 +279,4 @@ export const opsApi = {
|
||||
|
||||
sendPushTestOffline: (payload: { appKey: string; userId: string; title: string; body: string; payload?: string }) =>
|
||||
client.post<{ data: PushTestResult }>('/ops/push/test-offline', payload),
|
||||
|
||||
getAppLicense: (appKey: string) =>
|
||||
client.get<{ data: LicenseStatusInfo }>(`/ops/apps/${appKey}/license`),
|
||||
|
||||
updateMaxDevices: (appKey: string, maxDevices: number) =>
|
||||
client.put(`/ops/apps/${appKey}/license/max-devices`, { maxDevices }),
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<el-card style="margin-bottom: 16px">
|
||||
<el-card>
|
||||
<template #header>功能服务</template>
|
||||
<el-table :data="detail.services" border stripe>
|
||||
<el-table-column prop="serviceType" label="服务类型" width="140" />
|
||||
@ -48,88 +48,21 @@
|
||||
<el-table-column prop="config" label="配置" min-width="240" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card v-if="licenseInfo?.exists">
|
||||
<template #header>
|
||||
<div class="header-row">
|
||||
<span>License 授权管理</span>
|
||||
<el-tag :type="licenseInfo.active ? 'success' : 'danger'" size="small">
|
||||
{{ licenseInfo.active ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="已注册设备">{{ licenseInfo.registeredDevices ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="过期时间">
|
||||
{{ licenseInfo.expiresAt ? fmt(licenseInfo.expiresAt) : '永久' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最大设备数">
|
||||
<div v-if="!editingMaxDevices" class="max-devices-display">
|
||||
<span>{{ licenseInfo.maxDevices ?? '-' }}</span>
|
||||
<el-button link type="primary" size="small" @click="startEditMaxDevices" style="margin-left:8px">
|
||||
修改
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-else class="max-devices-edit">
|
||||
<el-input-number v-model="editMaxDevicesValue" :min="1" :max="999999" size="small" />
|
||||
<el-button type="primary" size="small" :loading="savingMaxDevices" @click="saveMaxDevices" style="margin-left:8px">
|
||||
保存
|
||||
</el-button>
|
||||
<el-button size="small" @click="editingMaxDevices = false" style="margin-left:4px">取消</el-button>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { opsApi, type AppDetail, type LicenseStatusInfo } from '@/api/ops'
|
||||
import { opsApi, type AppDetail } from '@/api/ops'
|
||||
|
||||
const route = useRoute()
|
||||
const detail = ref<AppDetail | null>(null)
|
||||
const licenseInfo = ref<LicenseStatusInfo | null>(null)
|
||||
const editingMaxDevices = ref(false)
|
||||
const editMaxDevicesValue = ref(1)
|
||||
const savingMaxDevices = ref(false)
|
||||
|
||||
async function loadDetail() {
|
||||
const appKey = route.params.appKey as string
|
||||
const res = await opsApi.getApp(appKey)
|
||||
detail.value = res.data.data
|
||||
loadLicense(appKey)
|
||||
}
|
||||
|
||||
async function loadLicense(appKey: string) {
|
||||
try {
|
||||
const res = await opsApi.getAppLicense(appKey)
|
||||
licenseInfo.value = res.data.data
|
||||
} catch {
|
||||
licenseInfo.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function startEditMaxDevices() {
|
||||
editMaxDevicesValue.value = licenseInfo.value?.maxDevices ?? 1
|
||||
editingMaxDevices.value = true
|
||||
}
|
||||
|
||||
async function saveMaxDevices() {
|
||||
if (!detail.value) return
|
||||
savingMaxDevices.value = true
|
||||
try {
|
||||
await opsApi.updateMaxDevices(detail.value.app.appKey, editMaxDevicesValue.value)
|
||||
ElMessage.success('最大设备数已更新')
|
||||
editingMaxDevices.value = false
|
||||
loadLicense(detail.value.app.appKey)
|
||||
} catch {
|
||||
ElMessage.error('更新失败')
|
||||
} finally {
|
||||
savingMaxDevices.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function fmt(value: string) {
|
||||
@ -151,12 +84,4 @@ onMounted(loadDetail)
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.max-devices-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.max-devices-edit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -104,6 +104,13 @@ export const licenseApi = {
|
||||
)
|
||||
},
|
||||
|
||||
updateAppLicense(appKey: string, data: { maxDevices?: number; isActive?: boolean; remark?: string }) {
|
||||
return licenseClient.patch<{ data: AppLicense }>(
|
||||
`/api/license/admin/apps/${encodeURIComponent(appKey)}`,
|
||||
data,
|
||||
)
|
||||
},
|
||||
|
||||
revokeDevice(id: string) {
|
||||
return licenseClient.delete<{ data: null }>(`/api/license/admin/devices/${encodeURIComponent(id)}`)
|
||||
},
|
||||
|
||||
@ -51,7 +51,17 @@
|
||||
{{ license?.isActive ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最大设备数">{{ license?.maxDevices ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最大设备数">
|
||||
<div v-if="!editingMaxDevices" class="max-devices-display">
|
||||
<span>{{ license?.maxDevices ?? '-' }}</span>
|
||||
<el-button link type="primary" size="small" style="margin-left:8px" @click="startEditMaxDevices">修改</el-button>
|
||||
</div>
|
||||
<div v-else class="max-devices-edit">
|
||||
<el-input-number v-model="editMaxDevicesValue" :min="1" :max="999999" size="small" />
|
||||
<el-button type="primary" size="small" :loading="savingMaxDevices" style="margin-left:8px" @click="saveMaxDevices">保存</el-button>
|
||||
<el-button size="small" style="margin-left:4px" @click="editingMaxDevices = false">取消</el-button>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="已注册设备">{{ license?.registeredDevices ?? devices.length }}</el-descriptions-item>
|
||||
<el-descriptions-item label="过期时间">
|
||||
{{ license?.expiresAt ? formatDate(license.expiresAt) : '永久' }}
|
||||
@ -150,6 +160,9 @@ const currentApp = ref<App | null>(null)
|
||||
const license = ref<AppLicense | null>(null)
|
||||
const devices = ref<LicenseDevice[]>([])
|
||||
const loading = ref(false)
|
||||
const editingMaxDevices = ref(false)
|
||||
const editMaxDevicesValue = ref(1)
|
||||
const savingMaxDevices = ref(false)
|
||||
|
||||
const appName = computed(() => currentApp.value?.name || license.value?.name || appKey.value || '-')
|
||||
const activeDevices = computed(() => devices.value.filter(d => d.isActive).length)
|
||||
@ -179,6 +192,27 @@ async function loadData() {
|
||||
}
|
||||
}
|
||||
|
||||
function startEditMaxDevices() {
|
||||
editMaxDevicesValue.value = license.value?.maxDevices ?? 1
|
||||
editingMaxDevices.value = true
|
||||
}
|
||||
|
||||
async function saveMaxDevices() {
|
||||
const key = appKey.value
|
||||
if (!key) return
|
||||
savingMaxDevices.value = true
|
||||
try {
|
||||
const res = await licenseApi.updateAppLicense(key, { maxDevices: editMaxDevicesValue.value })
|
||||
license.value = res.data.data
|
||||
editingMaxDevices.value = false
|
||||
ElMessage.success('最大设备数已更新')
|
||||
} catch {
|
||||
ElMessage.error('更新失败')
|
||||
} finally {
|
||||
savingMaxDevices.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeDevice(row: LicenseDevice) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认吊销该设备?', '吊销确认', { type: 'warning' })
|
||||
@ -321,4 +355,6 @@ onBeforeUnmount(() => {
|
||||
.portal-bar-title { font-size: 18px; font-weight: 600; }
|
||||
.stack-cell { display: flex; flex-direction: column; gap: 2px; line-height: 1.35; }
|
||||
.stack-cell small { color: #909399; font-size: 12px; }
|
||||
.max-devices-display { display: flex; align-items: center; }
|
||||
.max-devices-edit { display: flex; align-items: center; }
|
||||
</style>
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户