This commit is contained in:
@@ -285,8 +285,8 @@
|
||||
|
||||
<style scoped lang="scss">
|
||||
.alert-history-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -58,7 +58,11 @@
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="告警级别" prop="alert_level">
|
||||
<el-select v-model="form.alert_level" placeholder="请选择告警级别" style="width: 100%">
|
||||
<el-select
|
||||
v-model="form.alert_level"
|
||||
placeholder="请选择告警级别"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="警告" value="warning" />
|
||||
<el-option label="严重" value="critical" />
|
||||
</el-select>
|
||||
@@ -155,188 +159,33 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, nextTick, watch, h } from 'vue'
|
||||
import { ElMessage, ElMessageBox, ElTag, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { PollingAlertService } from '@/api/modules'
|
||||
import type { PollingAlertRule, CreatePollingAlertRuleRequest } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { ref, reactive, computed, nextTick, watch, h } from 'vue'
|
||||
import { ElMessage, ElMessageBox, ElTag, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { PollingAlertService } from '@/api/modules'
|
||||
import type { PollingAlertRule, CreatePollingAlertRuleRequest } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<PollingAlertRule[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'create' | 'edit'>('create')
|
||||
const formRef = ref<FormInstance>()
|
||||
const contextMenuRef = ref()
|
||||
const currentRow = ref<PollingAlertRule | null>(null)
|
||||
const loading = ref(false)
|
||||
const tableData = ref<PollingAlertRule[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'create' | 'edit'>('create')
|
||||
const formRef = ref<FormInstance>()
|
||||
const contextMenuRef = ref()
|
||||
const currentRow = ref<PollingAlertRule | null>(null)
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const form = reactive<CreatePollingAlertRuleRequest & { id?: number }>({
|
||||
rule_name: '',
|
||||
alert_level: '',
|
||||
metric_type: '',
|
||||
task_type: '',
|
||||
threshold: 0,
|
||||
operator: '>',
|
||||
cooldown_minutes: 5,
|
||||
notify_channels: ''
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
rule_name: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
|
||||
alert_level: [{ required: true, message: '请选择告警级别', trigger: 'change' }],
|
||||
metric_type: [{ required: true, message: '请选择指标类型', trigger: 'change' }],
|
||||
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
|
||||
threshold: [{ required: true, message: '请输入阈值', trigger: 'blur' }],
|
||||
operator: [{ required: true, message: '请选择运算符', trigger: 'change' }],
|
||||
cooldown_minutes: [{ required: true, message: '请输入冷却时间', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const getAlertLevelLabel = (level: string) => {
|
||||
return level === 'critical' ? '严重' : '警告'
|
||||
}
|
||||
|
||||
const getMetricTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
queue_size: '队列大小',
|
||||
success_rate: '成功率',
|
||||
avg_duration: '平均执行时间',
|
||||
concurrency: '并发数'
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
const getTaskTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
'polling:realname': '实名认证轮询',
|
||||
'polling:carddata': '卡数据轮询',
|
||||
'polling:package': '套餐轮询'
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'rule_name',
|
||||
label: '规则名称',
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
prop: 'alert_level',
|
||||
label: '告警级别',
|
||||
width: 100,
|
||||
slots: {
|
||||
default: ({ row }: any) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: row.alert_level === 'critical' ? 'danger' : 'warning' },
|
||||
() => getAlertLevelLabel(row.alert_level)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'metric_type_name',
|
||||
label: '指标类型',
|
||||
width: 120,
|
||||
formatter: (row: any) => getMetricTypeLabel(row.metric_type)
|
||||
},
|
||||
{
|
||||
prop: 'task_type_name',
|
||||
label: '任务类型',
|
||||
width: 150,
|
||||
formatter: (row: any) => getTaskTypeLabel(row.task_type)
|
||||
},
|
||||
{
|
||||
prop: 'operator',
|
||||
label: '运算符',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'threshold',
|
||||
label: '阈值',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'cooldown_minutes',
|
||||
label: '冷却时间(分钟)',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 80,
|
||||
slots: {
|
||||
default: ({ row }: any) =>
|
||||
h(ElTag, { type: row.status === 1 ? 'success' : 'info' }, () =>
|
||||
row.status === 1 ? '启用' : '禁用'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: any) => formatDateTime(row.created_at)
|
||||
}
|
||||
])
|
||||
|
||||
const columnOptions = computed(() =>
|
||||
columns.value.map((col) => ({
|
||||
label: col.label,
|
||||
prop: col.prop || col.label,
|
||||
visible: true
|
||||
}))
|
||||
)
|
||||
|
||||
// 右键菜单项
|
||||
const contextMenuItems = computed<MenuItemType[]>(() => {
|
||||
return [
|
||||
{ key: 'edit', label: '编辑' },
|
||||
{ key: 'toggle', label: currentRow.value?.status === 1 ? '禁用' : '启用' },
|
||||
{ key: 'delete', label: '删除' }
|
||||
]
|
||||
})
|
||||
|
||||
watch(dialogVisible, (val) => {
|
||||
if (!val) {
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await PollingAlertService.getPollingAlertRules()
|
||||
if (res.code === 0 && res.data) {
|
||||
tableData.value = res.data.items
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
const form = reactive<CreatePollingAlertRuleRequest & { id?: number }>({
|
||||
rule_name: '',
|
||||
alert_level: '',
|
||||
metric_type: '',
|
||||
@@ -346,134 +195,287 @@ const resetForm = () => {
|
||||
cooldown_minutes: 5,
|
||||
notify_channels: ''
|
||||
})
|
||||
}
|
||||
|
||||
const showCreateDialog = () => {
|
||||
resetForm()
|
||||
dialogType.value = 'create'
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
const rules: FormRules = {
|
||||
rule_name: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
|
||||
alert_level: [{ required: true, message: '请选择告警级别', trigger: 'change' }],
|
||||
metric_type: [{ required: true, message: '请选择指标类型', trigger: 'change' }],
|
||||
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
|
||||
threshold: [{ required: true, message: '请输入阈值', trigger: 'blur' }],
|
||||
operator: [{ required: true, message: '请选择运算符', trigger: 'change' }],
|
||||
cooldown_minutes: [{ required: true, message: '请输入冷却时间', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const getAlertLevelLabel = (level: string) => {
|
||||
return level === 'critical' ? '严重' : '警告'
|
||||
}
|
||||
|
||||
const getMetricTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
queue_size: '队列大小',
|
||||
success_rate: '成功率',
|
||||
avg_duration: '平均执行时间',
|
||||
concurrency: '并发数'
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
const getTaskTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
'polling:realname': '实名认证轮询',
|
||||
'polling:carddata': '卡数据轮询',
|
||||
'polling:package': '套餐轮询'
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'rule_name',
|
||||
label: '规则名称',
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
prop: 'alert_level',
|
||||
label: '告警级别',
|
||||
width: 100,
|
||||
slots: {
|
||||
default: ({ row }: any) =>
|
||||
h(ElTag, { type: row.alert_level === 'critical' ? 'danger' : 'warning' }, () =>
|
||||
getAlertLevelLabel(row.alert_level)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'metric_type_name',
|
||||
label: '指标类型',
|
||||
width: 120,
|
||||
formatter: (row: any) => getMetricTypeLabel(row.metric_type)
|
||||
},
|
||||
{
|
||||
prop: 'task_type_name',
|
||||
label: '任务类型',
|
||||
width: 150,
|
||||
formatter: (row: any) => getTaskTypeLabel(row.task_type)
|
||||
},
|
||||
{
|
||||
prop: 'operator',
|
||||
label: '运算符',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'threshold',
|
||||
label: '阈值',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'cooldown_minutes',
|
||||
label: '冷却时间(分钟)',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 80,
|
||||
slots: {
|
||||
default: ({ row }: any) =>
|
||||
h(ElTag, { type: row.status === 1 ? 'success' : 'info' }, () =>
|
||||
row.status === 1 ? '启用' : '禁用'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: any) => formatDateTime(row.created_at)
|
||||
}
|
||||
])
|
||||
|
||||
const columnOptions = computed(() =>
|
||||
columns.value.map((col) => ({
|
||||
label: col.label,
|
||||
prop: col.prop || col.label,
|
||||
visible: true
|
||||
}))
|
||||
)
|
||||
|
||||
// 右键菜单项
|
||||
const contextMenuItems = computed<MenuItemType[]>(() => {
|
||||
return [
|
||||
{ key: 'edit', label: '编辑' },
|
||||
{ key: 'toggle', label: currentRow.value?.status === 1 ? '禁用' : '启用' },
|
||||
{ key: 'delete', label: '删除' }
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const showEditDialog = async (row: PollingAlertRule) => {
|
||||
try {
|
||||
const res = await PollingAlertService.getPollingAlertRuleById(row.id)
|
||||
if (res.code === 0 && res.data) {
|
||||
Object.assign(form, {
|
||||
id: res.data.id,
|
||||
rule_name: res.data.rule_name,
|
||||
alert_level: res.data.alert_level,
|
||||
metric_type: res.data.metric_type,
|
||||
task_type: res.data.task_type,
|
||||
threshold: res.data.threshold,
|
||||
operator: res.data.operator,
|
||||
cooldown_minutes: res.data.cooldown_minutes,
|
||||
notify_channels: res.data.notify_channels
|
||||
})
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
watch(dialogVisible, (val) => {
|
||||
if (!val) {
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取规则详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
if (dialogType.value === 'create') {
|
||||
const res = await PollingAlertService.createPollingAlertRule(form)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('创建成功')
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
}
|
||||
} else {
|
||||
const { id, metric_type, task_type, operator, ...updateData } = form
|
||||
const res = await PollingAlertService.updatePollingAlertRule(id!, updateData)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('更新成功')
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(dialogType.value === 'create' ? '创建失败' : '更新失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const toggleStatus = async (row: PollingAlertRule) => {
|
||||
try {
|
||||
const newStatus = row.status === 1 ? 0 : 1
|
||||
const res = await PollingAlertService.updatePollingAlertRule(row.id, { status: newStatus })
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(newStatus === 1 ? '已启用' : '已禁用')
|
||||
loadData()
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (row: PollingAlertRule) => {
|
||||
ElMessageBox.confirm('确定要删除该告警规则吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await PollingAlertService.deletePollingAlertRule(row.id)
|
||||
const res = await PollingAlertService.getPollingAlertRules()
|
||||
if (res.code === 0 && res.data) {
|
||||
tableData.value = res.data.items
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
rule_name: '',
|
||||
alert_level: '',
|
||||
metric_type: '',
|
||||
task_type: '',
|
||||
threshold: 0,
|
||||
operator: '>',
|
||||
cooldown_minutes: 5,
|
||||
notify_channels: ''
|
||||
})
|
||||
}
|
||||
|
||||
const showCreateDialog = () => {
|
||||
resetForm()
|
||||
dialogType.value = 'create'
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
const showEditDialog = async (row: PollingAlertRule) => {
|
||||
try {
|
||||
const res = await PollingAlertService.getPollingAlertRuleById(row.id)
|
||||
if (res.code === 0 && res.data) {
|
||||
Object.assign(form, {
|
||||
id: res.data.id,
|
||||
rule_name: res.data.rule_name,
|
||||
alert_level: res.data.alert_level,
|
||||
metric_type: res.data.metric_type,
|
||||
task_type: res.data.task_type,
|
||||
threshold: res.data.threshold,
|
||||
operator: res.data.operator,
|
||||
cooldown_minutes: res.data.cooldown_minutes,
|
||||
notify_channels: res.data.notify_channels
|
||||
})
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取规则详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
if (dialogType.value === 'create') {
|
||||
const res = await PollingAlertService.createPollingAlertRule(form)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('创建成功')
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
}
|
||||
} else {
|
||||
const { id, metric_type, task_type, operator, ...updateData } = form
|
||||
const res = await PollingAlertService.updatePollingAlertRule(id!, updateData)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('更新成功')
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(dialogType.value === 'create' ? '创建失败' : '更新失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const toggleStatus = async (row: PollingAlertRule) => {
|
||||
try {
|
||||
const newStatus = row.status === 1 ? 0 : 1
|
||||
const res = await PollingAlertService.updatePollingAlertRule(row.id, { status: newStatus })
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
ElMessage.success(newStatus === 1 ? '已启用' : '已禁用')
|
||||
loadData()
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败')
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: PollingAlertRule, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'edit':
|
||||
showEditDialog(currentRow.value)
|
||||
break
|
||||
case 'toggle':
|
||||
toggleStatus(currentRow.value)
|
||||
break
|
||||
case 'delete':
|
||||
handleDelete(currentRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
loadData()
|
||||
const handleDelete = (row: PollingAlertRule) => {
|
||||
ElMessageBox.confirm('确定要删除该告警规则吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await PollingAlertService.deletePollingAlertRule(row.id)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: PollingAlertRule, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'edit':
|
||||
showEditDialog(currentRow.value)
|
||||
break
|
||||
case 'toggle':
|
||||
toggleStatus(currentRow.value)
|
||||
break
|
||||
case 'delete':
|
||||
handleDelete(currentRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
loadData()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.alert-rules-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.alert-rules-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -71,12 +71,7 @@
|
||||
width="500px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
|
||||
<el-form-item label="任务类型">
|
||||
<el-input v-model="currentRow.task_type_name" disabled />
|
||||
</el-form-item>
|
||||
@@ -99,194 +94,183 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { PollingConcurrencyService } from '@/api/modules'
|
||||
import type { PollingConcurrency, UpdateConcurrencyParams } from '@/types/api'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { PollingConcurrencyService } from '@/api/modules'
|
||||
import type { PollingConcurrency, UpdateConcurrencyParams } from '@/types/api'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<PollingConcurrency[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentClickRow = ref<PollingConcurrency | null>(null)
|
||||
const loading = ref(false)
|
||||
const tableData = ref<PollingConcurrency[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentClickRow = ref<PollingConcurrency | null>(null)
|
||||
|
||||
const currentRow = ref<PollingConcurrency>({
|
||||
task_type: '',
|
||||
task_type_name: '',
|
||||
max_concurrency: 0,
|
||||
current: 0,
|
||||
available: 0,
|
||||
utilization: 0
|
||||
})
|
||||
const currentRow = ref<PollingConcurrency>({
|
||||
task_type: '',
|
||||
task_type_name: '',
|
||||
max_concurrency: 0,
|
||||
current: 0,
|
||||
available: 0,
|
||||
utilization: 0
|
||||
})
|
||||
|
||||
const formData = reactive<UpdateConcurrencyParams>({
|
||||
max_concurrency: 100
|
||||
})
|
||||
const formData = reactive<UpdateConcurrencyParams>({
|
||||
max_concurrency: 100
|
||||
})
|
||||
|
||||
const formRules: FormRules = {
|
||||
max_concurrency: [
|
||||
{ required: true, message: '请输入最大并发数', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, max: 1000, message: '并发数范围为 1-1000', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const contextMenuItems = computed<MenuItemType[]>(() => [
|
||||
{
|
||||
label: '修改配置',
|
||||
key: 'edit',
|
||||
permission: 'polling_concurrency_update'
|
||||
},
|
||||
{
|
||||
label: '重置',
|
||||
key: 'reset',
|
||||
permission: 'polling_concurrency_reset'
|
||||
const formRules: FormRules = {
|
||||
max_concurrency: [
|
||||
{ required: true, message: '请输入最大并发数', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, max: 1000, message: '并发数范围为 1-1000', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const getUtilizationColor = (percentage: number) => {
|
||||
if (percentage >= 90) return '#f56c6c'
|
||||
if (percentage >= 70) return '#e6a23c'
|
||||
if (percentage >= 50) return '#409eff'
|
||||
return '#67c23a'
|
||||
}
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await PollingConcurrencyService.getConcurrencyList()
|
||||
tableData.value = data.items
|
||||
} catch (error) {
|
||||
ElMessage.error('加载并发配置失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = async (row: PollingConcurrency) => {
|
||||
try {
|
||||
// 调用接口#2:获取指定任务类型的最新并发配置
|
||||
const { data } = await PollingConcurrencyService.getConcurrencyByType(row.task_type)
|
||||
currentRow.value = { ...data }
|
||||
formData.max_concurrency = data.max_concurrency
|
||||
dialogVisible.value = true
|
||||
} catch (error) {
|
||||
ElMessage.error('获取并发配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = async (row: PollingConcurrency) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要重置 ${row.task_type_name} 的并发计数吗?`,
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await PollingConcurrencyService.resetConcurrency({ task_type: row.task_type })
|
||||
ElMessage.success('重置成功')
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('重置失败')
|
||||
const contextMenuItems = computed<MenuItemType[]>(() => [
|
||||
{
|
||||
label: '修改配置',
|
||||
key: 'edit',
|
||||
permission: 'polling_concurrency_update'
|
||||
},
|
||||
{
|
||||
label: '重置',
|
||||
key: 'reset',
|
||||
permission: 'polling_concurrency_reset'
|
||||
}
|
||||
])
|
||||
|
||||
const getUtilizationColor = (percentage: number) => {
|
||||
if (percentage >= 90) return '#f56c6c'
|
||||
if (percentage >= 70) return '#e6a23c'
|
||||
if (percentage >= 50) return '#409eff'
|
||||
return '#67c23a'
|
||||
}
|
||||
}
|
||||
|
||||
const handleResetAll = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定要重置所有任务类型的并发计数吗?',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await PollingConcurrencyService.resetConcurrency()
|
||||
ElMessage.success('重置成功')
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('重置失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async valid => {
|
||||
if (!valid) return
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await PollingConcurrencyService.updateConcurrency(
|
||||
currentRow.value.task_type,
|
||||
formData
|
||||
)
|
||||
ElMessage.success('修改成功')
|
||||
dialogVisible.value = false
|
||||
const { data } = await PollingConcurrencyService.getConcurrencyList()
|
||||
tableData.value = data.items
|
||||
} catch (error) {
|
||||
ElMessage.error('加载并发配置失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = async (row: PollingConcurrency) => {
|
||||
try {
|
||||
// 调用接口#2:获取指定任务类型的最新并发配置
|
||||
const { data } = await PollingConcurrencyService.getConcurrencyByType(row.task_type)
|
||||
currentRow.value = { ...data }
|
||||
formData.max_concurrency = data.max_concurrency
|
||||
dialogVisible.value = true
|
||||
} catch (error) {
|
||||
ElMessage.error('获取并发配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = async (row: PollingConcurrency) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要重置 ${row.task_type_name} 的并发计数吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await PollingConcurrencyService.resetConcurrency({ task_type: row.task_type })
|
||||
ElMessage.success('重置成功')
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
ElMessage.error('修改失败')
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('重置失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDialogClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: PollingConcurrency, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentClickRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentClickRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'edit':
|
||||
handleEdit(currentClickRow.value)
|
||||
break
|
||||
case 'reset':
|
||||
handleReset(currentClickRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
const handleResetAll = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要重置所有任务类型的并发计数吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await PollingConcurrencyService.resetConcurrency()
|
||||
ElMessage.success('重置成功')
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('重置失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
try {
|
||||
await PollingConcurrencyService.updateConcurrency(currentRow.value.task_type, formData)
|
||||
ElMessage.success('修改成功')
|
||||
dialogVisible.value = false
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
ElMessage.error('修改失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDialogClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: PollingConcurrency, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentClickRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentClickRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'edit':
|
||||
handleEdit(currentClickRow.value)
|
||||
break
|
||||
case 'reset':
|
||||
handleReset(currentClickRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -697,8 +697,8 @@
|
||||
|
||||
<style scoped lang="scss">
|
||||
.polling-config-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -208,7 +208,7 @@
|
||||
:total="logPagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
style=" justify-content: flex-end;margin-top: 16px"
|
||||
@size-change="getCleanupLogs"
|
||||
@current-change="getCleanupLogs"
|
||||
/>
|
||||
@@ -619,7 +619,7 @@
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--el-color-danger);
|
||||
font-weight: 600;
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -86,7 +86,11 @@
|
||||
>
|
||||
<el-form ref="batchFormRef" :model="batchForm" :rules="batchRules" label-width="120px">
|
||||
<el-form-item label="任务类型" prop="task_type">
|
||||
<el-select v-model="batchForm.task_type" placeholder="请选择任务类型" style="width: 100%">
|
||||
<el-select
|
||||
v-model="batchForm.task_type"
|
||||
placeholder="请选择任务类型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="实名认证轮询" value="polling:realname" />
|
||||
<el-option label="卡数据轮询" value="polling:carddata" />
|
||||
<el-option label="套餐轮询" value="polling:package" />
|
||||
@@ -261,7 +265,11 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务类型" prop="task_type">
|
||||
<el-select v-model="singleForm.task_type" placeholder="请选择任务类型" style="width: 100%">
|
||||
<el-select
|
||||
v-model="singleForm.task_type"
|
||||
placeholder="请选择任务类型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="实名认证轮询" value="polling:realname" />
|
||||
<el-option label="卡数据轮询" value="polling:carddata" />
|
||||
<el-option label="套餐轮询" value="polling:package" />
|
||||
@@ -278,277 +286,62 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick, h } from 'vue'
|
||||
import { ElMessage, ElMessageBox, ElTag, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { ManualTriggerService, CarrierService, ShopService, CardService } from '@/api/modules'
|
||||
import type {
|
||||
ManualTriggerLog,
|
||||
ManualTriggerHistoryQueryParams,
|
||||
BatchManualTriggerRequest,
|
||||
ConditionManualTriggerRequest,
|
||||
SingleManualTriggerRequest,
|
||||
TaskType
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { ref, reactive, computed, onMounted, nextTick, h } from 'vue'
|
||||
import { ElMessage, ElMessageBox, ElTag, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { ManualTriggerService, CarrierService, ShopService, CardService } from '@/api/modules'
|
||||
import type {
|
||||
ManualTriggerLog,
|
||||
ManualTriggerHistoryQueryParams,
|
||||
BatchManualTriggerRequest,
|
||||
ConditionManualTriggerRequest,
|
||||
SingleManualTriggerRequest,
|
||||
TaskType
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<ManualTriggerLog[]>([])
|
||||
const tableRef = ref()
|
||||
const contextMenuRef = ref()
|
||||
const currentClickRow = ref<ManualTriggerLog | null>(null)
|
||||
const loading = ref(false)
|
||||
const tableData = ref<ManualTriggerLog[]>([])
|
||||
const tableRef = ref()
|
||||
const contextMenuRef = ref()
|
||||
const currentClickRow = ref<ManualTriggerLog | null>(null)
|
||||
|
||||
const batchDialogVisible = ref(false)
|
||||
const conditionDialogVisible = ref(false)
|
||||
const singleDialogVisible = ref(false)
|
||||
const batchDialogVisible = ref(false)
|
||||
const conditionDialogVisible = ref(false)
|
||||
const singleDialogVisible = ref(false)
|
||||
|
||||
const batchFormRef = ref<FormInstance>()
|
||||
const conditionFormRef = ref<FormInstance>()
|
||||
const singleFormRef = ref<FormInstance>()
|
||||
const batchFormRef = ref<FormInstance>()
|
||||
const conditionFormRef = ref<FormInstance>()
|
||||
const singleFormRef = ref<FormInstance>()
|
||||
|
||||
const carrierList = ref<any[]>([])
|
||||
const shopTreeData = ref<any[]>([])
|
||||
const cardList = ref<any[]>([])
|
||||
const carrierList = ref<any[]>([])
|
||||
const shopTreeData = ref<any[]>([])
|
||||
const cardList = ref<any[]>([])
|
||||
|
||||
// 分页数据
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
task_type: undefined as TaskType | undefined
|
||||
})
|
||||
|
||||
// 批量触发表单
|
||||
const batchForm = reactive({
|
||||
task_type: '' as TaskType | '',
|
||||
card_ids_text: ''
|
||||
})
|
||||
|
||||
// 条件触发表单
|
||||
const conditionForm = reactive<ConditionManualTriggerRequest>({
|
||||
task_type: '' as TaskType | '',
|
||||
limit: 100,
|
||||
card_status: undefined,
|
||||
card_type: undefined,
|
||||
carrier_code: undefined,
|
||||
enable_polling: undefined,
|
||||
package_ids: undefined,
|
||||
shop_id: undefined
|
||||
})
|
||||
|
||||
// 单卡触发表单
|
||||
const singleForm = reactive({
|
||||
card_id: undefined as number | undefined,
|
||||
task_type: '' as TaskType | ''
|
||||
})
|
||||
|
||||
const batchRules: FormRules = {
|
||||
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const conditionRules: FormRules = {
|
||||
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
|
||||
limit: [{ required: true, message: '请输入限制数量', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const singleRules: FormRules = {
|
||||
card_id: [{ required: true, message: '请输入卡ID', trigger: 'blur' }],
|
||||
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
// 搜索表单配置
|
||||
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||
{
|
||||
label: '任务类型',
|
||||
prop: 'task_type',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请选择任务类型'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '实名认证轮询', value: 'polling:realname' },
|
||||
{ label: '卡数据轮询', value: 'polling:carddata' },
|
||||
{ label: '套餐轮询', value: 'polling:package' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
const typeMap: Record<string, 'info' | 'warning' | 'success' | 'danger'> = {
|
||||
pending: 'info',
|
||||
processing: 'warning',
|
||||
completed: 'success',
|
||||
cancelled: 'danger'
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
const getProgressPercentage = (row: ManualTriggerLog) => {
|
||||
if (row.total_count === 0) return 0
|
||||
return Math.round((row.processed_count / row.total_count) * 100)
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'task_type_name',
|
||||
label: '任务类型',
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
prop: 'trigger_type_name',
|
||||
label: '触发类型',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
minWidth: 100,
|
||||
slots: {
|
||||
default: ({ row }: any) =>
|
||||
h(ElTag, { type: getStatusTag(row.status) }, () => row.status_name)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '进度',
|
||||
minWidth: 200,
|
||||
slots: {
|
||||
default: ({ row }: any) =>
|
||||
h('div', { style: 'display: flex; align-items: center; gap: 8px;' }, [
|
||||
h('span', { style: 'font-size: 12px;' }, `${row.processed_count}/${row.total_count}`),
|
||||
h('el-progress', {
|
||||
percentage: getProgressPercentage(row),
|
||||
'stroke-width': 6,
|
||||
'show-text': false,
|
||||
style: 'flex: 1;'
|
||||
})
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'success_count',
|
||||
label: '成功数',
|
||||
minWidth: 100
|
||||
},
|
||||
{
|
||||
prop: 'failed_count',
|
||||
label: '失败数',
|
||||
minWidth: 100
|
||||
},
|
||||
{
|
||||
prop: 'triggered_at',
|
||||
label: '触发时间',
|
||||
minWidth: 180,
|
||||
formatter: (row: any) => formatDateTime(row.triggered_at)
|
||||
},
|
||||
{
|
||||
prop: 'completed_at',
|
||||
label: '完成时间',
|
||||
minWidth: 180,
|
||||
formatter: (row: any) => (row.completed_at ? formatDateTime(row.completed_at) : '-')
|
||||
}
|
||||
])
|
||||
|
||||
const columnOptions = computed(() =>
|
||||
columns.value.map((col) => ({
|
||||
label: col.label,
|
||||
prop: col.prop || col.label,
|
||||
visible: true
|
||||
}))
|
||||
)
|
||||
|
||||
const contextMenuItems = computed<MenuItemType[]>(() => {
|
||||
const items: MenuItemType[] = [{ label: '刷新', key: 'refresh' }]
|
||||
|
||||
if (
|
||||
currentClickRow.value &&
|
||||
(currentClickRow.value.status === 'pending' || currentClickRow.value.status === 'processing')
|
||||
) {
|
||||
items.push({
|
||||
label: '取消任务',
|
||||
key: 'cancel',
|
||||
permission: 'polling_manual_trigger_cancel'
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const queryParams: ManualTriggerHistoryQueryParams = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
...searchForm
|
||||
}
|
||||
const { data } = await ManualTriggerService.getHistory(queryParams)
|
||||
tableData.value = data.items
|
||||
pagination.total = data.total
|
||||
} catch (error) {
|
||||
ElMessage.error('加载触发历史失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
task_type: undefined
|
||||
// 分页数据
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
total: 0
|
||||
})
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadData()
|
||||
}
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
task_type: undefined as TaskType | undefined
|
||||
})
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.page_size = size
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
const showBatchTriggerDialog = () => {
|
||||
Object.assign(batchForm, {
|
||||
task_type: '',
|
||||
// 批量触发表单
|
||||
const batchForm = reactive({
|
||||
task_type: '' as TaskType | '',
|
||||
card_ids_text: ''
|
||||
})
|
||||
batchDialogVisible.value = true
|
||||
nextTick(() => {
|
||||
batchFormRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
const showConditionTriggerDialog = () => {
|
||||
Object.assign(conditionForm, {
|
||||
task_type: '',
|
||||
// 条件触发表单
|
||||
const conditionForm = reactive<ConditionManualTriggerRequest>({
|
||||
task_type: '' as TaskType | '',
|
||||
limit: 100,
|
||||
card_status: undefined,
|
||||
card_type: undefined,
|
||||
@@ -557,207 +350,422 @@ const showConditionTriggerDialog = () => {
|
||||
package_ids: undefined,
|
||||
shop_id: undefined
|
||||
})
|
||||
conditionDialogVisible.value = true
|
||||
nextTick(() => {
|
||||
conditionFormRef.value?.clearValidate()
|
||||
|
||||
// 单卡触发表单
|
||||
const singleForm = reactive({
|
||||
card_id: undefined as number | undefined,
|
||||
task_type: '' as TaskType | ''
|
||||
})
|
||||
}
|
||||
|
||||
const showSingleTriggerDialog = () => {
|
||||
Object.assign(singleForm, {
|
||||
card_id: undefined,
|
||||
task_type: ''
|
||||
})
|
||||
singleDialogVisible.value = true
|
||||
loadCards() // 加载初始卡列表
|
||||
nextTick(() => {
|
||||
singleFormRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
const batchRules: FormRules = {
|
||||
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const handleBatchSubmit = async () => {
|
||||
if (!batchFormRef.value) return
|
||||
await batchFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
const conditionRules: FormRules = {
|
||||
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
|
||||
limit: [{ required: true, message: '请输入限制数量', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析卡ID列表
|
||||
const cardIds = batchForm.card_ids_text
|
||||
.split(/[,\n]/)
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id)
|
||||
.map((id) => parseInt(id))
|
||||
.filter((id) => !isNaN(id))
|
||||
const singleRules: FormRules = {
|
||||
card_id: [{ required: true, message: '请输入卡ID', trigger: 'blur' }],
|
||||
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
if (cardIds.length > 1000) {
|
||||
ElMessage.warning('卡ID数量不能超过1000个')
|
||||
return
|
||||
}
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const requestData: BatchManualTriggerRequest = {
|
||||
task_type: batchForm.task_type as TaskType,
|
||||
card_ids: cardIds.length > 0 ? cardIds : undefined
|
||||
}
|
||||
|
||||
await ManualTriggerService.batchTrigger(requestData)
|
||||
ElMessage.success('批量触发成功')
|
||||
batchDialogVisible.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
ElMessage.error('批量触发失败')
|
||||
// 搜索表单配置
|
||||
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||
{
|
||||
label: '任务类型',
|
||||
prop: 'task_type',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请选择任务类型'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '实名认证轮询', value: 'polling:realname' },
|
||||
{ label: '卡数据轮询', value: 'polling:carddata' },
|
||||
{ label: '套餐轮询', value: 'polling:package' }
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
])
|
||||
|
||||
const handleConditionSubmit = async () => {
|
||||
if (!conditionFormRef.value) return
|
||||
await conditionFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
try {
|
||||
const requestData: ConditionManualTriggerRequest = {
|
||||
task_type: conditionForm.task_type as TaskType,
|
||||
limit: conditionForm.limit,
|
||||
card_status: conditionForm.card_status,
|
||||
card_type: conditionForm.card_type,
|
||||
carrier_code: conditionForm.carrier_code,
|
||||
enable_polling: conditionForm.enable_polling,
|
||||
package_ids: conditionForm.package_ids,
|
||||
shop_id: conditionForm.shop_id
|
||||
}
|
||||
|
||||
await ManualTriggerService.conditionTrigger(requestData)
|
||||
ElMessage.success('条件触发成功')
|
||||
conditionDialogVisible.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
ElMessage.error('条件触发失败')
|
||||
const getStatusTag = (status: string) => {
|
||||
const typeMap: Record<string, 'info' | 'warning' | 'success' | 'danger'> = {
|
||||
pending: 'info',
|
||||
processing: 'warning',
|
||||
completed: 'success',
|
||||
cancelled: 'danger'
|
||||
}
|
||||
})
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
const handleSingleSubmit = async () => {
|
||||
if (!singleFormRef.value) return
|
||||
await singleFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
const getProgressPercentage = (row: ManualTriggerLog) => {
|
||||
if (row.total_count === 0) return 0
|
||||
return Math.round((row.processed_count / row.total_count) * 100)
|
||||
}
|
||||
|
||||
try {
|
||||
await ManualTriggerService.singleTrigger({
|
||||
card_id: singleForm.card_id!,
|
||||
task_type: singleForm.task_type as TaskType
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'task_type_name',
|
||||
label: '任务类型',
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
prop: 'trigger_type_name',
|
||||
label: '触发类型',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
minWidth: 100,
|
||||
slots: {
|
||||
default: ({ row }: any) =>
|
||||
h(ElTag, { type: getStatusTag(row.status) }, () => row.status_name)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '进度',
|
||||
minWidth: 200,
|
||||
slots: {
|
||||
default: ({ row }: any) =>
|
||||
h('div', { style: 'display: flex; align-items: center; gap: 8px;' }, [
|
||||
h('span', { style: 'font-size: 12px;' }, `${row.processed_count}/${row.total_count}`),
|
||||
h('el-progress', {
|
||||
percentage: getProgressPercentage(row),
|
||||
'stroke-width': 6,
|
||||
'show-text': false,
|
||||
style: 'flex: 1;'
|
||||
})
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'success_count',
|
||||
label: '成功数',
|
||||
minWidth: 100
|
||||
},
|
||||
{
|
||||
prop: 'failed_count',
|
||||
label: '失败数',
|
||||
minWidth: 100
|
||||
},
|
||||
{
|
||||
prop: 'triggered_at',
|
||||
label: '触发时间',
|
||||
minWidth: 180,
|
||||
formatter: (row: any) => formatDateTime(row.triggered_at)
|
||||
},
|
||||
{
|
||||
prop: 'completed_at',
|
||||
label: '完成时间',
|
||||
minWidth: 180,
|
||||
formatter: (row: any) => (row.completed_at ? formatDateTime(row.completed_at) : '-')
|
||||
}
|
||||
])
|
||||
|
||||
const columnOptions = computed(() =>
|
||||
columns.value.map((col) => ({
|
||||
label: col.label,
|
||||
prop: col.prop || col.label,
|
||||
visible: true
|
||||
}))
|
||||
)
|
||||
|
||||
const contextMenuItems = computed<MenuItemType[]>(() => {
|
||||
const items: MenuItemType[] = [{ label: '刷新', key: 'refresh' }]
|
||||
|
||||
if (
|
||||
currentClickRow.value &&
|
||||
(currentClickRow.value.status === 'pending' || currentClickRow.value.status === 'processing')
|
||||
) {
|
||||
items.push({
|
||||
label: '取消任务',
|
||||
key: 'cancel',
|
||||
permission: 'polling_manual_trigger_cancel'
|
||||
})
|
||||
ElMessage.success('单卡触发成功')
|
||||
singleDialogVisible.value = false
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const queryParams: ManualTriggerHistoryQueryParams = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
...searchForm
|
||||
}
|
||||
const { data } = await ManualTriggerService.getHistory(queryParams)
|
||||
tableData.value = data.items
|
||||
pagination.total = data.total
|
||||
} catch (error) {
|
||||
ElMessage.error('加载触发历史失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
task_type: undefined
|
||||
})
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.page_size = size
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
const showBatchTriggerDialog = () => {
|
||||
Object.assign(batchForm, {
|
||||
task_type: '',
|
||||
card_ids_text: ''
|
||||
})
|
||||
batchDialogVisible.value = true
|
||||
nextTick(() => {
|
||||
batchFormRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
const showConditionTriggerDialog = () => {
|
||||
Object.assign(conditionForm, {
|
||||
task_type: '',
|
||||
limit: 100,
|
||||
card_status: undefined,
|
||||
card_type: undefined,
|
||||
carrier_code: undefined,
|
||||
enable_polling: undefined,
|
||||
package_ids: undefined,
|
||||
shop_id: undefined
|
||||
})
|
||||
conditionDialogVisible.value = true
|
||||
nextTick(() => {
|
||||
conditionFormRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
const showSingleTriggerDialog = () => {
|
||||
Object.assign(singleForm, {
|
||||
card_id: undefined,
|
||||
task_type: ''
|
||||
})
|
||||
singleDialogVisible.value = true
|
||||
loadCards() // 加载初始卡列表
|
||||
nextTick(() => {
|
||||
singleFormRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
const handleBatchSubmit = async () => {
|
||||
if (!batchFormRef.value) return
|
||||
await batchFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
try {
|
||||
// 解析卡ID列表
|
||||
const cardIds = batchForm.card_ids_text
|
||||
.split(/[,\n]/)
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id)
|
||||
.map((id) => parseInt(id))
|
||||
.filter((id) => !isNaN(id))
|
||||
|
||||
if (cardIds.length > 1000) {
|
||||
ElMessage.warning('卡ID数量不能超过1000个')
|
||||
return
|
||||
}
|
||||
|
||||
const requestData: BatchManualTriggerRequest = {
|
||||
task_type: batchForm.task_type as TaskType,
|
||||
card_ids: cardIds.length > 0 ? cardIds : undefined
|
||||
}
|
||||
|
||||
await ManualTriggerService.batchTrigger(requestData)
|
||||
ElMessage.success('批量触发成功')
|
||||
batchDialogVisible.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
ElMessage.error('批量触发失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleConditionSubmit = async () => {
|
||||
if (!conditionFormRef.value) return
|
||||
await conditionFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
try {
|
||||
const requestData: ConditionManualTriggerRequest = {
|
||||
task_type: conditionForm.task_type as TaskType,
|
||||
limit: conditionForm.limit,
|
||||
card_status: conditionForm.card_status,
|
||||
card_type: conditionForm.card_type,
|
||||
carrier_code: conditionForm.carrier_code,
|
||||
enable_polling: conditionForm.enable_polling,
|
||||
package_ids: conditionForm.package_ids,
|
||||
shop_id: conditionForm.shop_id
|
||||
}
|
||||
|
||||
await ManualTriggerService.conditionTrigger(requestData)
|
||||
ElMessage.success('条件触发成功')
|
||||
conditionDialogVisible.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
ElMessage.error('条件触发失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSingleSubmit = async () => {
|
||||
if (!singleFormRef.value) return
|
||||
await singleFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
try {
|
||||
await ManualTriggerService.singleTrigger({
|
||||
card_id: singleForm.card_id!,
|
||||
task_type: singleForm.task_type as TaskType
|
||||
})
|
||||
ElMessage.success('单卡触发成功')
|
||||
singleDialogVisible.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
ElMessage.error('单卡触发失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleCancelTask = async (row: ManualTriggerLog) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要取消该任务吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await ManualTriggerService.cancelTrigger({ trigger_id: row.id })
|
||||
ElMessage.success('任务已取消')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
ElMessage.error('单卡触发失败')
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('取消任务失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancelTask = async (row: ManualTriggerLog) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要取消该任务吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
const handleRowContextMenu = (row: ManualTriggerLog, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentClickRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
await ManualTriggerService.cancelTrigger({ trigger_id: row.id })
|
||||
ElMessage.success('任务已取消')
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentClickRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'refresh':
|
||||
loadData()
|
||||
break
|
||||
case 'cancel':
|
||||
handleCancelTask(currentClickRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 加载运营商列表
|
||||
const loadCarriers = async (carrierName?: string) => {
|
||||
try {
|
||||
const { data } = await CarrierService.getCarriers({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
carrier_name: carrierName
|
||||
})
|
||||
carrierList.value = data.items || data.list || []
|
||||
} catch (error) {
|
||||
console.error('加载运营商列表失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
const remoteSearchCarrier = (query: string) => {
|
||||
if (query) {
|
||||
loadCarriers(query)
|
||||
} else {
|
||||
loadCarriers()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载店铺树形数据
|
||||
const loadShops = async () => {
|
||||
try {
|
||||
const { data } = await ShopService.getShops()
|
||||
shopTreeData.value = data.items || data.list || []
|
||||
} catch (error) {
|
||||
console.error('加载店铺列表失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载卡列表
|
||||
const loadCards = async (iccid?: string) => {
|
||||
try {
|
||||
const { data } = await CardService.getStandaloneIotCards({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
iccid: iccid
|
||||
})
|
||||
cardList.value = data.items || data.list || []
|
||||
} catch (error) {
|
||||
console.error('加载卡列表失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
const remoteSearchCard = (query: string) => {
|
||||
if (query) {
|
||||
loadCards(query)
|
||||
} else {
|
||||
loadCards()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('取消任务失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleRowContextMenu = (row: ManualTriggerLog, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentClickRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentClickRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'refresh':
|
||||
loadData()
|
||||
break
|
||||
case 'cancel':
|
||||
handleCancelTask(currentClickRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 加载运营商列表
|
||||
const loadCarriers = async (carrierName?: string) => {
|
||||
try {
|
||||
const { data } = await CarrierService.getCarriers({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
carrier_name: carrierName
|
||||
})
|
||||
carrierList.value = data.items || data.list || []
|
||||
} catch (error) {
|
||||
console.error('加载运营商列表失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
const remoteSearchCarrier = (query: string) => {
|
||||
if (query) {
|
||||
loadCarriers(query)
|
||||
} else {
|
||||
loadCarriers()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载店铺树形数据
|
||||
const loadShops = async () => {
|
||||
try {
|
||||
const { data } = await ShopService.getShops()
|
||||
shopTreeData.value = data.items || data.list || []
|
||||
} catch (error) {
|
||||
console.error('加载店铺列表失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载卡列表
|
||||
const loadCards = async (iccid?: string) => {
|
||||
try {
|
||||
const { data } = await CardService.getStandaloneIotCards({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
iccid: iccid
|
||||
})
|
||||
cardList.value = data.items || data.list || []
|
||||
} catch (error) {
|
||||
console.error('加载卡列表失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
const remoteSearchCard = (query: string) => {
|
||||
if (query) {
|
||||
loadCards(query)
|
||||
} else {
|
||||
loadCards()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadCarriers()
|
||||
loadShops()
|
||||
})
|
||||
loadShops()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.manual-trigger-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.manual-trigger-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -290,17 +290,17 @@
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.stat-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
color: #fff;
|
||||
border-radius: 12px;
|
||||
|
||||
&.total {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
@@ -327,9 +327,9 @@
|
||||
flex: 1;
|
||||
|
||||
.stat-label {
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
@@ -345,8 +345,8 @@
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: bold;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
@@ -361,14 +361,14 @@
|
||||
|
||||
.info-item {
|
||||
.label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 500;
|
||||
margin-left: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user