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>
这个提交包含在:
XuqmGroup 2026-05-21 12:47:03 +08:00
父节点 290a6999fe
当前提交 66129cb89d
共有 4 个文件被更改,包括 46 次插入92 次删除

查看文件

@ -76,14 +76,6 @@ export interface ServiceRequestPage {
totalPages: number totalPages: number
} }
export interface LicenseStatusInfo {
exists: boolean
active?: boolean
maxDevices?: number
registeredDevices?: number
expiresAt?: string | null
}
export interface AppItem { export interface AppItem {
id: string id: string
appKey: string appKey: string
@ -287,10 +279,4 @@ export const opsApi = {
sendPushTestOffline: (payload: { appKey: string; userId: string; title: string; body: string; payload?: string }) => sendPushTestOffline: (payload: { appKey: string; userId: string; title: string; body: string; payload?: string }) =>
client.post<{ data: PushTestResult }>('/ops/push/test-offline', payload), 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-descriptions>
</el-card> </el-card>
<el-card style="margin-bottom: 16px"> <el-card>
<template #header>功能服务</template> <template #header>功能服务</template>
<el-table :data="detail.services" border stripe> <el-table :data="detail.services" border stripe>
<el-table-column prop="serviceType" label="服务类型" width="140" /> <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-column prop="config" label="配置" min-width="240" show-overflow-tooltip />
</el-table> </el-table>
</el-card> </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> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus' import { opsApi, type AppDetail } from '@/api/ops'
import { opsApi, type AppDetail, type LicenseStatusInfo } from '@/api/ops'
const route = useRoute() const route = useRoute()
const detail = ref<AppDetail | null>(null) 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() { async function loadDetail() {
const appKey = route.params.appKey as string const appKey = route.params.appKey as string
const res = await opsApi.getApp(appKey) const res = await opsApi.getApp(appKey)
detail.value = res.data.data 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) { function fmt(value: string) {
@ -151,12 +84,4 @@ onMounted(loadDetail)
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
} }
.max-devices-display {
display: flex;
align-items: center;
}
.max-devices-edit {
display: flex;
align-items: center;
}
</style> </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) { revokeDevice(id: string) {
return licenseClient.delete<{ data: null }>(`/api/license/admin/devices/${encodeURIComponent(id)}`) return licenseClient.delete<{ data: null }>(`/api/license/admin/devices/${encodeURIComponent(id)}`)
}, },

查看文件

@ -51,7 +51,17 @@
{{ license?.isActive ? '正常' : '停用' }} {{ license?.isActive ? '正常' : '停用' }}
</el-tag> </el-tag>
</el-descriptions-item> </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?.registeredDevices ?? devices.length }}</el-descriptions-item>
<el-descriptions-item label="过期时间"> <el-descriptions-item label="过期时间">
{{ license?.expiresAt ? formatDate(license.expiresAt) : '永久' }} {{ license?.expiresAt ? formatDate(license.expiresAt) : '永久' }}
@ -150,6 +160,9 @@ const currentApp = ref<App | null>(null)
const license = ref<AppLicense | null>(null) const license = ref<AppLicense | null>(null)
const devices = ref<LicenseDevice[]>([]) const devices = ref<LicenseDevice[]>([])
const loading = ref(false) 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 appName = computed(() => currentApp.value?.name || license.value?.name || appKey.value || '-')
const activeDevices = computed(() => devices.value.filter(d => d.isActive).length) 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) { async function revokeDevice(row: LicenseDevice) {
try { try {
await ElMessageBox.confirm('确认吊销该设备?', '吊销确认', { type: 'warning' }) await ElMessageBox.confirm('确认吊销该设备?', '吊销确认', { type: 'warning' })
@ -321,4 +355,6 @@ onBeforeUnmount(() => {
.portal-bar-title { font-size: 18px; font-weight: 600; } .portal-bar-title { font-size: 18px; font-weight: 600; }
.stack-cell { display: flex; flex-direction: column; gap: 2px; line-height: 1.35; } .stack-cell { display: flex; flex-direction: column; gap: 2px; line-height: 1.35; }
.stack-cell small { color: #909399; font-size: 12px; } .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> </style>