feat: 新增电工APP端与定时任务管理功能
- 新增电工APP独立鉴权中间件与登录注册接口 - 新增定时任务管理模块,支持任务配置、执行日志与手动触发 - 新增运维费定价与电工派单成本价目管理 - 新增电工资质证照管理与到期提醒定时任务 - 完善数据库实体与DTO校验,优化权限控制
This commit is contained in:
1 parent
1803ad679c
commit
f4454ad014
57 files changed
+9388
-200
No files matched your search
@@ -0,0 +1,649 @@
|
||||
<template>
|
||||
<div class="job-management">
|
||||
<a-card :bordered="false" class="search-card">
|
||||
<a-form layout="inline" :model="queryParams">
|
||||
<a-form-item label="任务名称">
|
||||
<a-input
|
||||
v-model:value="queryParams.jobName"
|
||||
placeholder="请输入任务名称"
|
||||
allow-clear
|
||||
style="width: 180px"
|
||||
@pressEnter="handleSearch"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<a-select
|
||||
v-model:value="queryParams.status"
|
||||
placeholder="全部"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-select-option :value="1">启用</a-select-option>
|
||||
<a-select-option :value="0">停用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<a-card :bordered="false" class="table-card">
|
||||
<div class="table-wrapper" ref="wrapperRef">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="jobList"
|
||||
:loading="tableLoading"
|
||||
:pagination="paginationConfig"
|
||||
row-key="id"
|
||||
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'jobName'">
|
||||
<span>{{ record.jobName }}</span>
|
||||
<a-tag v-if="record.registered === false" color="red" style="margin-left: 6px">未注册</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'params'">
|
||||
<a-tooltip :title="formatParams(record.params, true)">
|
||||
<span class="ellipsis-text">{{ formatParams(record.params) }}</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-switch
|
||||
:checked="record.status === 1"
|
||||
:loading="statusLoadingId === record.id"
|
||||
checked-children="启用"
|
||||
un-checked-children="停用"
|
||||
@change="(checked: boolean) => handleStatusChange(record, checked)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'lastRunAt'">
|
||||
{{ formatDateTime(record.lastRunAt) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'lastRunStatus'">
|
||||
<a-tag v-if="record.lastRunStatus" :color="runStatusColor(record.lastRunStatus)">
|
||||
{{ runStatusText(record.lastRunStatus) }}
|
||||
</a-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'lastRunCostMs'">
|
||||
{{ record.lastRunCostMs === null || record.lastRunCostMs === undefined ? '-' : `${record.lastRunCostMs} ms` }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'nextRunTime'">
|
||||
{{ formatDateTime(record.nextRunTime) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'retry'">
|
||||
{{ `${record.retryTimes ?? 0} 次 / ${record.retryInterval ?? 0} 秒` }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" :disabled="record.registered === false" @click="handleEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="link" size="small" :disabled="record.registered === false" @click="handleRun(record)">
|
||||
<template #icon><PlayCircleOutlined /></template>
|
||||
立即执行
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleOpenLogs(record)">
|
||||
<template #icon><ProfileOutlined /></template>
|
||||
日志
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="editVisible"
|
||||
title="编辑任务"
|
||||
:confirm-loading="saving"
|
||||
:mask-closable="false"
|
||||
width="640px"
|
||||
@ok="handleSave"
|
||||
@cancel="handleCancelEdit"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="任务名称">
|
||||
<span>{{ currentJob?.jobName || '-' }}</span>
|
||||
</a-form-item>
|
||||
<a-form-item label="cron 表达式" name="cronExpression">
|
||||
<a-input v-model:value="formData.cronExpression" placeholder="如 0 3 * * *" :maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-for="field in paramFields"
|
||||
:key="field.key"
|
||||
:label="field.label"
|
||||
:name="['params', field.key]"
|
||||
:rules="getParamRules(field)"
|
||||
>
|
||||
<a-input-number
|
||||
v-if="field.type === 'number'"
|
||||
v-model:value="formData.params[field.key]"
|
||||
:min="field.min"
|
||||
:max="field.max"
|
||||
:placeholder="field.placeholder || `请输入${field.label}`"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<a-input
|
||||
v-else
|
||||
v-model:value="formData.params[field.key]"
|
||||
:placeholder="field.placeholder || `请输入${field.label}`"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="重试次数" name="retryTimes">
|
||||
<a-input-number v-model:value="formData.retryTimes" :min="0" :max="5" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="重试间隔(秒)" name="retryInterval">
|
||||
<a-input-number v-model:value="formData.retryInterval" :min="10" :max="3600" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea
|
||||
v-model:value="formData.remark"
|
||||
placeholder="请输入备注"
|
||||
:maxlength="255"
|
||||
show-count
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-drawer
|
||||
v-model:open="logVisible"
|
||||
:title="`执行日志 - ${currentJob?.jobName || ''}`"
|
||||
width="900"
|
||||
>
|
||||
<div class="log-toolbar">
|
||||
<a-select
|
||||
v-model:value="logQuery.status"
|
||||
placeholder="全部状态"
|
||||
allow-clear
|
||||
style="width: 140px"
|
||||
@change="handleLogSearch"
|
||||
>
|
||||
<a-select-option :value="1">成功</a-select-option>
|
||||
<a-select-option :value="2">失败</a-select-option>
|
||||
<a-select-option :value="3">跳过</a-select-option>
|
||||
</a-select>
|
||||
<a-button danger @click="handleClearLogs">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
清理日志
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="logColumns"
|
||||
:data-source="logList"
|
||||
:loading="logLoading"
|
||||
:pagination="logPaginationConfig"
|
||||
:expandable="logExpandable"
|
||||
row-key="id"
|
||||
size="small"
|
||||
@change="handleLogTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'createdAt'">
|
||||
{{ formatDateTime(record.createdAt) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'triggerType'">
|
||||
{{ triggerTypeText(record.triggerType) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="runStatusColor(record.status)">{{ runStatusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'costMs'">
|
||||
{{ record.costMs === null || record.costMs === undefined ? '-' : `${record.costMs} ms` }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'message'">
|
||||
<a-tooltip :title="record.message || ''">
|
||||
<span class="ellipsis-text">{{ record.message || '-' }}</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
<template #expandedRowRender="{ record }">
|
||||
<pre class="error-stack">{{ record.errorStack }}</pre>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
EditOutlined,
|
||||
PlayCircleOutlined,
|
||||
ProfileOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { jobApi, type JobItem, type JobParamField, type JobLogItem } from '@/api/job'
|
||||
import { useTableScroll } from '@/composables/useTableScroll'
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll()
|
||||
|
||||
const tableLoading = ref(false)
|
||||
const saving = ref(false)
|
||||
const statusLoadingId = ref<number | null>(null)
|
||||
const jobList = ref<JobItem[]>([])
|
||||
const total = ref(0)
|
||||
|
||||
const queryParams = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
jobName: '',
|
||||
status: undefined as number | undefined,
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ title: '任务名称', dataIndex: 'jobName', key: 'jobName', width: 180 },
|
||||
{ title: '任务键', dataIndex: 'jobKey', key: 'jobKey', width: 150, ellipsis: true },
|
||||
{ title: 'cron 表达式', dataIndex: 'cronExpression', key: 'cronExpression', width: 150 },
|
||||
{ title: '参数', dataIndex: 'params', key: 'params', width: 180, ellipsis: true },
|
||||
{ title: '状态', key: 'status', width: 100, align: 'center' as const },
|
||||
{ title: '上次执行时间', dataIndex: 'lastRunAt', key: 'lastRunAt', width: 170 },
|
||||
{ title: '上次状态', key: 'lastRunStatus', width: 100, align: 'center' as const },
|
||||
{ title: '耗时', key: 'lastRunCostMs', width: 100 },
|
||||
{ title: '下次执行时间', key: 'nextRunTime', width: 170 },
|
||||
{ title: '重试配置', key: 'retry', width: 140 },
|
||||
{ title: '操作', key: 'action', width: 220, fixed: 'right' as const },
|
||||
]
|
||||
|
||||
const paginationConfig = reactive({
|
||||
current: queryParams.page,
|
||||
pageSize: queryParams.pageSize,
|
||||
total: total.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
showTotal: (t: number) => `共 ${t} 条记录`,
|
||||
})
|
||||
|
||||
const editVisible = ref(false)
|
||||
const currentJob = ref<JobItem | null>(null)
|
||||
const paramFields = ref<JobParamField[]>([])
|
||||
const formRef = ref()
|
||||
|
||||
const formData = reactive({
|
||||
cronExpression: '',
|
||||
params: {} as Record<string, any>,
|
||||
retryTimes: 0,
|
||||
retryInterval: 60,
|
||||
remark: '',
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
cronExpression: [
|
||||
{ required: true, message: '请输入 cron 表达式', trigger: 'blur' },
|
||||
{ max: 64, message: 'cron 表达式长度不能超过 64', trigger: 'blur' },
|
||||
],
|
||||
retryTimes: [{ required: true, type: 'number' as const, message: '请输入重试次数', trigger: 'change' }],
|
||||
retryInterval: [{ required: true, type: 'number' as const, message: '请输入重试间隔', trigger: 'change' }],
|
||||
remark: [{ max: 255, message: '备注长度不能超过 255', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
const logVisible = ref(false)
|
||||
const logLoading = ref(false)
|
||||
const logList = ref<JobLogItem[]>([])
|
||||
const logTotal = ref(0)
|
||||
|
||||
const logQuery = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
status: undefined as number | undefined,
|
||||
triggerType: undefined as number | undefined,
|
||||
})
|
||||
|
||||
const logColumns = [
|
||||
{ title: '执行时间', dataIndex: 'createdAt', key: 'createdAt', width: 170 },
|
||||
{ title: '触发方式', dataIndex: 'triggerType', key: 'triggerType', width: 90 },
|
||||
{ title: '状态', key: 'status', width: 80, align: 'center' as const },
|
||||
{ title: '尝试', dataIndex: 'attempt', key: 'attempt', width: 70 },
|
||||
{ title: '耗时', key: 'costMs', width: 90 },
|
||||
{ title: '摘要', dataIndex: 'message', key: 'message' },
|
||||
]
|
||||
|
||||
const logExpandable = {
|
||||
rowExpandable: (record: JobLogItem) => !!record.errorStack,
|
||||
}
|
||||
|
||||
const logPaginationConfig = reactive({
|
||||
current: logQuery.page,
|
||||
pageSize: logQuery.pageSize,
|
||||
total: logTotal.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
pageSizeOptions: ['10', '20', '50'],
|
||||
showTotal: (t: number) => `共 ${t} 条记录`,
|
||||
})
|
||||
|
||||
function formatDateTime(time?: string | null): string {
|
||||
if (!time) return '-'
|
||||
return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
function formatParams(params?: Record<string, any>, full = false): string {
|
||||
if (!params || !Object.keys(params).length) return '-'
|
||||
const text = Object.entries(params)
|
||||
.map(([key, value]) => `${key}=${value === null || value === undefined ? '' : value}`)
|
||||
.join(full ? '\n' : ', ')
|
||||
return text || '-'
|
||||
}
|
||||
|
||||
function runStatusText(status: number): string {
|
||||
return ({ 1: '成功', 2: '失败', 3: '跳过' } as Record<number, string>)[status] || '-'
|
||||
}
|
||||
|
||||
function runStatusColor(status: number): string {
|
||||
return ({ 1: 'green', 2: 'red', 3: 'orange' } as Record<number, string>)[status] || 'default'
|
||||
}
|
||||
|
||||
function triggerTypeText(type: number): string {
|
||||
return ({ 1: '定时', 2: '手动', 3: '重试' } as Record<number, string>)[type] || '-'
|
||||
}
|
||||
|
||||
function getParamRules(field: JobParamField) {
|
||||
if (field.type !== 'number') return []
|
||||
return [
|
||||
{
|
||||
validator: (_rule: any, value: any) => {
|
||||
if (value === undefined || value === null || value === '') return Promise.resolve()
|
||||
if (field.min !== undefined && value < field.min) {
|
||||
return Promise.reject(new Error(`${field.label}不能小于 ${field.min}`))
|
||||
}
|
||||
if (field.max !== undefined && value > field.max) {
|
||||
return Promise.reject(new Error(`${field.label}不能大于 ${field.max}`))
|
||||
}
|
||||
return Promise.resolve()
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
async function fetchJobList() {
|
||||
tableLoading.value = true
|
||||
try {
|
||||
const res: any = await jobApi.getJobList({ ...queryParams })
|
||||
if (res.code === 200) {
|
||||
jobList.value = res.data.list
|
||||
total.value = res.data.total
|
||||
paginationConfig.total = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取任务列表失败')
|
||||
} finally {
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
queryParams.page = 1
|
||||
paginationConfig.current = 1
|
||||
fetchJobList()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
queryParams.jobName = ''
|
||||
queryParams.status = undefined
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
queryParams.page = pag.current
|
||||
queryParams.pageSize = pag.pageSize
|
||||
paginationConfig.current = pag.current
|
||||
paginationConfig.pageSize = pag.pageSize
|
||||
fetchJobList()
|
||||
}
|
||||
|
||||
async function handleStatusChange(record: JobItem, checked: boolean) {
|
||||
const status: 0 | 1 = checked ? 1 : 0
|
||||
statusLoadingId.value = record.id
|
||||
try {
|
||||
const res: any = await jobApi.changeJobStatus(record.id, status)
|
||||
if (res.code === 200) {
|
||||
record.status = status
|
||||
message.success(status === 1 ? '已启用' : '已停用')
|
||||
fetchJobList()
|
||||
} else {
|
||||
message.error(res.message || '状态更新失败')
|
||||
}
|
||||
} catch (error) {
|
||||
// 失败时 record.status 未变更,受控开关自动回滚
|
||||
} finally {
|
||||
statusLoadingId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit(record: JobItem) {
|
||||
currentJob.value = record
|
||||
paramFields.value = record.paramFields || []
|
||||
formData.cronExpression = record.cronExpression || ''
|
||||
formData.params = { ...(record.params || {}) }
|
||||
formData.retryTimes = record.retryTimes ?? 0
|
||||
formData.retryInterval = record.retryInterval ?? 60
|
||||
formData.remark = record.remark || ''
|
||||
editVisible.value = true
|
||||
nextTick(() => formRef.value?.clearValidate())
|
||||
}
|
||||
|
||||
function handleCancelEdit() {
|
||||
editVisible.value = false
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!currentJob.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const res: any = await jobApi.updateJob(currentJob.value.id, {
|
||||
cronExpression: formData.cronExpression,
|
||||
params: { ...formData.params },
|
||||
retryTimes: formData.retryTimes,
|
||||
retryInterval: formData.retryInterval,
|
||||
remark: formData.remark,
|
||||
})
|
||||
if (res.code === 200) {
|
||||
message.success('保存成功')
|
||||
editVisible.value = false
|
||||
fetchJobList()
|
||||
} else {
|
||||
message.error(res.message || '保存失败')
|
||||
}
|
||||
} catch (error) {
|
||||
// 保存失败保持弹窗打开,后端 message 由响应拦截器提示
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleRun(record: JobItem) {
|
||||
Modal.confirm({
|
||||
title: '立即执行确认',
|
||||
content: `确定立即执行任务「${record.jobName}」吗?`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
const res: any = await jobApi.runJob(record.id)
|
||||
if (res.code === 200) {
|
||||
message.success(res.data?.message || '执行完成')
|
||||
fetchJobList()
|
||||
if (logVisible.value && currentJob.value?.id === record.id) {
|
||||
fetchJobLogs()
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleOpenLogs(record: JobItem) {
|
||||
currentJob.value = record
|
||||
logQuery.page = 1
|
||||
logQuery.pageSize = 10
|
||||
logQuery.status = undefined
|
||||
logQuery.triggerType = undefined
|
||||
logPaginationConfig.current = 1
|
||||
logPaginationConfig.pageSize = 10
|
||||
logVisible.value = true
|
||||
fetchJobLogs()
|
||||
}
|
||||
|
||||
async function fetchJobLogs() {
|
||||
if (!currentJob.value) return
|
||||
logLoading.value = true
|
||||
try {
|
||||
const res: any = await jobApi.getJobLogs(currentJob.value.id, {
|
||||
page: logQuery.page,
|
||||
pageSize: logQuery.pageSize,
|
||||
status: logQuery.status,
|
||||
triggerType: logQuery.triggerType,
|
||||
})
|
||||
if (res.code === 200) {
|
||||
logList.value = res.data.list
|
||||
logTotal.value = res.data.total
|
||||
logPaginationConfig.total = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取执行日志失败')
|
||||
} finally {
|
||||
logLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogSearch() {
|
||||
logQuery.page = 1
|
||||
logPaginationConfig.current = 1
|
||||
fetchJobLogs()
|
||||
}
|
||||
|
||||
function handleLogTableChange(pag: any) {
|
||||
logQuery.page = pag.current
|
||||
logQuery.pageSize = pag.pageSize
|
||||
logPaginationConfig.current = pag.current
|
||||
logPaginationConfig.pageSize = pag.pageSize
|
||||
fetchJobLogs()
|
||||
}
|
||||
|
||||
function handleClearLogs() {
|
||||
Modal.confirm({
|
||||
title: '清理日志确认',
|
||||
content: '确定清理 90 天前的执行日志吗?此操作不可恢复。',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
const res: any = await jobApi.clearJobLogs(90)
|
||||
if (res.code === 200) {
|
||||
message.success(`已清理 ${res.data?.deleted ?? 0} 条日志`)
|
||||
handleLogSearch()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchJobList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.job-management {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.search-card {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
font-size: 13px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ellipsis-text {
|
||||
max-width: 160px;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.log-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.error-stack {
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #cf1322;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user