Show app license device list directly
这个提交包含在:
父节点
690d930a17
当前提交
c1c80e1a7c
@ -66,7 +66,7 @@ licenseClient.interceptors.response.use(
|
||||
},
|
||||
)
|
||||
|
||||
export interface LicenseCompany {
|
||||
export interface AppLicense {
|
||||
id: string
|
||||
name: string
|
||||
maxDevices: number
|
||||
@ -80,7 +80,7 @@ export interface LicenseCompany {
|
||||
|
||||
export interface LicenseDevice {
|
||||
id: string
|
||||
companyId: string
|
||||
appKey: string
|
||||
deviceId: string
|
||||
deviceName?: string | null
|
||||
deviceModel?: string | null
|
||||
@ -98,34 +98,12 @@ export interface LicenseDevice {
|
||||
}
|
||||
|
||||
export const licenseApi = {
|
||||
listCompanies() {
|
||||
return licenseClient.get<{ data: LicenseCompany[] }>('/api/license/admin/companies')
|
||||
},
|
||||
|
||||
createCompany(data: { name: string; maxDevices: number; expiresAt?: string; remark?: string }) {
|
||||
return licenseClient.post<{ data: LicenseCompany }>('/api/license/admin/companies', data)
|
||||
},
|
||||
|
||||
getCompany(id: string) {
|
||||
return licenseClient.get<{ data: { company: LicenseCompany; devices: LicenseDevice[] } }>(
|
||||
`/api/license/admin/companies/${encodeURIComponent(id)}`,
|
||||
getAppLicense(appKey: string) {
|
||||
return licenseClient.get<{ data: { license: AppLicense; devices: LicenseDevice[] } }>(
|
||||
`/api/license/admin/apps/${encodeURIComponent(appKey)}`,
|
||||
)
|
||||
},
|
||||
|
||||
updateCompany(
|
||||
id: string,
|
||||
data: { name?: string; maxDevices?: number; expiresAt?: string; isActive?: boolean; remark?: string },
|
||||
) {
|
||||
return licenseClient.put<{ data: LicenseCompany }>(
|
||||
`/api/license/admin/companies/${encodeURIComponent(id)}`,
|
||||
data,
|
||||
)
|
||||
},
|
||||
|
||||
deleteCompany(id: string) {
|
||||
return licenseClient.delete<{ data: null }>(`/api/license/admin/companies/${encodeURIComponent(id)}`)
|
||||
},
|
||||
|
||||
revokeDevice(id: string) {
|
||||
return licenseClient.delete<{ data: null }>(`/api/license/admin/devices/${encodeURIComponent(id)}`)
|
||||
},
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<el-option v-for="a in portalApps" :key="a.appKey" :label="a.name" :value="a.appKey" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-page-header v-else @back="$router.back()" :content="`授权管理 — ${appKey}`" style="margin-bottom:20px" />
|
||||
<el-page-header v-else @back="$router.back()" :content="`授权管理 - ${appName}`" style="margin-bottom:20px" />
|
||||
<el-empty v-if="isServicesPortal && !appKey" description="请选择一个应用" style="margin-top:80px" />
|
||||
|
||||
<template v-if="!isServicesPortal || appKey">
|
||||
@ -22,285 +22,137 @@
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="never" style="margin-top:16px">
|
||||
<div class="license-summary">
|
||||
<el-descriptions :column="isMobile ? 1 : 3" border>
|
||||
<el-descriptions-item label="应用">{{ appName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="AppKey">{{ appKey || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="授权状态">
|
||||
<el-tag :type="license?.isActive ? 'success' : 'danger'" size="small">
|
||||
{{ license?.isActive ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最大设备数">{{ license?.maxDevices ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="已注册设备">{{ license?.registeredDevices ?? devices.length }}</el-descriptions-item>
|
||||
<el-descriptions-item label="过期时间">
|
||||
{{ license?.expiresAt ? formatDate(license.expiresAt) : '永久' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="toolbar responsive-toolbar">
|
||||
<el-button type="primary" @click="openCreateDialog">新增公司</el-button>
|
||||
<el-button v-if="appKey" @click="downloadLicenseFile">下载 License 文件</el-button>
|
||||
<el-button @click="loadData" :loading="loading">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<el-table :data="companies" v-loading="loading" border stripe>
|
||||
<el-table-column prop="name" label="公司名称" min-width="160" />
|
||||
<el-table-column prop="maxDevices" label="最大设备数" width="120" />
|
||||
<el-table-column prop="registeredDevices" label="已注册设备" width="120" />
|
||||
<el-table-column label="过期时间" width="160">
|
||||
<el-table :data="devices" v-loading="loading" border stripe>
|
||||
<el-table-column prop="deviceId" label="设备ID" min-width="220" />
|
||||
<el-table-column prop="deviceName" label="设备名称" min-width="130">
|
||||
<template #default="{ row }">{{ row.deviceName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="用户" min-width="180">
|
||||
<template #default="{ row }">
|
||||
{{ row.expiresAt ? formatDate(row.expiresAt) : '永久' }}
|
||||
<div class="stack-cell">
|
||||
<span>{{ row.userName || row.userId || '-' }}</span>
|
||||
<small v-if="row.userName && row.userId">{{ row.userId }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<el-table-column label="联系方式" min-width="190">
|
||||
<template #default="{ row }">
|
||||
<div class="stack-cell">
|
||||
<span>{{ row.userPhone || '-' }}</span>
|
||||
<small v-if="row.userEmail">{{ row.userEmail }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="设备信息" min-width="190">
|
||||
<template #default="{ row }">
|
||||
<div class="stack-cell">
|
||||
<span>{{ [row.deviceVendor, row.deviceModel].filter(Boolean).join(' ') || '-' }}</span>
|
||||
<small v-if="row.osVersion">{{ row.osVersion }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="注册时间" width="170">
|
||||
<template #default="{ row }">{{ formatDate(row.registeredAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最后验证" width="170">
|
||||
<template #default="{ row }">{{ row.lastVerifiedAt ? formatDate(row.lastVerifiedAt) : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isActive ? 'success' : 'danger'" size="small">
|
||||
{{ row.isActive ? '正常' : '停用' }}
|
||||
{{ row.isActive ? '正常' : '吊销' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="viewCompany(row)">详情</el-button>
|
||||
<el-button size="small" type="primary" plain @click="editCompany(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" plain @click="deleteCompany(row)">删除</el-button>
|
||||
<el-button v-if="row.isActive" size="small" type="danger" plain @click="revokeDevice(row)">吊销</el-button>
|
||||
<el-button v-else size="small" type="success" plain @click="reactivateDevice(row)">激活</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- Company Detail Dialog -->
|
||||
<el-dialog v-model="showDetailDialog" :title="detailTitle" :width="dialogWidth" destroy-on-close>
|
||||
<el-descriptions :column="isMobile ? 1 : 2" border>
|
||||
<el-descriptions-item label="ID">{{ selectedCompany?.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="名称">{{ selectedCompany?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最大设备">{{ selectedCompany?.maxDevices }}</el-descriptions-item>
|
||||
<el-descriptions-item label="已注册">{{ selectedCompany?.registeredDevices }}</el-descriptions-item>
|
||||
<el-descriptions-item label="过期时间">
|
||||
{{ selectedCompany?.expiresAt ? formatDate(selectedCompany.expiresAt) : '永久' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="selectedCompany?.isActive ? 'success' : 'danger'" size="small">
|
||||
{{ selectedCompany?.isActive ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ selectedCompany?.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<h4 style="margin:20px 0 12px">设备列表</h4>
|
||||
<el-table :data="companyDevices" border stripe size="small">
|
||||
<el-table-column prop="deviceId" label="设备ID" min-width="200" />
|
||||
<el-table-column prop="deviceName" label="设备名称" min-width="120">
|
||||
<template #default="{ row }">{{ row.deviceName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="用户" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="user-cell">
|
||||
<span>{{ row.userName || row.userId || '-' }}</span>
|
||||
<small v-if="row.userName && row.userId">{{ row.userId }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="联系方式" min-width="190">
|
||||
<template #default="{ row }">
|
||||
<div class="user-cell">
|
||||
<span>{{ row.userPhone || '-' }}</span>
|
||||
<small v-if="row.userEmail">{{ row.userEmail }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="设备信息" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="user-cell">
|
||||
<span>{{ [row.deviceVendor, row.deviceModel].filter(Boolean).join(' ') || '-' }}</span>
|
||||
<small v-if="row.osVersion">{{ row.osVersion }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="注册时间" width="160">
|
||||
<template #default="{ row }">{{ formatDate(row.registeredAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最后验证" width="160">
|
||||
<template #default="{ row }">{{ row.lastVerifiedAt ? formatDate(row.lastVerifiedAt) : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isActive ? 'success' : 'danger'" size="small">
|
||||
{{ row.isActive ? '正常' : '吊销' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.isActive" size="small" type="danger" plain @click="revokeDevice(row)">吊销</el-button>
|
||||
<el-button v-else size="small" type="success" plain @click="reactivateDevice(row)">激活</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Create/Edit Dialog -->
|
||||
<el-dialog v-model="showFormDialog" :title="formTitle" :width="dialogWidth">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="公司名称" required>
|
||||
<el-input v-model="form.name" placeholder="请输入公司名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最大设备数" required>
|
||||
<el-input-number v-model="form.maxDevices" :min="1" :max="9999" />
|
||||
</el-form-item>
|
||||
<el-form-item label="过期时间">
|
||||
<el-date-picker v-model="form.expiresAt" type="datetime" placeholder="选择过期时间(留空为永久)" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-switch v-model="form.isActive" active-text="正常" inactive-text="停用" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showFormDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitForm">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { licenseApi, type LicenseCompany, type LicenseDevice } from '@/api/license'
|
||||
import { licenseApi, type AppLicense, type LicenseDevice } from '@/api/license'
|
||||
import { appApi, type App } from '@/api/app'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const isMobile = ref(false)
|
||||
const dialogWidth = computed(() => (isMobile.value ? 'calc(100vw - 24px)' : '720px'))
|
||||
|
||||
const appKey = computed(() => route.query.appKey as string || route.params.appKey as string)
|
||||
const isServicesPortal = computed(() => route.path.startsWith('/services/'))
|
||||
|
||||
const portalApps = ref<App[]>([])
|
||||
const companies = ref<LicenseCompany[]>([])
|
||||
const companyDevices = ref<LicenseDevice[]>([])
|
||||
const currentApp = ref<App | null>(null)
|
||||
const license = ref<AppLicense | null>(null)
|
||||
const devices = ref<LicenseDevice[]>([])
|
||||
const loading = ref(false)
|
||||
const showDetailDialog = ref(false)
|
||||
const showFormDialog = ref(false)
|
||||
const selectedCompany = ref<LicenseCompany | null>(null)
|
||||
const submitting = ref(false)
|
||||
const isEditing = ref(false)
|
||||
|
||||
const form = ref({
|
||||
id: '',
|
||||
name: '',
|
||||
maxDevices: 1,
|
||||
expiresAt: null as Date | null,
|
||||
isActive: true,
|
||||
remark: '',
|
||||
})
|
||||
const appName = computed(() => currentApp.value?.name || license.value?.name || appKey.value || '-')
|
||||
const activeDevices = computed(() => devices.value.filter(d => d.isActive).length)
|
||||
const revokedDevices = computed(() => devices.value.filter(d => !d.isActive).length)
|
||||
|
||||
const statCards = computed(() => [
|
||||
{ label: '公司总数', value: companies.value.length },
|
||||
{ label: '设备总数', value: companies.value.reduce((sum, c) => sum + (c.registeredDevices || 0), 0) },
|
||||
{ label: '活跃公司', value: companies.value.filter(c => c.isActive).length },
|
||||
{ label: '过期公司', value: companies.value.filter(c => c.expiresAt && new Date(c.expiresAt) < new Date()).length },
|
||||
{ label: '最大设备数', value: license.value?.maxDevices ?? '-' },
|
||||
{ label: '已注册设备', value: license.value?.registeredDevices ?? devices.value.length },
|
||||
{ label: '正常设备', value: activeDevices.value },
|
||||
{ label: '吊销设备', value: revokedDevices.value },
|
||||
])
|
||||
|
||||
const detailTitle = computed(() => selectedCompany.value ? `${selectedCompany.value.name} — 详情` : '公司详情')
|
||||
const formTitle = computed(() => isEditing.value ? '编辑公司' : '新增公司')
|
||||
|
||||
async function loadData() {
|
||||
const key = appKey.value
|
||||
if (!key) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await licenseApi.listCompanies()
|
||||
companies.value = res.data.data || []
|
||||
} catch (e) {
|
||||
// error handled by interceptor
|
||||
const [licenseRes, appRes] = await Promise.all([
|
||||
licenseApi.getAppLicense(key),
|
||||
appApi.get(key).catch(() => null),
|
||||
])
|
||||
license.value = licenseRes.data.data.license
|
||||
devices.value = licenseRes.data.data.devices || []
|
||||
currentApp.value = appRes?.data.data ?? portalApps.value.find(item => item.appKey === key) ?? null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function viewCompany(row: LicenseCompany) {
|
||||
selectedCompany.value = row
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await licenseApi.getCompany(row.id)
|
||||
companyDevices.value = res.data.data.devices || []
|
||||
showDetailDialog.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function editCompany(row: LicenseCompany) {
|
||||
isEditing.value = true
|
||||
form.value = {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
maxDevices: row.maxDevices,
|
||||
expiresAt: row.expiresAt ? new Date(row.expiresAt) : null,
|
||||
isActive: row.isActive,
|
||||
remark: row.remark || '',
|
||||
}
|
||||
showFormDialog.value = true
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
isEditing.value = false
|
||||
form.value = {
|
||||
id: '',
|
||||
name: '',
|
||||
maxDevices: 1,
|
||||
expiresAt: null,
|
||||
isActive: true,
|
||||
remark: '',
|
||||
}
|
||||
showFormDialog.value = true
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (!form.value.name.trim()) {
|
||||
return ElMessage.warning('请输入公司名称')
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
const data = {
|
||||
name: form.value.name,
|
||||
maxDevices: form.value.maxDevices,
|
||||
expiresAt: form.value.expiresAt ? form.value.expiresAt.toISOString().slice(0, 19).replace('T', ' ') : undefined,
|
||||
isActive: form.value.isActive,
|
||||
remark: form.value.remark || undefined,
|
||||
}
|
||||
if (isEditing.value) {
|
||||
await licenseApi.updateCompany(form.value.id, data)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await licenseApi.createCompany({
|
||||
name: data.name,
|
||||
maxDevices: data.maxDevices,
|
||||
expiresAt: data.expiresAt,
|
||||
remark: data.remark,
|
||||
})
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
showFormDialog.value = false
|
||||
loadData()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCompany(row: LicenseCompany) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除公司 "${row.name}"?关联的设备也将被删除。`, '删除确认', {
|
||||
type: 'warning', confirmButtonText: '确认删除', cancelButtonText: '取消',
|
||||
})
|
||||
await licenseApi.deleteCompany(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} catch (e) {
|
||||
// cancelled
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeDevice(row: LicenseDevice) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认吊销该设备?', '吊销确认', { type: 'warning' })
|
||||
await licenseApi.revokeDevice(row.id)
|
||||
ElMessage.success('已吊销')
|
||||
if (selectedCompany.value) {
|
||||
viewCompany(selectedCompany.value)
|
||||
}
|
||||
loadData()
|
||||
} catch (e) {
|
||||
// cancelled
|
||||
@ -310,9 +162,6 @@ async function revokeDevice(row: LicenseDevice) {
|
||||
async function reactivateDevice(row: LicenseDevice) {
|
||||
await licenseApi.reactivateDevice(row.id)
|
||||
ElMessage.success('已激活')
|
||||
if (selectedCompany.value) {
|
||||
viewCompany(selectedCompany.value)
|
||||
}
|
||||
loadData()
|
||||
}
|
||||
|
||||
@ -320,9 +169,8 @@ async function downloadLicenseFile() {
|
||||
const key = appKey.value
|
||||
if (!key) return
|
||||
const res = await appApi.downloadLicenseFile(key)
|
||||
const app = portalApps.value.find(item => item.appKey === key)
|
||||
const disposition = res.headers['content-disposition'] as string | undefined
|
||||
const filename = parseFilename(disposition) ?? `${app?.name || key}.xuqmlicense`
|
||||
const filename = parseFilename(disposition) ?? `${appName.value || key}.xuqmlicense`
|
||||
const url = URL.createObjectURL(res.data)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
@ -347,19 +195,32 @@ function switchApp(key: string) {
|
||||
|
||||
function formatDate(d: string | number) {
|
||||
const date = new Date(d)
|
||||
return date.toLocaleString('zh-CN')
|
||||
return Number.isNaN(date.getTime()) ? '-' : date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
function updateViewport() {
|
||||
isMobile.value = window.innerWidth < 768
|
||||
window.addEventListener('resize', () => { isMobile.value = window.innerWidth < 768 })
|
||||
}
|
||||
|
||||
watch(appKey, () => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
updateViewport()
|
||||
window.addEventListener('resize', updateViewport)
|
||||
loadData()
|
||||
if (isServicesPortal.value) {
|
||||
appApi.list().then(res => {
|
||||
portalApps.value = res.data.data || []
|
||||
currentApp.value = portalApps.value.find(item => item.appKey === appKey.value) ?? currentApp.value
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateViewport)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -367,10 +228,11 @@ onMounted(() => {
|
||||
.stat-card { display: flex; flex-direction: column; align-items: center; padding: 12px; }
|
||||
.stat-value { font-size: 28px; font-weight: 700; color: #0E7BF2; }
|
||||
.stat-label { font-size: 13px; color: #64748b; margin-top: 4px; }
|
||||
.toolbar { margin-bottom: 12px; display: flex; gap: 8px; }
|
||||
.license-summary { margin-bottom: 16px; }
|
||||
.toolbar { margin-bottom: 12px; display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.table-wrap { overflow-x: auto; }
|
||||
.portal-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
|
||||
.portal-bar-title { font-size: 18px; font-weight: 600; }
|
||||
.user-cell { display: flex; flex-direction: column; gap: 2px; line-height: 1.35; }
|
||||
.user-cell small { color: #909399; font-size: 12px; }
|
||||
.stack-cell { display: flex; flex-direction: column; gap: 2px; line-height: 1.35; }
|
||||
.stack-cell small { color: #909399; font-size: 12px; }
|
||||
</style>
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户