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
|
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>
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户