feat: 添加任务管理和字典管理功能,优化系统配置和代码结构

本次提交主要包含以下变更:
1. 新增任务管理模块,支持定时任务的创建、更新、删除和状态管理
2. 新增字典管理模块,用于维护系统中常用的固定数据
3. 优化系统配置,移除Celery,改用APScheduler进行任务调度
4. 修复部分前端组件的样式问题,统一输入框宽度
5. 更新文档和相关图片资源,完善系统功能描述

这些变更旨在增强系统的可维护性和功能性,提供更灵活的任务调度机制和更便捷的字典数据管理方式。
This commit is contained in:
zhangtao
2025-04-12 06:43:27 +08:00
parent d0d3b12383
commit 00b7fe54ba
128 changed files with 6217 additions and 1181 deletions
-38
View File
@@ -1,38 +0,0 @@
import request from "@/utils/request";
export function getTaskList() {
return request({
url: "/api/v1/monitor/task/list",
method: "get",
});
}
export function getTaskDetail(taskId) {
return request({
url: `/api/v1/monitor/task/detail/${taskId}`,
method: "get",
});
}
export function createTask(body) {
return request({
url: "/api/v1/monitor/task/create",
method: "post",
data: body,
});
}
export function cancelTask(taskId) {
return request({
url: `/api/v1/monitor/task/cancel/${taskId}`,
method: "put",
});
}
export function deleteTask(taskId) {
return request({
url: `/api/v1/monitor/task/delete/${taskId}`,
method: "delete",
});
}
+122
View File
@@ -0,0 +1,122 @@
import request from "@/utils/request";
export function getDictTypeList(query) {
return request({
url: "/api/v1/system/dict/type/list",
method: "get",
params: query,
});
}
export function getDictTypeOptionselect() {
return request({
url: "/api/v1/system/dict/type/optionselect",
method: "get",
});
}
export function getDictTypeDetail(query) {
return request({
url: "/api/v1/system/dict/type/detail",
method: "get",
params: query,
});
}
export function createDictType(body) {
return request({
url: "/api/v1/system/dict/type/create",
method: "post",
data: body,
});
}
export function updateDictType(body) {
return request({
url: "/api/v1/system/dict/type/update",
method: "put",
data: body,
});
}
export function deleteDictType(query) {
return request({
url: "/api/v1/system/dict/type/delete",
method: "delete",
params: query,
});
}
export function exportDictType(body) {
return request({
url: "/api/v1/system/dict/type/export",
method: "post",
data: body,
responseType: "blob",
});
}
export function getDictDataList(query) {
return request({
url: "/api/v1/system/dict/data/list",
method: "get",
params: query,
});
}
export function getDictDataDetail(query) {
return request({
url: "/api/v1/system/dict/data/detail",
method: "get",
params: query,
});
}
export function createDictData(body) {
return request({
url: "/api/v1/system/dict/data/create",
method: "post",
data: body,
});
}
export function updateDictData(body) {
return request({
url: "/api/v1/system/dict/data/update",
method: "put",
data: body,
});
}
export function deleteDictData(query) {
return request({
url: "/api/v1/system/dict/data/delete",
method: "delete",
params: query,
});
}
export function exportDictData(body) {
return request({
url: "/api/v1/system/dict/data/export",
method: "post",
data: body,
responseType: "blob",
});
}
export function getDictTypeOption(query) {
return request({
url: "/api/v1/system/dict/type/data",
method: "get",
params: query,
});
}
export function getDictDataByType(query) {
return request({
url: "/api/v1/system/dict/data/type",
method: "get",
params: query,
});
}
+67
View File
@@ -0,0 +1,67 @@
import request from "@/utils/request";
export function getJobList(query) {
return request({
url: "/api/v1/system/job/list",
method: "get",
params: query,
});
}
export function getJobDetail(query) {
return request({
url: "/api/v1/system/job/detail",
method: "get",
params: query,
});
}
export function createJob(body) {
return request({
url: "/api/v1/system/job/create",
method: "post",
data: body,
});
}
export function updateJob(body) {
return request({
url: "/api/v1/system/job/update",
method: "put",
data: body,
});
}
export function deleteJob(query) {
return request({
url: "/api/v1/system/job/delete",
method: "delete",
params: query,
});
}
export function exportJob(body) {
return request({
url: "/api/v1/system/job/export",
method: "post",
data: body,
responseType: "blob",
});
}
export function clearJob() {
return request({
url: "/api/v1/system/job/clear",
method: "delete",
});
}
export function OptionJob(params) {
return request({
url: "/api/v1/system/job/option",
method: "put",
params: params
});
}
-251
View File
@@ -1,251 +0,0 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="任务管理列表"
:bordered="false"
:headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 250px)' }">
<template #extra>
<a-button type="primary" :icon="h(PlusOutlined)" @click="handleOpenModal('create')" style="margin-right: 10px;">
新建
</a-button>
</template>
<a-table :rowKey="record => record.task_id"
:columns="columns"
:data-source="dataSource"
:loading="tableLoading"
:scroll="{ x: 400 }"
:style="{ minHeight: '420px' }">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a @click="viewTaskDetail(record)">查看</a>
<a-popconfirm title="确定取消吗?" ok-text="确定" cancel-text="取消" @confirm="cancelTaskID(record.task_id)">
<a style="color: blue;">取消</a>
</a-popconfirm>
<a-popconfirm title="确定删除吗?" ok-text="确定" cancel-text="取消" @confirm="deleteTaskID(record.task_id)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 弹窗区域 -->
<a-modal v-model:open="openModal" @ok="handleModalSubmit" :width="800" :destroyOnClose="true"
:confirmLoading="modalSubmitLoading" style="top: 30px">
<template #title>
<span>{{ modalTitle === 'create' ? '新建任务' : '查看任务' }}</span>
</template>
<div v-if="modalTitle === 'view'">
<a-spin :spinning="detailLoading">
<a-descriptions :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }"
:labelStyle="{ width: '140px' }" bordered>
<a-descriptions-item label="任务编号">{{ taskDetail.task_id || '无' }}</a-descriptions-item>
<a-descriptions-item label="任务状态">{{ taskDetail.status || '无' }}</a-descriptions-item>
<a-descriptions-item label="任务结果">
{{ typeof taskDetail.result === 'object' ? JSON.stringify(taskDetail.result, null, 2) : taskDetail.result || '无' }}
</a-descriptions-item>
<a-descriptions-item label="任务完成日期">
{{ taskDetail.date_done ? dayjs(taskDetail.date_done).format('YYYY-MM-DD HH:mm:ss') : '无' }}
</a-descriptions-item>
</a-descriptions>
</a-spin>
</div>
<div v-else-if="modalTitle === 'create'">
<a-form ref="createForm" :model="createFormState"
:labelCol="{ span: 5 }" :wrapperCol="{ span: 15 }">
<a-form-item name="res" label="任务参数" :rules="[{ required: true, message: '请输入任务参数' }]">
<a-input v-model:value="createFormState.res" placeholder="请输入任务参数" allowClear />
</a-form-item>
<a-form-item name="is_cron" label="是否开启定时任务">
<a-switch v-model:checked="createFormState.is_cron" />
</a-form-item>
<a-form-item v-if="createFormState.is_cron" name="cron_expression" label="定时策略" :rules="[{ required: true, message: '请输入定时策略' }]">
<a-input v-model:value="createFormState.cron_expression" placeholder="请输入 Cron 表达式" allowClear />
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted, h } from 'vue';
import { message } from 'ant-design-vue';
import type { TableColumnsType } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { cloneDeep, isEmpty } from '@/utils/util';
import PageHeader from '@/components/PageHeader.vue';
import { getTaskList, getTaskDetail, createTask, cancelTask, deleteTask } from '@/api/monitor/task';
import dayjs from 'dayjs';
// 类型定义
interface Task {
task_id?: string;
result?: string;
status?: string;
date_done?: string;
res?: string;
is_cron?: boolean;
cron_expression?: string;
}
// 状态管理
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref<'create' | 'view'>('create');
const modalSubmitLoading = ref(false);
const detailLoading = ref(false);
const dataSource = ref<Task[]>([]);
const taskDetail = ref<Task>({});
const createFormState = reactive<Task>({
res: '',
is_cron: false,
cron_expression: '',
});
const createForm = ref(); // 表单引用
// 表格列配置
const columns: TableColumnsType = [
{
title: '任务编号',
dataIndex: 'task_id',
ellipsis: true,
},
{
title: '任务状态',
dataIndex: ['result', 'status'],
ellipsis: true,
},
{
title: '任务结果',
dataIndex: ['result', 'result'],
ellipsis: true,
customRender: ({ text }) => {
if (text && text.exc_type) {
return `异常: ${text.exc_type} - ${text.exc_message.join(', ')}`;
}
return text || '无';
},
},
{
title: '完成时间',
dataIndex: ['result', 'date_done'],
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '无';
},
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
fixed: 'right',
ellipsis: true,
width: 180,
},
];
// 加载表格数据
const loadTableData = async () => {
tableLoading.value = true;
try {
const response = await getTaskList();
dataSource.value = response.data.data; // 移除 index 字段
} catch (error) {
message.error('加载任务列表失败');
} finally {
tableLoading.value = false;
}
};
// 查看任务详情
const viewTaskDetail = async (record: Task) => {
detailLoading.value = true;
try {
const response = await getTaskDetail(record.task_id!);
taskDetail.value = response.data.data;
modalTitle.value = 'view';
openModal.value = true;
} catch (error) {
message.error('获取任务详情失败');
} finally {
detailLoading.value = false;
}
};
// 取消任务
const cancelTaskID = async (taskId: string) => {
try {
const response = await cancelTask(taskId);
message.success(response.data.msg);
loadTableData();
} catch (error) {
message.error('取消任务失败');
}
};
// 删除任务
const deleteTaskID = async (taskId: string) => {
try {
const response = await deleteTask(taskId);
message.success(response.data.msg);
loadTableData();
} catch (error) {
message.error('删除任务失败');
}
};
// 提交新建任务
const handleModalSubmit = async () => {
try {
await createForm.value.validate(); // 确保表单验证通过
const createBody = cloneDeep(createFormState);
// 如果未开启定时任务,移除 cron_expression 字段
if (!createBody.is_cron) {
delete createBody.cron_expression;
}
// 提交任务
const response = await createTask(createBody);
message.success(response.data.msg);
openModal.value = false;
createForm.value.resetFields(); // 重置表单
loadTableData();
} catch (error) {
if (error.errorFields) {
message.error('请填写完整任务参数');
} else {
message.error('创建任务失败');
}
}
};
// 打开弹窗
const handleOpenModal = (type: 'create' | 'view') => {
modalTitle.value = type;
openModal.value = true;
if (type === 'create') {
createForm.value?.resetFields(); // 重置表单
}
};
// 生命周期钩子
onMounted(() => loadTableData());
</script>
<style lang="scss" scoped>
.table-wrapper {
margin-block-end: 16px;
}
</style>
+2 -2
View File
@@ -144,7 +144,7 @@
label="排序"
:rules="[{ required: true, message: '请输入排序' }]"
>
<a-input-number v-model:value="createState.order" :min="1" />
<a-input-number v-model:value="createState.order" :min="1" style="width: 100%"/>
</a-form-item>
<a-form-item name="parent_id" label="上级部门">
@@ -204,7 +204,7 @@
label="排序"
:rules="[{ required: true, message: '请输入排序' }]"
>
<a-input-number v-model:value="updateState.order" :min="1" />
<a-input-number v-model:value="updateState.order" :min="1" style="width: 100%"/>
</a-form-item>
<a-form-item name="parent_id" label="上级部门">
+631
View File
@@ -0,0 +1,631 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 搜索表单 -->
<div class="table-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="dict_type" label="字典" style="max-width: 300px;">
<a-select v-model:value="queryState.dict_type" placeholder="请选择字典名称" allowClear @dropdownVisibleChange="loadDictTypes">
<a-select-option v-for="item in dictTypeOptions" :key="item.dict_type" :value="item.dict_type">
{{ item.dict_name }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="dict_name" label="标签" style="max-width: 300px;">
<a-input v-model:value="queryState.dict_label" placeholder="请输入字典数据标签"
allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="available" label="状态" style="max-width: 300px;">
<a-select v-model:value="queryState.available" placeholder="全部" allowClear>
<a-select-option value="1">启用</a-select-option>
<a-select-option value="0">停用</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="数据字典列表"
:bordered="false"
:headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-button type="primary" :icon="h(PlusOutlined)" @click="modalHandle('create')"
style="margin-right: 10px;">新建</a-button>
<a-button type="primary" :icon="h(DownOutlined)" @click="handleExport"
style="margin-right: 10px;">导出
</a-button>
</template>
<a-table :rowKey="record => record.id"
:columns="columns"
:data-source="dataSource"
:row-selection="rowSelection"
:loading="tableLoading"
@change="handleTableChange"
:scroll="{ x: 400 }"
:pagination="pagination"
:style="{ minHeight: '420px' }"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.dataIndex === 'is_default'">
<span>
<a-badge :status="record.is_default ? 'processing': 'error'" :text="record.is_default ? '是' : '否'" />
</span>
</template>
<template v-if="column.dataIndex === 'available'">
<span>
<a-badge :status="record.available ? 'processing': 'error'" :text="record.available ? '启用' : '停用'" />
</span>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a @click="modalHandle('view', index)">查看</a>
<a @click="modalHandle('update', index)">修改</a>
<a-popconfirm title="确定删除吗?" ok-text="确定" cancel-text="取消" @confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal v-model:open="openModal" @ok="handleModalSumbit" :width="800" :destroyOnClose="true"
:confirmLoading="modalSubmitLoading" style="top: 30px">
<template #title>
<span>{{ modalTitle === 'create' ? '新建数据字典' : (modalTitle === 'view' ? '查看数据字典' : '修改数据字典')
}}</span>
</template>
<div v-if="modalTitle === 'view'">
<a-spin :spinning="detailStateLoading">
<a-descriptions :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }"
:labelStyle="{ width: '140px' }" bordered>
<a-descriptions-item label="序号">{{ (pagination.current - 1) * pagination.pageSize +
detailState.index + 1 }}</a-descriptions-item>
<a-descriptions-item label="字典排序">{{ detailState.dict_sort }}</a-descriptions-item>
<a-descriptions-item label="字典标签">{{ detailState.dict_label }}</a-descriptions-item>
<a-descriptions-item label="字典键值">{{ detailState.dict_value }}</a-descriptions-item>
<a-descriptions-item label="字典类型">{{ detailState.dict_type }}</a-descriptions-item>
<a-descriptions-item label="样式属性">{{ detailState.css_class }}</a-descriptions-item>
<a-descriptions-item label="表格回显样式">{{ detailState.list_class }}</a-descriptions-item>
<a-descriptions-item label="是否默认">
<a-badge :status="detailState.is_default ? 'processing': 'error'" :text="detailState.is_default ? '是' : '否'" />
</a-descriptions-item>
<a-descriptions-item label="状态">
<a-badge :status="detailState.available ? 'processing': 'error'" :text="detailState.available ? '启用' : '停用'" />
</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detailState.creator ? detailState.creator.name : '-'
}}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailState.created_at }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ detailState.updated_at }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ detailState.description
}}</a-descriptions-item>
</a-descriptions>
</a-spin>
</div>
<div v-else-if="modalTitle === 'create'">
<a-form ref="createForm" :model="createState"
v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="dict_type" label="字典类型">
<a-input v-model:value="createState.dict_type" placeholder="请输入字典类型" :disabled="true"></a-input>
</a-form-item>
<a-form-item name="dict_sort" label="字典排序" :rules="[{ required: true, message: '请输入字典排序', pattern: /^[0-9]*$/ }]">
<a-input-number v-model:value="createState.dict_sort" placeholder="请输入字典排序" :min="1" :precision="0" style="width: 100%"/>
</a-form-item>
<a-form-item name="dict_label" label="字典标签" :rules="[{ required: true, message: '请输入字典标签' }]">
<a-input v-model:value="createState.dict_label" placeholder="请输入字典标签" allowClear></a-input>
</a-form-item>
<a-form-item name="dict_value" label="字典键值" :rules="[{ required: true, message: '请输入字典键值' }]">
<a-input v-model:value="createState.dict_value" placeholder="请输入字典键值" allowClear></a-input>
</a-form-item>
<a-form-item name="css_class" label="样式属性" :rules="[{ required: false, message: '请输入样式属性' }]">
<a-input v-model:value="createState.css_class" placeholder="请输入样式属性" allowClear></a-input>
</a-form-item>
<a-form-item name="list_class" label="表格回显样式" :rules="[{ required: false, message: '请选择表格回显样式' }]">
<a-select v-model:value="createState.list_class" placeholder="请选择表格回显样式" allowClear>
<a-select-option v-for="item in listClassOptions" :key="item.value" :value="item.value">
{{ item.label }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="is_default" label="是否默认" :rules="[{ required: true, message: '请选择是否默认' }]">
<a-radio-group v-model:value="createState.is_default">
<a-radio :value="true"></a-radio>
<a-radio :value="false"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="createState.available">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="createState.description" placeholder="请输入备注" :rows="4"
allowClear />
</a-form-item>
</a-form>
</div>
<div v-else>
<a-form ref="updateForm" :model="updateState"
v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="dict_type" label="字典类型">
<a-input v-model:value="updateState.dict_type" placeholder="请输入字典类型" :disabled="true"></a-input>
</a-form-item>
<a-form-item name="dict_sort" label="字典排序" :rules="[{ required: true, message: '请输入字典排序', pattern: /^[0-9]*$/ }]">
<a-input-number v-model:value="updateState.dict_sort" placeholder="请输入字典排序" :min="1" :precision="0" style="width: 100%"/>
</a-form-item>
<a-form-item name="dict_label" label="字典标签" :rules="[{ required: true, message: '请输入字典标签' }]">
<a-input v-model:value="updateState.dict_label" placeholder="请输入字典标签" allowClear></a-input>
</a-form-item>
<a-form-item name="dict_value" label="字典键值" :rules="[{ required: true, message: '请输入字典键值' }]">
<a-input v-model:value="updateState.dict_value" placeholder="请输入字典键值" allowClear></a-input>
</a-form-item>
<a-form-item name="css_class" label="样式属性" :rules="[{ required: false, message: '请输入样式属性' }]">
<a-input v-model:value="updateState.css_class" placeholder="请输入样式属性" allowClear></a-input>
</a-form-item>
<a-form-item name="list_class" label="表格回显样式" :rules="[{ required: false, message: '请选择表格回显样式' }]">
<a-select v-model:value="updateState.list_class" placeholder="请选择表格回显样式" allowClear>
<a-select-option v-for="item in listClassOptions" :key="item.value" :value="item.value">
{{ item.label }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="is_default" label="是否默认" :rules="[{ required: true, message: '请选择是否默认' }]">
<a-radio-group v-model:value="updateState.is_default">
<a-radio :value="true"></a-radio>
<a-radio :value="false"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="updateState.available">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="updateState.description" placeholder="请输入备注" :rows="4"
allowClear />
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref, onMounted, h } from 'vue';
import { useRoute } from 'vue-router';
import { Table, message, Modal } from 'ant-design-vue';
import type { TableColumnsType, MenuProps } from 'ant-design-vue';
import { PlusOutlined, DownOutlined, CheckOutlined, StopOutlined } from '@ant-design/icons-vue';
import { cloneDeep, isEmpty } from '@/utils/util';
import PageHeader from '@/components/PageHeader.vue';
import { getDictTypeOptionselect, getDictTypeList,getDictTypeDetail,createDictType,updateDictType,deleteDictType,exportDictType,getDictDataList,getDictDataDetail,createDictData,updateDictData,deleteDictData,exportDictData,getDictTypeOption,getDictDataByType } from '@/api/system/dict'
import type { searchDictDataType, tableDictDataType } from './types'
const route = useRoute();
const createForm = ref();
const updateForm = ref();
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const dataSource = ref<tableDictDataType[]>([]);
const selectedRowKeys = ref<tableDictDataType['id'][]>([]);
// 数据标签回显样式
const listClassOptions = ref([
{ value: "default", label: "默认(default)" },
{ value: "primary", label: "主要(primary)" },
{ value: "success", label: "成功(success)" },
{ value: "info", label: "信息(info)" },
{ value: "warning", label: "警告(warning)" },
{ value: "danger", label: "危险(danger)" }
]);
const queryState = reactive<searchDictDataType>({
dict_label: null,
dict_type: null,
available: null
});
const dictTypeOptions = ref<any[]>([]);
const pagination = reactive({
current: 1,
pageSize: 10,
defaultPageSize: 10,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `${range[0]}-${range[1]} 条 / 总共 ${total}`
})
const createState = reactive<tableDictDataType>({
dict_sort: null,
dict_label: '',
dict_value: '',
dict_type: '',
css_class: '',
list_class: null,
is_default: false,
available: true,
description: ''
})
const updateState = reactive<tableDictDataType>({
id: undefined,
dict_sort: null,
dict_label: '',
dict_value: '',
dict_type: '',
css_class: '',
list_class: null,
is_default: false,
available: true,
description: ''
})
const detailState = ref<tableDictDataType>({})
const columns: TableColumnsType = [
{
title: '序号',
dataIndex: 'index',
align: 'center',
ellipsis: true,
width: 80
},
{
title: '字典排序',
dataIndex: 'dict_sort',
ellipsis: true,
// align: 'center'
},
{
title: '字典标签',
dataIndex: 'dict_label',
ellipsis: true,
// align: 'center'
},
{
title: '字典键值',
dataIndex: 'dict_value',
ellipsis: true,
// align: 'center'
},
{
title: '字典类型',
dataIndex: 'dict_type',
ellipsis: true,
// align: 'center'
},
// {
// title: '样式属性',
// dataIndex: 'css_class',
// ellipsis: true,
// // align: 'center'
// },
// {
// title: '表格回显样式',
// dataIndex: 'list_class',
// ellipsis: true,
// // align: 'center'
// },
{
title: '是否默认',
dataIndex: 'is_default',
ellipsis: true,
// align: 'center'
},
{
title: '状态',
dataIndex: 'available',
// ellipsis: true,
align: 'center'
},
{
title: '备注',
dataIndex: 'description',
// align: 'center',
ellipsis: true,
// width: 500
},
{
title: '创建日期',
dataIndex: 'created_at',
// align: 'center',
ellipsis: true,
// width: 120
},
{
title: '更新日期',
dataIndex: 'updated_at',
// align: 'center',
ellipsis: true,
// width: 120
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
fixed: 'right',
ellipsis: true,
width: 150
}
];
// 表格选中配置
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableDictDataType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
// 加载字典类型
const loadDictTypes = () => {
getDictTypeOptionselect().then(response => {
const result = response.data;
dictTypeOptions.value = result.data;
message.success(result.msg);
}).catch(error => {
console.log(error);
message.error('获取字典类型失败');
});
}
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.dict_label) {
params['dict_label'] = queryState.dict_label
}
if (queryState.dict_type) {
params['dict_type'] = queryState.dict_type
} else if (route.query.dict_type) {
params['dict_type'] = Array.isArray(route.query.dict_type) ? route.query.dict_type[0] : route.query.dict_type
queryState.dict_type = Array.isArray(route.query.dict_type) ? route.query.dict_type[0] : route.query.dict_type
}
if (queryState.available) {
params['available'] = queryState.available == true ? true : false;
}
params['page_no'] = pagination.current
params['page_size'] = pagination.pageSize
getDictDataList(params).then(response => {
const result = response.data;
dataSource.value = result.data.items;
pagination.total = result.data.total;
pagination.current = result.data.page_no;
pagination.pageSize = result.data.page_size;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 生命周期钩子
onMounted(() => {
loadingData();
});
// 查询
const onFinish = () => {
pagination.current = 1;
loadingData();
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
pagination.current = 1;
queryState.available = null
loadingData();
}
// 表格分页处理
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
}
// 弹窗关键字处理
const modalHandle = (modalType: string, index?: number) => {
modalTitle.value = modalType;
openModal.value = true;
// 重新创建 createState 和 updateState 以重置为初始状态
Object.assign(createState, {
dict_sort: null,
dict_label: '',
dict_value: '',
dict_type: route.query.dict_type || '',
css_class: '',
list_class: null,
is_default: false,
available: true,
description: ''
});
Object.assign(updateState, {
id: undefined,
dict_sort: null,
dict_label: '',
dict_value: '',
dict_type: '',
css_class: '',
list_class: null,
is_default: false,
available: true,
description: ''
});
if (modalType === 'view' && index !== undefined) {
detailStateLoading.value = true;
detailState.value = dataSource.value[index];
detailState.value.index = index;
detailStateLoading.value = false;
} else if (modalType === 'update' && index !== undefined) {
const selected = dataSource.value[index];
Object.keys(updateState).forEach(key => {
updateState[key] = selected[key];
})
}
}
// 删除
const deleteRow = (row: tableDictDataType) => {
deleteDictData({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error)
})
}
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = cloneDeep(createState);
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
})
createDictData(createBody).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key]);
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
updateDictData(updateState).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
message.success(response.data.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}
}
// 导出按钮操作
const handleExport = () => {
Modal.confirm({
title: '警告',
content: '是否确认导出所有数据字典数据?',
onOk() {
const body = {
...queryState,
page_no: 1,
page_size: pagination.total
};
message.loading('正在导出数据,请稍候...', 0);
return exportDictData(body).then(response => {
const blob = new Blob([response.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
// 从响应头获取文件名
const contentDisposition = response.headers['content-disposition'];
let fileName = '数据字典.xlsx';
if (contentDisposition) {
const fileNameMatch = contentDisposition.match(/filename=(.*?)(;|$)/);
if (fileNameMatch) {
fileName = decodeURIComponent(fileNameMatch[1]);
}
}
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
message.destroy();
message.success('导出成功');
}).catch((error) => {
message.destroy();
console.error('导出错误:', error);
message.error('文件处理失败');
});
},
onCancel() {
message.info('已取消导出');
}
});
};
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
</style>
+498
View File
@@ -0,0 +1,498 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 搜索表单 -->
<div class="table-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="dict_name" label="名称" style="max-width: 300px;">
<a-input v-model:value="queryState.dict_name" placeholder="请输入字典名称"
allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="dict_type" label="类型" style="max-width: 300px;">
<a-input v-model:value="queryState.dict_type" placeholder="请输入字典类型"
allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="available" label="状态" style="max-width: 300px;">
<a-select v-model:value="queryState.available" placeholder="全部" allowClear>
<a-select-option value="1">启用</a-select-option>
<a-select-option value="0">停用</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="字典类型列表"
:bordered="false"
:headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-button type="primary" :icon="h(PlusOutlined)" @click="modalHandle('create')"
style="margin-right: 10px;">新建</a-button>
<a-button type="primary" :icon="h(DownOutlined)" @click="handleExport"
style="margin-right: 10px;">导出
</a-button>
</template>
<a-table :rowKey="record => record.id"
:columns="columns"
:data-source="dataSource"
:row-selection="rowSelection"
:loading="tableLoading"
@change="handleTableChange"
:scroll="{ x: 400, y: 370 }"
:pagination="pagination"
:style="{ minHeight: '400px' }"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.dataIndex === 'dict_type'">
<router-link :to="'/system/dict_data?dict_type=' + record.dict_type" class="link-type">
<span>{{ record.dict_type }}</span>
</router-link>
</template>
<template v-if="column.dataIndex === 'available'">
<span>
<a-badge :status="record.available ? 'processing': 'error'" :text="record.available ? '启用' : '停用'" />
</span>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a @click="modalHandle('view', index)">查看</a>
<a @click="modalHandle('update', index)">修改</a>
<a-popconfirm title="确定删除吗?" ok-text="确定" cancel-text="取消" @confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal v-model:open="openModal" @ok="handleModalSumbit" :width="800" :destroyOnClose="true"
:confirmLoading="modalSubmitLoading" style="top: 30px">
<template #title>
<span>{{ modalTitle === 'create' ? '新建字典类型' : (modalTitle === 'view' ? '查看字典类型' : '修改字典类型')
}}</span>
</template>
<div v-if="modalTitle === 'view'">
<a-spin :spinning="detailStateLoading">
<a-descriptions :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }"
:labelStyle="{ width: '140px' }" bordered>
<a-descriptions-item label="序号">{{ (pagination.current - 1) * pagination.pageSize +
detailState.index + 1 }}</a-descriptions-item>
<a-descriptions-item label="字典名称">{{ detailState.dict_name }}</a-descriptions-item>
<a-descriptions-item label="字典类型">{{ detailState.dict_type }}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-badge :status="detailState.available ? 'processing': 'error'" :text="detailState.available ? '启用' : '停用'" />
</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detailState.creator ? detailState.creator.name : '-'
}}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailState.created_at }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ detailState.updated_at }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ detailState.description
}}</a-descriptions-item>
</a-descriptions>
</a-spin>
</div>
<div v-else-if="modalTitle === 'create'">
<a-form ref="createForm" :model="createState"
v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="dict_name" label="字典名称" :rules="[{ required: true, message: '请输入字典名称' }]">
<a-input v-model:value="createState.dict_name" placeholder="请输入字典名称" allowClear></a-input>
</a-form-item>
<a-form-item name="dict_type" label="字典类型" :rules="[{ required: true, message: '请输入字典类型' }]">
<a-input v-model:value="createState.dict_type" placeholder="请输入字典类型" allowClear></a-input>
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="createState.available">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="createState.description" placeholder="请输入备注" :rows="4"
allowClear />
</a-form-item>
</a-form>
</div>
<div v-else>
<a-form ref="updateForm" :model="updateState"
v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="dict_name" label="字典名称" :rules="[{ required: true, message: '请输入字典名称' }]">
<a-input v-model:value="updateState.dict_name" placeholder="请输入字典名称" allowClear></a-input>
</a-form-item>
<a-form-item name="dict_type" label="字典类型" :rules="[{ required: true, message: '请输入字典类型' }]">
<a-input v-model:value="updateState.dict_type" placeholder="请输入字典类型" allowClear></a-input>
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="updateState.available">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="updateState.description" placeholder="请输入备注" :rows="4"
allowClear />
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref, onMounted, h } from 'vue';
import { Table, message, Modal } from 'ant-design-vue';
import type { TableColumnsType, MenuProps } from 'ant-design-vue';
import { PlusOutlined, DownOutlined, CheckOutlined, StopOutlined } from '@ant-design/icons-vue';
import { cloneDeep, isEmpty } from '@/utils/util';
import PageHeader from '@/components/PageHeader.vue';
import { getDictTypeList,getDictTypeDetail,createDictType,updateDictType,deleteDictType,exportDictType,getDictDataList,getDictDataDetail,createDictData,updateDictData,deleteDictData,exportDictData,getDictTypeOption,getDictDataByType } from '@/api/system/dict'
import type { searchDataType, tableDictType } from './types'
const createForm = ref();
const updateForm = ref();
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const dataSource = ref<tableDictType[]>([]);
const selectedRowKeys = ref<tableDictType['id'][]>([]);
const queryState = reactive<searchDataType>({
dict_name: null,
dict_type: null,
available: null
});
const pagination = reactive({
current: 1,
pageSize: 10,
defaultPageSize: 10,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `${range[0]}-${range[1]} 条 / 总共 ${total}`
})
const createState = reactive<tableDictType>({
dict_name: '',
dict_type: '',
available: true,
description: ''
})
const updateState = reactive<tableDictType>({
id: undefined,
dict_name: '',
dict_type: '',
available: true,
description: ''
})
const detailState = ref<tableDictType>({})
const columns: TableColumnsType = [
{
title: '序号',
dataIndex: 'index',
align: 'center',
ellipsis: true,
width: 80
},
{
title: '字典名称',
dataIndex: 'dict_name',
ellipsis: true,
// align: 'center'
},
{
title: '字典类型',
dataIndex: 'dict_type',
ellipsis: true,
// align: 'center'
},
{
title: '状态',
dataIndex: 'available',
// ellipsis: true,
align: 'center'
},
{
title: '备注',
dataIndex: 'description',
// align: 'center',
ellipsis: true,
// width: 500
},
{
title: '创建日期',
dataIndex: 'created_at',
// align: 'center',
ellipsis: true,
// width: 120
},
{
title: '更新日期',
dataIndex: 'updated_at',
// align: 'center',
ellipsis: true,
// width: 120
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
fixed: 'right',
ellipsis: true,
width: 150
}
];
// 表格选中配置
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableDictType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.dict_name) {
params['dict_name'] = queryState.dict_name
}
if (queryState.dict_type) {
params['dict_type'] = queryState.dict_type
}
if (queryState.available) {
params['available'] = queryState.available == true ? true : false;
}
params['page_no'] = pagination.current
params['page_size'] = pagination.pageSize
getDictTypeList(params).then(response => {
const result = response.data;
dataSource.value = result.data.items;
pagination.total = result.data.total;
pagination.current = result.data.page_no;
pagination.pageSize = result.data.page_size;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 生命周期钩子
onMounted(() => loadingData());
// 查询
const onFinish = () => {
pagination.current = 1;
loadingData();
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
pagination.current = 1;
queryState.available = null
loadingData();
}
// 表格分页处理
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
}
// 弹窗关键字处理
const modalHandle = (modalType: string, index?: number) => {
modalTitle.value = modalType;
openModal.value = true;
// 重新创建 createState 和 updateState 以重置为初始状态
Object.assign(createState, {
dict_name: '',
dict_type: '',
available: true,
description: ''
});
Object.assign(updateState, {
id: undefined,
dict_name: '',
dict_type: '',
available: true,
description: ''
});
if (modalType === 'view' && index !== undefined) {
detailStateLoading.value = true;
detailState.value = dataSource.value[index];
detailState.value.index = index;
detailStateLoading.value = false;
} else if (modalType === 'update' && index !== undefined) {
const selected = dataSource.value[index];
Object.keys(updateState).forEach(key => {
updateState[key] = selected[key];
})
}
}
// 删除
const deleteRow = (row: tableDictType) => {
deleteDictType({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error)
})
}
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = cloneDeep(createState);
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
})
createDictType(createBody).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key]);
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
updateDictType(updateState).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
message.success(response.data.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}
}
// 导出按钮操作
const handleExport = () => {
Modal.confirm({
title: '警告',
content: '是否确认导出所有字典类型数据?',
onOk() {
const body = {
...queryState,
page_no: 1,
page_size: pagination.total
};
message.loading('正在导出数据,请稍候...', 0);
return exportDictType(body).then(response => {
const blob = new Blob([response.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
// 从响应头获取文件名
const contentDisposition = response.headers['content-disposition'];
let fileName = '字典类型.xlsx';
if (contentDisposition) {
const fileNameMatch = contentDisposition.match(/filename=(.*?)(;|$)/);
if (fileNameMatch) {
fileName = decodeURIComponent(fileNameMatch[1]);
}
}
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
message.destroy();
message.success('导出成功');
}).catch((error) => {
message.destroy();
console.error('导出错误:', error);
message.error('文件处理失败');
});
},
onCancel() {
message.info('已取消导出');
}
});
};
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
</style>
+47
View File
@@ -0,0 +1,47 @@
export interface searchDataType {
dict_name?: string
dict_type?: string
available?: boolean
}
export interface searchDictDataType {
dict_label?: string
dict_type?: string
available?: boolean
}
export interface tableDictType {
id?: number;
index?: number;
dict_name?: string;
dict_type?: string;
available?: boolean;
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
export interface tableDictDataType {
id?: number;
index?: number;
dict_sort?: number;
dict_label?: string;
dict_value?: string;
dict_type?: string;
css_class?: string;
list_class?: string;
is_default?: boolean;
available?: boolean;
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
interface creatorType {
id?: number;
name?: string;
username?: string;
}
+629
View File
@@ -0,0 +1,629 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 搜索表单 -->
<div class="table-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="name" label="名称" style="max-width: 300px;">
<a-input v-model:value="queryState.name" placeholder="请输入任务名称"
allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="status" label="状态" style="max-width: 300px;">
<a-select v-model:value="queryState.status" placeholder="全部" allowClear>
<a-select-option value="1">启用</a-select-option>
<a-select-option value="0">停用</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="date-range-picker" label="创建日期" style="max-width: 350px;">
<a-range-picker v-model:value="queryState.date_range" value-format="YYYY-MM-DD" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="定时任务列表"
:bordered="false"
:headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-button type="primary" :icon="h(PlusOutlined)" @click="modalHandle('create')"
style="margin-right: 10px;">新建</a-button>
<a-button type="primary" :icon="h(DownOutlined)" @click="handleExport"
style="margin-right: 10px;">导出
</a-button>
<a-button type="primary" danger :icon="h(DeleteOutlined)" @click="handleClear"
style="margin-right: 10px;">清除
</a-button>
</template>
<a-table :rowKey="record => record.id"
:columns="columns"
:data-source="dataSource"
:row-selection="rowSelection"
:loading="tableLoading"
@change="handleTableChange"
:scroll="{ x: 400 }"
:pagination="pagination"
:style="{ minHeight: '420px' }"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.dataIndex === 'status'">
<span>
<a-badge :status="record.status ? 'processing': 'error'" :text="record.status ? '启用' : '停用'" />
</span>
</template>
<template v-if="column.dataIndex === 'coalesce'">
<span>
<a-badge :status="record.coalesce ? 'processing': 'error'" :text="record.status ? '是' : '否'" />
</span>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a @click="modalHandle('view', index)">查看</a>
<a @click="modalHandle('update', index)">修改</a>
<a-popconfirm title="确定删除吗?" ok-text="确定" cancel-text="取消" @confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
<a-dropdown>
<a>更多 <DownOutlined /></a>
<template #overlay>
<a-menu>
<a-menu-item @click="handleOption(record, 1)">暂停</a-menu-item>
<a-menu-item @click="handleOption(record, 2)">恢复</a-menu-item>
<a-menu-item @click="handleOption(record, 3)">重启</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal v-model:open="openModal" @ok="handleModalSumbit" :width="800" :destroyOnClose="true"
:confirmLoading="modalSubmitLoading" style="top: 30px">
<template #title>
<span>
{{ modalTitle === 'create' ? '新建定时任务' : (modalTitle === 'view' ? '查看定时任务' : '修改定时任务')}}
</span>
</template>
<div v-if="modalTitle === 'view'">
<a-spin :spinning="detailStateLoading">
<a-descriptions :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }"
:labelStyle="{ width: '140px' }" bordered>
<a-descriptions-item label="序号">{{ (pagination.current - 1) * pagination.pageSize + detailState.index + 1 }}</a-descriptions-item>
<a-descriptions-item label="任务名称">{{ detailState.name }}</a-descriptions-item>
<a-descriptions-item label="任务函数">{{ detailState.func }}</a-descriptions-item>
<a-descriptions-item label="触发器">
{{ detailState.trigger }}
</a-descriptions-item>
<a-descriptions-item label="位置参数">{{ detailState.args }}</a-descriptions-item>
<a-descriptions-item label="关键字参数">{{ detailState.kwargs }}</a-descriptions-item>
<a-descriptions-item label="是否并行">
<a-badge :status="detailState.coalesce ? 'processing': 'error'" :text="detailState.coalesce ? '是' : '否'" />
</a-descriptions-item>
<a-descriptions-item label="最大实例数">{{ detailState.max_instances }}</a-descriptions-item>
<a-descriptions-item label="任务存储">
{{ detailState.jobstore }}
</a-descriptions-item>
<a-descriptions-item label="任务执行器">
{{ detailState.executor }}
</a-descriptions-item>
<a-descriptions-item label="触发器参数">{{ detailState.trigger_args }}</a-descriptions-item>
<a-descriptions-item label="任务状态">
<a-badge :status="detailState.status ? 'processing': 'error'" :text="detailState.status ? '启用' : '停用'" />
</a-descriptions-item>
<a-descriptions-item label="日志信息">{{ detailState.message }}</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detailState.creator ? detailState.creator.name : '-'}}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailState.created_at }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ detailState.updated_at }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ detailState.description}}</a-descriptions-item>
</a-descriptions>
</a-spin>
</div>
<div v-else-if="modalTitle === 'create'">
<a-form ref="createForm" :model="createState"
v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="任务名称" :rules="[{ required: true, message: '请输入任务名称' }]">
<a-input v-model:value="createState.name" placeholder="请输入任务名称" allowClear></a-input>
</a-form-item>
<a-form-item name="jobstore" label="任务存储器" :rules="[{ required: true, message: '请选择任务存储' }]">
<a-select v-model:value="createState.jobstore" placeholder="请选择任务存储" allowClear :options="jobGroupOptions" />
</a-form-item>
<a-form-item name="executor" label="任务执行器" :rules="[{ required: true, message: '请选择任务执行器' }]">
<a-select v-model:value="createState.executor" placeholder="请选择任务执行器" allowClear :options="jobExecutorOptions" />
</a-form-item>
<a-form-item name="func" label="任务函数" :rules="[{ required: true, message: '请输入任务函数 module.function' }]">
<a-input v-model:value="createState.func" placeholder="请输入任务函数 module.function" allowClear></a-input>
</a-form-item>
<a-form-item name="args" label="位置参数" :rules="[{ required: false, message: '请输入位置参数' }]">
<a-input v-model:value="createState.args" placeholder="请输入位置参数" allowClear></a-input>
</a-form-item>
<a-form-item name="kwargs" label="关键字参数" :rules="[{ required: false, message: '请输入关键字参数' }]">
<a-input v-model:value="createState.kwargs" placeholder="请输入关键字参数" allowClear></a-input>
</a-form-item>
<a-form-item name="trigger" label="任务触发器" :rules="[{ required: true, message: '请选择触发器' }]">
<a-select v-model:value="createState.trigger" placeholder="请选择触发器" allowClear :options="triggerOptions" />
</a-form-item>
<a-form-item v-if="createState.trigger === 'date'" name="trigger_args" label="运行日期" :rules="[{ required: true, message: '请选择运行日期' }]">
<a-date-picker v-model:value="createState.trigger_args" format="YYYY-MM-DD HH:mm:ss" placeholder="请选择运行日期" :show-time="{ defaultValue: dayjs('00:00:00', 'HH:mm:ss') }" style="width: 100%" :value-format="'YYYY-MM-DD HH:mm:ss'"/>
</a-form-item>
<a-form-item v-else-if="createState.trigger === 'interval'" name="trigger_args" label="间隔时间" :rules="[{ required: true, message: '请输入间隔时间' }]">
<a-input v-model:value="createState.trigger_args" placeholder="请输入 秒-分-时-天-周 (* * * * 1) " allowClear></a-input>
</a-form-item>
<a-form-item v-else-if="createState.trigger === 'cron'" name="trigger_args" label="Cron表达式" :rules="[{ required: true, message: '请输入Cron表达式' }]">
<a-input v-model:value="createState.trigger_args" placeholder="请输入 Cron表达式(*/3 * * * *)" allowClear></a-input>
</a-form-item>
<a-form-item name="coalesce" label="是否并行" :rules="[{ required: true, message: '请选择是否合并运行' }]">
<a-radio-group v-model:value="createState.coalesce">
<a-radio :value="true"></a-radio>
<a-radio :value="false"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="max_instances" label="最大实例数" :rules="[{ required: true, message: '请输入最大实例数' }]">
<a-input-number v-model:value="createState.max_instances" :min="1" placeholder="请输入最大实例数" style="width: 100%" />
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="createState.description" placeholder="请输入备注" :rows="4"
allowClear />
</a-form-item>
</a-form>
</div>
<div v-else>
<a-form ref="updateForm" :model="updateState"
v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="任务名称" :rules="[{ required: true, message: '请输入任务名称' }]">
<a-input v-model:value="updateState.name" placeholder="请输入任务名称" allowClear></a-input>
</a-form-item>
<a-form-item name="jobstore" label="任务存储器" :rules="[{ required: true, message: '请选择任务存储' }]">
<a-select v-model:value="updateState.jobstore" placeholder="请选择任务存储" allowClear :options="jobGroupOptions" />
</a-form-item>
<a-form-item name="executor" label="任务执行器" :rules="[{ required: true, message: '请选择任务执行器' }]">
<a-select v-model:value="updateState.executor" placeholder="请选择任务执行器" allowClear :options="jobExecutorOptions" />
</a-form-item>
<a-form-item name="func" label="任务函数" :rules="[{ required: true, message: '请输入任务函数' }]">
<a-input v-model:value="updateState.func" placeholder="请输入任务函数" allowClear></a-input>
</a-form-item>
<a-form-item name="args" label="位置参数" :rules="[{ required: false, message: '请输入位置参数' }]">
<a-input v-model:value="updateState.args" placeholder="请输入位置参数" allowClear></a-input>
</a-form-item>
<a-form-item name="kwargs" label="关键字参数" :rules="[{ required: false, message: '请输入关键字参数' }]">
<a-input v-model:value="updateState.kwargs" placeholder="请输入关键字参数" allowClear></a-input>
</a-form-item>
<a-form-item name="trigger" label="任务触发器" :rules="[{ required: true, message: '请选择触发器' }]">
<a-select v-model:value="updateState.trigger" placeholder="请选择触发器" allowClear :options="triggerOptions" />
</a-form-item>
<a-form-item v-if="updateState.trigger === 'date'" name="trigger_args" label="运行日期" :rules="[{ required: true, message: '请选择运行日期' }]">
<a-date-picker v-model:value="updateState.trigger_args" format="YYYY-MM-DD HH:mm:ss" placeholder="请选择运行日期" :show-time="{ defaultValue: dayjs('00:00:00', 'HH:mm:ss') }" style="width: 100%" :value-format="'YYYY-MM-DD HH:mm:ss'"/>
</a-form-item>
<a-form-item v-else-if="updateState.trigger === 'interval'" name="trigger_args" label="间隔时间" :rules="[{ required: true, message: '请输入间隔时间' }]">
<a-input v-model:value="updateState.trigger_args" placeholder="请输入 秒-分-时-天-周 (* * * * 1) " allowClear></a-input>
</a-form-item>
<a-form-item v-else-if="updateState.trigger === 'cron'" name="trigger_args" label="Cron表达式" :rules="[{ required: true, message: '请输入Cron表达式' }]">
<a-input v-model:value="updateState.trigger_args" placeholder="请输入 Cron表达式(*/3 * * * *)" allowClear></a-input>
</a-form-item>
<a-form-item name="coalesce" label="是否并行" :rules="[{ required: true, message: '请选择是否合并运行' }]">
<a-radio-group v-model:value="updateState.coalesce">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="max_instances" label="最大实例数" :rules="[{ required: true, message: '请输入最大实例数' }]">
<a-input-number v-model:value="updateState.max_instances" :min="1" placeholder="请输入最大实例数" style="width: 100%" />
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="updateState.description" placeholder="请输入备注" :rows="4"
allowClear />
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref, onMounted, h, watch } from 'vue';
import dayjs, { Dayjs } from 'dayjs';
import { Table, message, Modal } from 'ant-design-vue';
import type { TableColumnsType, MenuProps } from 'ant-design-vue';
import { PlusOutlined, DownOutlined, DeleteOutlined, CheckOutlined, StopOutlined } from '@ant-design/icons-vue';
import { cloneDeep, isEmpty } from '@/utils/util';
import PageHeader from '@/components/PageHeader.vue';
import { getJobList, getJobDetail, createJob,updateJob,deleteJob,exportJob, clearJob, OptionJob} from '@/api/system/job'
import type { searchType, tableJobType } from './types'
const createForm = ref();
const updateForm = ref();
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const dataSource = ref<tableJobType[]>([]);
const selectedRowKeys = ref<tableJobType['id'][]>([]);
const queryState = reactive<searchType>({});
const pagination = reactive({
current: 1,
pageSize: 10,
defaultPageSize: 10,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `${range[0]}-${range[1]} 条 / 总共 ${total}`
})
const jobGroupOptions = ref([
{ value: 'default', label: '默认(Memory)' },
{ value: 'redis', label: 'Redis' },
])
const jobExecutorOptions = ref([
{ value: 'default', label: '默认执行器' },
{ value: 'processpool', label: '进程池' }
])
const triggerOptions = ref([
{ value: 'date', label: '指定日期(date)' },
{ value: 'interval', label: '间隔触发器(interval)' },
{ value: 'cron', label: 'cron表达式' },
])
const createState = reactive<tableJobType>({
name:'',
func:'',
trigger:null,
args:'',
kwargs:'',
coalesce: false,
max_instances:1,
jobstore:'default',
executor:'default',
trigger_args: null,
status: null,
message:'',
description:''
})
const updateState = reactive<tableJobType>({
id: undefined,
name:'',
func:'',
trigger:null,
args:'',
kwargs:'',
coalesce:false,
max_instances:1,
jobstore:'default',
executor:'default',
trigger_args: null,
status:null,
message:'',
description:''
})
const detailState = ref<tableJobType>({})
const columns: TableColumnsType = [
{
title: '序号',
dataIndex: 'index',
align: 'center',
width: 80
},
{
title: '任务名称',
dataIndex: 'name',
ellipsis: true
},
{
title: '执行函数',
dataIndex: 'func',
ellipsis: true
},
{
title: '触发器',
dataIndex: 'trigger',
ellipsis: true,
width: 100
},
{
title: '存储器',
dataIndex: 'jobstore',
ellipsis: true,
width: 100
},
{
title: '执行器',
dataIndex: 'executor',
ellipsis: true,
width: 100
},
{
title: '并发执行',
dataIndex: 'coalesce',
align: 'center',
width: 120,
},
{
title: '状态',
dataIndex: 'status',
align: 'center',
width: 100
},
{
title: '创建时间',
dataIndex: 'created_at',
ellipsis: true,
width: 180
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
fixed: 'right',
width: 200
}
];
// 表格选中配置
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableJobType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.name) {
params['name'] = queryState.name
}
if (queryState.date_range) {
params['start_time'] = `${queryState.date_range[0]} 00:00:00`;
params['end_time'] = `${queryState.date_range[1]} 23:59:59`;
}
if (queryState.status) {
params['status'] = queryState.status == true ? true : false;
}
params['page_no'] = pagination.current
params['page_size'] = pagination.pageSize
getJobList(params).then(response => {
const result = response.data;
dataSource.value = result.data.items;
pagination.total = result.data.total;
pagination.current = result.data.page_no;
pagination.pageSize = result.data.page_size;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 生命周期钩子
onMounted(() => loadingData());
// 查询
const onFinish = () => {
pagination.current = 1;
loadingData();
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
pagination.current = 1;
queryState.status = null
queryState.date_range = null
loadingData();
}
// 表格分页处理
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
}
const modalHandle = (modalType: string, index?: number) => {
modalTitle.value = modalType;
openModal.value = true;
if (modalType === 'view' && index !== undefined) {
detailStateLoading.value = true;
detailState.value = dataSource.value[index];
detailState.value.index = index;
detailStateLoading.value = false;
} else if (modalType === 'update' && index !== undefined) {
const selected = dataSource.value[index];
if (selected.trigger === 'date' && selected.trigger_args) {
selected.trigger_args = dayjs(selected.trigger_args).format('YYYY-MM-DD HH:mm:ss');
}
Object.keys(updateState).forEach(key => {
updateState[key] = selected[key];
})
}
}
// 删除
const deleteRow = (row: tableJobType) => {
deleteJob({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error)
})
}
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = cloneDeep(createState);
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
})
createJob(createBody).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key]);
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
updateJob(updateState).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
message.success(response.data.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}
}
// 导出按钮操作
const handleExport = () => {
Modal.confirm({
title: '警告',
content: '是否确认导出所有定时任务数据?',
onOk() {
const body = {
...queryState,
page_no: 1,
page_size: pagination.total
};
message.loading('正在导出数据,请稍候...', 0);
return exportJob(body).then(response => {
const blob = new Blob([response.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
// 从响应头获取文件名
const contentDisposition = response.headers['content-disposition'];
let fileName = '定时任务.xlsx';
if (contentDisposition) {
const fileNameMatch = contentDisposition.match(/filename=(.*?)(;|$)/);
if (fileNameMatch) {
fileName = decodeURIComponent(fileNameMatch[1]);
}
}
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
message.destroy();
message.success('导出成功');
}).catch((error) => {
message.destroy();
console.error('导出错误:', error);
message.error('文件处理失败');
});
},
onCancel() {
message.info('已取消导出');
}
});
};
// 清空按钮操作
const handleClear = () => {
Modal.confirm({
title: '警告',
content: '是否确认清空所有定时任务数据?',
onOk() {
clearJob().then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
})
}
})
}
// 操作按钮:操作类型 1: 暂停 2: 恢复 3: 重启
const handleOption = (row: tableJobType, option: number) => {
OptionJob({id: row.id, option: option}).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
})
}
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
</style>
+33
View File
@@ -0,0 +1,33 @@
export interface searchType {
name?: string
status?: boolean
date_range?: [string, string];
}
export interface tableJobType {
id?: number;
index?: number;
name?: string;
func?: string;
trigger?: string;
args?: string;
kwargs?: string;
coalesce?: boolean;
max_instances?: number;
jobstore?: string;
executor?: string;
trigger_args?: string;
status?: boolean;
message?: string;
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
interface creatorType {
id?: number;
name?: string;
username?: string;
}
+5 -5
View File
@@ -203,7 +203,7 @@
</a-popover>
</a-form-item>
<a-form-item name="order" label="排序">
<a-input-number v-model:value="createState.order" :min="1" />
<a-input-number v-model:value="createState.order" :min="1" style="width: 100%"/>
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="createState.available">
@@ -303,7 +303,7 @@
</a-popover>
</a-form-item>
<a-form-item name="order" label="排序">
<a-input-number v-model:value="updateState.order" :min="1" />
<a-input-number v-model:value="updateState.order" :min="1" style="width: 100%"/>
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="updateState.available">
@@ -325,7 +325,7 @@
<a-input v-model:value="updateState.route_path" placeholder="请输入路由路径" allowClear></a-input>
</a-form-item>
<a-form-item v-if="updateState.type === 1" name="redirect" label="重定向"
:rules="[{ required: updateState.type === 1 ? true : false, message: '请输入重定向' }]">
:rules="[{ required: false, message: '请输入重定向' }]">
<a-input v-model:value="updateState.redirect" placeholder="请输入重定向" allowClear></a-input>
</a-form-item>
<a-form-item v-if="updateState.type === 2" name="component_path" label="组件地址"
@@ -390,7 +390,7 @@ const createState = reactive<tableDataType>({
route_name: '',
route_path: '',
component_path: '',
redirect: '',
redirect: null,
parent_id: undefined,
cache: true,
hidden: false,
@@ -407,7 +407,7 @@ const updateState = reactive<tableDataType>({
route_name: '',
route_path: '',
component_path: '',
redirect: '',
redirect: null,
parent_id: undefined,
cache: true,
hidden: false,
+2 -2
View File
@@ -134,7 +134,7 @@
<a-input v-model:value="createState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
<a-form-item name="order" label="排序" :rules="[{ required: true, message: '请输入排序' }]">
<a-input-number v-model:value="createState.order" :min="1" />
<a-input-number v-model:value="createState.order" :min="1" style="width: 100%"/>
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="createState.available">
@@ -153,7 +153,7 @@
<a-input v-model:value="updateState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
<a-form-item name="order" label="排序" :rules="[{ required: true, message: '请输入排序' }]">
<a-input-number v-model:value="updateState.order" :min="1" />
<a-input-number v-model:value="updateState.order" :min="1" style="width: 100%"/>
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="updateState.available">
+2 -2
View File
@@ -143,7 +143,7 @@
<a-input v-model:value="createState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
<a-form-item name="order" label="排序" :rules="[{ required: true, message: '请输入排序' }]">
<a-input-number v-model:value="createState.order" :min="1" />
<a-input-number v-model:value="createState.order" :min="1" style="width: 100%"/>
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="createState.available">
@@ -162,7 +162,7 @@
<a-input v-model:value="updateState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
<a-form-item name="order" label="排序" :rules="[{ required: true, message: '请输入排序' }]">
<a-input-number v-model:value="updateState.order" :min="1" />
<a-input-number v-model:value="updateState.order" :min="1" style="width: 100%"/>
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="updateState.available">