日志管理+任务管理
This commit is contained in:
11
.prettierrc.json
Normal file
11
.prettierrc.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
"typecheck": "vue-tsc --noEmit --skipLibCheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"@better-scroll/core": "2.5.1",
|
||||
"@iconify/vue": "4.1.2",
|
||||
"@sa/axios": "workspace:*",
|
||||
|
||||
@@ -59,17 +59,16 @@ export type TableConfig<A extends ApiFn, T, C> = {
|
||||
immediate?: boolean;
|
||||
};
|
||||
|
||||
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
|
||||
export default function useHookTable(config: any) {
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
const { bool: empty, setBool: setEmpty } = useBoolean();
|
||||
|
||||
const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
|
||||
|
||||
const searchParams: NonNullable<Parameters<A>[0]> = reactive({ ...apiParams });
|
||||
const searchParams: any = reactive({ ...apiParams });
|
||||
const allColumns = ref(config.columns()) as Ref<any>;
|
||||
|
||||
const allColumns = ref(config.columns()) as Ref<C[]>;
|
||||
|
||||
const data: Ref<T[]> = ref([]);
|
||||
const data: Ref<any> = ref([]);
|
||||
|
||||
const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
|
||||
|
||||
@@ -82,7 +81,7 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
|
||||
|
||||
const defaultChecks = getColumnChecks(allColumns.value);
|
||||
|
||||
columnChecks.value = defaultChecks.map(col => ({
|
||||
columnChecks.value = defaultChecks.map((col:any) => ({
|
||||
...col,
|
||||
checked: checkMap.get(col.key) ?? col.checked
|
||||
}));
|
||||
@@ -92,10 +91,9 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
|
||||
startLoading();
|
||||
|
||||
const formattedParams = formatSearchParams(searchParams);
|
||||
|
||||
const response = await apiFn(formattedParams);
|
||||
|
||||
const transformed = transformer(response as Awaited<ReturnType<A>>);
|
||||
const transformed = transformer(response as Awaited<any>);
|
||||
|
||||
data.value = transformed.rows;
|
||||
|
||||
@@ -123,7 +121,7 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
|
||||
*
|
||||
* @param params
|
||||
*/
|
||||
function updateSearchParams(params: Partial<Parameters<A>[0]>) {
|
||||
function updateSearchParams(params: any) {
|
||||
Object.assign(searchParams, params);
|
||||
}
|
||||
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@ant-design/icons-vue':
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1(vue@3.4.27(typescript@5.4.5))
|
||||
'@better-scroll/core':
|
||||
specifier: 2.5.1
|
||||
version: 2.5.1
|
||||
|
||||
@@ -10,6 +10,8 @@ interface Props {
|
||||
loading?: boolean;
|
||||
tableType?: string;
|
||||
showDelete?: boolean;
|
||||
showExport?: boolean;
|
||||
notShowAdd?: boolean;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
@@ -18,6 +20,7 @@ interface Emits {
|
||||
(e: 'add'): void;
|
||||
(e: 'delete'): void;
|
||||
(e: 'refresh'): void;
|
||||
(e: 'export'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
@@ -37,6 +40,10 @@ function batchDelete() {
|
||||
function refresh() {
|
||||
emit('refresh');
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
emit('export');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -49,12 +56,8 @@ function refresh() {
|
||||
<span>{{ $t('common.add') }}</span>
|
||||
</div>
|
||||
</AButton>
|
||||
<APopconfirm
|
||||
v-if="isShowBtn(`system:${tableType}:remove`) && showDelete"
|
||||
:title="$t('common.confirmDelete')"
|
||||
:disabled="disabledDelete"
|
||||
@confirm="batchDelete"
|
||||
>
|
||||
<APopconfirm v-if="isShowBtn(`system:${tableType}:remove`) && showDelete" :title="$t('common.confirmDelete')"
|
||||
:disabled="disabledDelete" @confirm="batchDelete">
|
||||
<AButton size="small" danger :disabled="disabledDelete">
|
||||
<div class="flex-y-center gap-8px">
|
||||
<icon-ic-round-delete class="text-icon" />
|
||||
@@ -69,6 +72,12 @@ function refresh() {
|
||||
<span>{{ $t('common.refresh') }}</span>
|
||||
</div>
|
||||
</AButton>
|
||||
<AButton size="small" type="primary" @click="handleExport" v-show="showExport">
|
||||
<div class="flex-y-center gap-8px">
|
||||
<icon-mdi-refresh class="text-icon" :class="{ 'animate-spin': loading }" />
|
||||
<span>{{ $t('common.export') }}</span>
|
||||
</div>
|
||||
</AButton>
|
||||
<TableColumnSetting v-model:columns="columns" />
|
||||
<slot name="suffix"></slot>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { transformRecordToOption } from '@/utils/common';
|
||||
|
||||
export const enableStatusRecord: Record<Api.Common.EnableStatus, App.I18n.I18nKey> = {
|
||||
export const enableStatusRecord: any= {
|
||||
'0': 'page.manage.common.status.enable',
|
||||
'1': 'page.manage.common.status.disable'
|
||||
};
|
||||
|
||||
export const enableStatusOptions = transformRecordToOption(enableStatusRecord);
|
||||
|
||||
export const menuIconTypeRecord: Record<Api.SystemManage.IconType, App.I18n.I18nKey> = {
|
||||
export const menuIconTypeRecord: any = {
|
||||
'1': 'page.manage.menu.iconType.iconify',
|
||||
'2': 'page.manage.menu.iconType.local'
|
||||
};
|
||||
|
||||
@@ -6,10 +6,8 @@ import { useAppStore } from '@/store/modules/app';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
type TableData<T = object> = AntDesign.TableData<T>;
|
||||
type GetTableData<A extends AntDesign.TableApiFn> = AntDesign.GetTableData<A>;
|
||||
type TableColumn<T> = AntDesign.TableColumn<T>;
|
||||
|
||||
export function useTable<A extends AntDesign.TableApiFn>(config: AntDesign.AntDesignTableConfig<A>) {
|
||||
export function useTable(config: any) {
|
||||
const scope = effectScope();
|
||||
const appStore = useAppStore();
|
||||
|
||||
@@ -26,21 +24,21 @@ export function useTable<A extends AntDesign.TableApiFn>(config: AntDesign.AntDe
|
||||
searchParams,
|
||||
updateSearchParams,
|
||||
resetSearchParams
|
||||
} = useHookTable<A, GetTableData<A>, TableColumn<AntDesign.TableDataWithIndex<GetTableData<A>>>>({
|
||||
} = useHookTable({
|
||||
apiFn,
|
||||
apiParams,
|
||||
columns: config.columns,
|
||||
transformer: res => {
|
||||
transformer: (res:any) => {
|
||||
const { rows = [], total = 0 } = res.data || {};
|
||||
return {
|
||||
rows: rows.map((row, index) => ({ ...row, id: rowKey ? row[rowKey] : index })),
|
||||
rows: rows.map((row:any, index:any) => ({ ...row, id: rowKey ? row[rowKey] : index })),
|
||||
total
|
||||
};
|
||||
},
|
||||
getColumnChecks: cols => {
|
||||
getColumnChecks: (cols:any) => {
|
||||
const checks: AntDesign.TableColumnCheck[] = [];
|
||||
|
||||
cols.forEach(column => {
|
||||
cols.forEach((column:any) => {
|
||||
if (column.key) {
|
||||
checks.push({
|
||||
key: column.key as string,
|
||||
@@ -52,22 +50,22 @@ export function useTable<A extends AntDesign.TableApiFn>(config: AntDesign.AntDe
|
||||
|
||||
return checks;
|
||||
},
|
||||
getColumns: (cols, checks) => {
|
||||
const columnMap = new Map<string, TableColumn<GetTableData<A>>>();
|
||||
getColumns: (cols:any, checks:any) => {
|
||||
const columnMap = new Map<string, any>();
|
||||
|
||||
cols.forEach(column => {
|
||||
cols.forEach((column:any) => {
|
||||
if (column.key) {
|
||||
columnMap.set(column.key as string, column);
|
||||
}
|
||||
});
|
||||
|
||||
const filteredColumns = checks
|
||||
.filter(item => item.checked)
|
||||
.map(check => columnMap.get(check.key) as TableColumn<GetTableData<A>>);
|
||||
.filter((item:any) => item.checked)
|
||||
.map((check:any) => columnMap.get(check.key) );
|
||||
|
||||
return filteredColumns;
|
||||
},
|
||||
onFetched: async transformed => {
|
||||
onFetched: async (transformed:any) => {
|
||||
const { total } = transformed;
|
||||
|
||||
updatePagination({
|
||||
@@ -150,7 +148,7 @@ export function useTableOperate<T extends TableData<{ [key: string]: any }>>(
|
||||
) {
|
||||
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
|
||||
|
||||
const operateType = ref<AntDesign.TableOperateType>('add');
|
||||
const operateType = ref<any>('add');
|
||||
const { getData, idKey = 'id' } = options;
|
||||
/** the editing row data */
|
||||
const editingData: Ref<T | null> = ref(null);
|
||||
|
||||
@@ -49,7 +49,12 @@ const local: any = {
|
||||
yesOrNo: {
|
||||
yes: 'Yes',
|
||||
no: 'No'
|
||||
}
|
||||
},
|
||||
ms: 'ms',
|
||||
normal: 'Nomal',
|
||||
abnormal: 'Abnormal',
|
||||
export: 'Export',
|
||||
loading: 'Loading',
|
||||
},
|
||||
request: {
|
||||
logout: 'Logout user after request failed',
|
||||
@@ -483,6 +488,36 @@ const local: any = {
|
||||
},
|
||||
addDict: 'Add Dictionary',
|
||||
editDict: 'Edit Dictionary'
|
||||
},
|
||||
log:{
|
||||
logId:'Log ID',
|
||||
module:'System Module',
|
||||
operType:'Operation Type',
|
||||
operName:'Operator',
|
||||
operIp:'Operation Address',
|
||||
operArea:'Operation Location',
|
||||
operStatus:'Status',
|
||||
operTime:'Operation Date',
|
||||
useTime:'Time Consumption',
|
||||
backUser:'Back User',
|
||||
phoneUser:'Phone User',
|
||||
other:'Other',
|
||||
},
|
||||
task:{
|
||||
taskId:'ID',
|
||||
taskName:'Name',
|
||||
group:'Group',
|
||||
invoke:'Invoke',
|
||||
cron:'Cron',
|
||||
status:'Status',
|
||||
log:'Log',
|
||||
createTime:'Create Time',
|
||||
targetParams:'Arguments',
|
||||
remark:'Remark',
|
||||
viewJob:'View Job',
|
||||
addJob:'Add Job',
|
||||
editJob:'Edit Job',
|
||||
getInfoError:'Get Info Error',
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -49,7 +49,12 @@ const local:any = {
|
||||
yesOrNo: {
|
||||
yes: '是',
|
||||
no: '否'
|
||||
}
|
||||
},
|
||||
ms: '毫秒',
|
||||
normal: '正常',
|
||||
abnormal: '异常',
|
||||
export:'导出',
|
||||
loading: '加载中...',
|
||||
},
|
||||
request: {
|
||||
logout: '请求失败后登出用户',
|
||||
@@ -483,6 +488,37 @@ const local:any = {
|
||||
},
|
||||
addDict: '新增字典',
|
||||
editDict: '编辑字典'
|
||||
},
|
||||
log:{
|
||||
logId:'日志编号',
|
||||
module:'系统模块',
|
||||
operType:'操作类型',
|
||||
operName:'操作人员',
|
||||
operIp:'操作地址',
|
||||
operArea:'操作地点',
|
||||
operStatus:'操作状态',
|
||||
operTime:'操作日期',
|
||||
useTime:'消耗时间',
|
||||
backUser:'后台用户',
|
||||
phoneUser:'手机用户',
|
||||
other:'其他',
|
||||
},
|
||||
task:{
|
||||
taskId:'ID',
|
||||
taskName:'任务名称',
|
||||
group:'任务组名',
|
||||
invoke:'调用目标',
|
||||
cron:'cron表达式',
|
||||
status:'状态',
|
||||
log:'记录日志',
|
||||
createTime:'创建时间',
|
||||
targetParams:'传入参数',
|
||||
remark:'备注',
|
||||
viewJob:'查看任务',
|
||||
addJob:'新增任务',
|
||||
editJob:'编辑任务',
|
||||
viewInfoErr:'查看异常信息',
|
||||
getInfoError:'获取信息失败',
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
18
src/service/api/job.ts
Normal file
18
src/service/api/job.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { request } from '../request';
|
||||
|
||||
|
||||
export function doGetjobList(params?: Api.SystemManage.UserSearchParams) {
|
||||
return request<Api.SystemManage.UserList>({
|
||||
url: '/schedule/job/list',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function doGetjobInfo(paramsId: any) {
|
||||
return request<any>({
|
||||
url: `/schedule/job/${paramsId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
15
src/service/api/log.ts
Normal file
15
src/service/api/log.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { request } from '../request';
|
||||
|
||||
|
||||
export function doGetlogList(params?: Api.SystemManage.UserSearchParams) {
|
||||
return request<Api.SystemManage.UserList>({
|
||||
url: '/system/operlog/list',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
export function doDeleteLog(userId: any) {
|
||||
console.log(userId,+userId.join(','));
|
||||
return request({ url: `/system/operlog/${userId.join(',')}`, method: 'delete' });
|
||||
}
|
||||
4
src/typings/auto-imports.d.ts
vendored
4
src/typings/auto-imports.d.ts
vendored
@@ -56,6 +56,7 @@ declare global {
|
||||
const doCheckUserRepeat: typeof import('../service/api/auth')['doCheckUserRepeat']
|
||||
const doDeleteDept: typeof import('../service/api/dept')['doDeleteDept']
|
||||
const doDeleteDict: typeof import('../service/api/dict')['doDeleteDict']
|
||||
const doDeleteLog: typeof import('../service/api/log')['doDeleteLog']
|
||||
const doDeleteLogout: typeof import('../service/api/auth')['doDeleteLogout']
|
||||
const doDeleteMenu: typeof import('../service/api/menu')['doDeleteMenu']
|
||||
const doDeletePost: typeof import('../service/api/post')['doDeletePost']
|
||||
@@ -81,6 +82,9 @@ declare global {
|
||||
const doGetUserList: typeof import('../service/api/user')['doGetUserList']
|
||||
const doGetUserPostsAndRoles: typeof import('../service/api/user')['doGetUserPostsAndRoles']
|
||||
const doGetUserRoutes: typeof import('../service/api/route')['doGetUserRoutes']
|
||||
const doGetjobInfo: typeof import('../service/api/job')['doGetjobInfo']
|
||||
const doGetjobList: typeof import('../service/api/job')['doGetjobList']
|
||||
const doGetlogList: typeof import('../service/api/log')['doGetlogList']
|
||||
const doPostRole: typeof import('../service/api/role')['doPostRole']
|
||||
const doPostUser: typeof import('../service/api/user')['doPostUser']
|
||||
const doPutRole: typeof import('../service/api/role')['doPutRole']
|
||||
|
||||
2
src/typings/components.d.ts
vendored
2
src/typings/components.d.ts
vendored
@@ -38,6 +38,7 @@ declare module 'vue' {
|
||||
AppProvider: typeof import('./../components/common/app-provider.vue')['default']
|
||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
|
||||
ARow: typeof import('ant-design-vue/es')['Row']
|
||||
ASegmented: typeof import('ant-design-vue/es')['Segmented']
|
||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||
@@ -62,6 +63,7 @@ declare module 'vue' {
|
||||
FullScreen: typeof import('./../components/common/full-screen.vue')['default']
|
||||
IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default']
|
||||
IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default']
|
||||
IconFont: typeof import('./../components/IconFont/index.vue')['default']
|
||||
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
|
||||
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
|
||||
IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default']
|
||||
|
||||
213
src/views/manage/log/index.vue
Normal file
213
src/views/manage/log/index.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<script setup lang="tsx">
|
||||
import { Button, Popconfirm, Tag } from 'ant-design-vue';
|
||||
import type { Key } from 'ant-design-vue/es/_util/type';
|
||||
import { useTable, useTableOperate } from '@/hooks/common/table';
|
||||
import { SimpleScrollbar } from '~/packages/materials/src';
|
||||
import logSearch from './modules/log-search.vue';
|
||||
import { useI18n } from "vue-i18n";
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
const wrapperEl = shallowRef<HTMLElement | null>(null);
|
||||
const { height: wrapperElHeight } = useElementSize(wrapperEl);
|
||||
|
||||
const scrollConfig = computed(() => {
|
||||
return {
|
||||
y: wrapperElHeight.value - 72,
|
||||
x: 1000
|
||||
};
|
||||
});
|
||||
|
||||
const { columns, columnChecks, data, loading, getData, mobilePagination, searchParams, resetSearchParams } = useTable({
|
||||
apiFn: doGetlogList,
|
||||
apiParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
status: undefined,
|
||||
},
|
||||
rowKey: 'operId',
|
||||
columns: () => [
|
||||
{
|
||||
key: 'operId',
|
||||
dataIndex: 'operId',
|
||||
title: t('page.manage.log.logId'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'title',
|
||||
dataIndex: 'title',
|
||||
title: t('page.manage.log.module'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'operatorType',
|
||||
dataIndex: 'operatorType',
|
||||
title: t('page.manage.log.operType'),
|
||||
align: 'center',
|
||||
customRender: ({ record }: any) => {
|
||||
if (record.operatorType === null) {
|
||||
return null;
|
||||
}
|
||||
const tagMap: any = {
|
||||
0: t('page.manage.log.other'),
|
||||
1: t('page.manage.log.backUser'),
|
||||
2: t('page.manage.log.phoneUser'),
|
||||
};
|
||||
const tagColor: any = {
|
||||
'0': 'pink',
|
||||
'1': 'warning',
|
||||
'2': 'blue',
|
||||
};
|
||||
return <Tag color={tagColor[record.operatorType]}> {tagMap[record.operatorType]} </Tag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'operName',
|
||||
dataIndex: 'operName',
|
||||
title: t('page.manage.log.operName'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'operIp',
|
||||
dataIndex: 'operIp',
|
||||
title: t('page.manage.log.operIp'),
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
dataIndex: 'status',
|
||||
align: 'center',
|
||||
customRender: ({ record }: any) => {
|
||||
if (record.status === null) {
|
||||
return null;
|
||||
}
|
||||
const tagMap: any = {
|
||||
0: t('common.normal'),
|
||||
1: t('common.abnormal'),
|
||||
};
|
||||
const tagColor: any = {
|
||||
'0': 'success',
|
||||
'1': 'error'
|
||||
};
|
||||
return <Tag color={tagColor[record.status]}> {tagMap[record.status]} </Tag>;
|
||||
},
|
||||
title: t('page.manage.log.operStatus'),
|
||||
},
|
||||
{
|
||||
key: 'operTime',
|
||||
dataIndex: 'operTime',
|
||||
align: 'center',
|
||||
title: t('page.manage.log.operTime'),
|
||||
},
|
||||
{
|
||||
key: 'costTime',
|
||||
dataIndex: 'costTime',
|
||||
align: 'center',
|
||||
title: t('page.manage.log.useTime'),
|
||||
customRender: ({ record }: any) => {
|
||||
if (!record.costTime) {
|
||||
return '0ms';
|
||||
}
|
||||
|
||||
return `${record.costTime} ${t('common.ms')}`;
|
||||
},
|
||||
|
||||
},
|
||||
// {
|
||||
// key: 'operate',
|
||||
// title: t('common.operate'),
|
||||
// align: 'center',
|
||||
// width: 200,
|
||||
// customRender: ({ record }: any) =>
|
||||
// !record.admin && (
|
||||
// <div class="flex justify-around gap-8px">
|
||||
// {isShowBtn('system:user:remove') && (
|
||||
// <Popconfirm onConfirm={() => handleDelete(record.operId)} title={t('common.confirmDelete')} >
|
||||
// <Button danger size="small" >
|
||||
// {t('common.delete')}
|
||||
// </Button>
|
||||
// </Popconfirm>
|
||||
// )}
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
]
|
||||
});
|
||||
|
||||
const {
|
||||
handleAdd,
|
||||
checkedRowKeys,
|
||||
onBatchDeleted,
|
||||
onDeleted
|
||||
// closeDrawer
|
||||
} = useTableOperate(data, { getData, idKey: 'operId' });
|
||||
const deptTreeData = ref<Api.Common.CommonTree>([]);
|
||||
|
||||
onMounted(() => {
|
||||
getUserDeptTree();
|
||||
});
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const { error } = await doDeleteLog(checkedRowKeys.value);
|
||||
if (!error) {
|
||||
onBatchDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
const { error } = await doDeleteLog([id]);
|
||||
if (!error) {
|
||||
onDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleUserSelectChange(selectedRowKeys: Key[], selectedRows: any[]) {
|
||||
checkedRowKeys.value = selectedRowKeys as number[];
|
||||
}
|
||||
|
||||
function fnTest() {
|
||||
searchParams.value = {
|
||||
|
||||
};
|
||||
|
||||
resetSearchParams();
|
||||
// 使用 nextTick 确保视图更新后检查
|
||||
nextTick(() => {
|
||||
console.log('视图更新后:', { ...searchParams.value });
|
||||
console.log(searchParams.value);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
async function getUserDeptTree() {
|
||||
const { error, data: tree } = await doGetUserDeptTree();
|
||||
if (!error) {
|
||||
deptTreeData.value = tree;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SimpleScrollbar>
|
||||
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||
<logSearch v-model:model="searchParams" :dept-tree-data="deptTreeData" @reset="fnTest" @search="getData" />
|
||||
<ACard :title="t('page.manage.user.title')" :bordered="false" :body-style="{ flex: 1, overflow: 'hidden' }"
|
||||
class="flex-col-stretch sm:flex-1-hidden card-wrapper">
|
||||
<template #extra>
|
||||
<TableHeaderOperation v-model:columns="columnChecks" :disabled-delete="checkedRowKeys.length === 0"
|
||||
:loading="loading" :show-delete="true" @add="handleAdd" @delete="handleBatchDelete" @refresh="getData"
|
||||
:not-show-add="true" />
|
||||
</template>
|
||||
<ATable ref="wrapperEl" row-key="operId" :columns="columns" :data-source="data" :loading="loading"
|
||||
:row-selection="{
|
||||
selectedRowKeys: checkedRowKeys,
|
||||
onChange: handleUserSelectChange,
|
||||
}" size="small" :pagination="mobilePagination" :scroll="scrollConfig" class="h-full" />
|
||||
|
||||
</ACard>
|
||||
</div>
|
||||
</SimpleScrollbar>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
127
src/views/manage/log/modules/log-search.vue
Normal file
127
src/views/manage/log/modules/log-search.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusOptions } from '@/constants/business';
|
||||
import { SyncOutlined, SearchOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'logSearch'
|
||||
});
|
||||
|
||||
|
||||
|
||||
interface Emits {
|
||||
(e: 'reset'): void;
|
||||
(e: 'search'): void;
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
deptTreeData: Api.Common.CommonTree;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const model = defineModel<any>('model', { required: true });
|
||||
|
||||
/**记录开始结束时间 */
|
||||
let queryRangePicker = ref<[string, string]>(['', '']);
|
||||
|
||||
function reset() {
|
||||
// 重置表单
|
||||
//formRef.value?.resetFields();
|
||||
|
||||
// 重置日期选择器
|
||||
queryRangePicker.value = ['', ''];
|
||||
|
||||
// 确保 model 也被重置
|
||||
model.value = {};
|
||||
|
||||
// 使用 nextTick 确保视图更新后检查
|
||||
nextTick(() => {
|
||||
console.log('视图更新后:', { ...model.value });
|
||||
console.log(model);
|
||||
});
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
function search() {
|
||||
if (!queryRangePicker.value) {
|
||||
queryRangePicker.value = ['', ''];
|
||||
}
|
||||
model.value.beginTime = queryRangePicker.value[0];
|
||||
model.value.endTime = queryRangePicker.value[1];
|
||||
emit('search');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard :title="$t('common.search')" :bordered="false" class="card-wrapper">
|
||||
<AForm :model="model" :label-width="80">
|
||||
<ARow :gutter="[16, 16]" wrap>
|
||||
<ACol :lg="6" :md="12" :xs="24">
|
||||
<AFormItem :label="$t('page.manage.log.operIp')" name="operIp" class="m-0">
|
||||
<AInput v-model:value="model.operIp" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
|
||||
<ACol :lg="6" :md="12" :xs="24">
|
||||
<AFormItem :label="$t('page.manage.log.module')" name="title" class="m-0">
|
||||
<AInput v-model:value="model.title" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
|
||||
|
||||
<ACol :lg="6" :md="12" :xs="24">
|
||||
<AFormItem :label="$t('page.manage.user.status')" name="status" class="m-0">
|
||||
<ASelect v-model:value="model.status" :placeholder="$t('page.manage.user.form.status')" allow-clear>
|
||||
<ASelectOption v-for="option in enableStatusOptions" :key="option.value" :value="option.value">
|
||||
{{ $t(option.label) }}
|
||||
</ASelectOption>
|
||||
</ASelect>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
|
||||
<ACol :lg="6" :md="12" :xs="24">
|
||||
<AFormItem :label="$t('page.manage.log.operName')" name="operName" class="m-0">
|
||||
<AInput v-model:value="model.operName" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
|
||||
|
||||
|
||||
|
||||
<a-col :lg="8" :md="12" :xs="24">
|
||||
<a-form-item :label="$t('page.manage.log.useTime')" name="queryRangePicker">
|
||||
<a-range-picker v-model:value="queryRangePicker" allow-clear bordered show-time
|
||||
value-format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss" style="width: 100%"></a-range-picker>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<ACol :lg="16" :md="12" :xs="24">
|
||||
<AFormItem class="m-0">
|
||||
<div class="w-full flex-y-center justify-end gap-8px">
|
||||
|
||||
<a-space :size="8">
|
||||
<a-button @click="reset">
|
||||
<template #icon>
|
||||
<SyncOutlined />
|
||||
</template>
|
||||
{{ $t('common.reset') }}
|
||||
</a-button>
|
||||
<AButton type="primary" ghost @click="search">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
{{ $t('common.search') }}
|
||||
</AButton>
|
||||
</a-space>
|
||||
|
||||
</div>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
|
||||
</ARow>
|
||||
</AForm>
|
||||
</ACard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -6,6 +6,8 @@ import { $t } from '@/locales';
|
||||
import { enableStatusRecord } from '@/constants/business';
|
||||
import RoleOperateDrawer from './modules/role-operate-drawer.vue';
|
||||
import RoleSearch from './modules/role-search.vue';
|
||||
import {useI18n} from "vue-i18n";
|
||||
const { t } = useI18n();
|
||||
|
||||
const wrapperEl = shallowRef<HTMLElement | null>(null);
|
||||
const { height: wrapperElHeight } = useElementSize(wrapperEl);
|
||||
@@ -45,17 +47,17 @@ const { columns, columnChecks, data, loading, getData, mobilePagination, searchP
|
||||
dataIndex: 'status',
|
||||
title: $t('page.manage.role.roleStatus'),
|
||||
align: 'center',
|
||||
customRender: ({ record }) => {
|
||||
customRender: ({ record }:any) => {
|
||||
if (record.status === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tagMap: Record<Api.Common.EnableStatus, string> = {
|
||||
const tagMap: any = {
|
||||
'0': 'success',
|
||||
'1': 'warning'
|
||||
};
|
||||
|
||||
const label = $t(enableStatusRecord[record.status]);
|
||||
const label = t(enableStatusRecord[record.status]);
|
||||
|
||||
return <Tag color={tagMap[record.status]}>{label}</Tag>;
|
||||
}
|
||||
@@ -76,7 +78,7 @@ const { columns, columnChecks, data, loading, getData, mobilePagination, searchP
|
||||
title: $t('common.operate'),
|
||||
align: 'center',
|
||||
width: 200,
|
||||
customRender: ({ record }) =>
|
||||
customRender: ({ record }:any) =>
|
||||
!record.admin && (
|
||||
<div class="flex justify-around gap-8px">
|
||||
{isShowBtn('system:role:edit') && (
|
||||
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
/** the type of operation */
|
||||
operateType: AntDesign.TableOperateType;
|
||||
/** the edit row data */
|
||||
rowData?: Api.SystemManage.Role | null;
|
||||
rowData?: any;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
630
src/views/manage/task/index.vue
Normal file
630
src/views/manage/task/index.vue
Normal file
@@ -0,0 +1,630 @@
|
||||
<script setup lang="tsx">
|
||||
import { message, Tag } from 'ant-design-vue';
|
||||
import type { Key } from 'ant-design-vue/es/_util/type';
|
||||
import { useTable, useTableOperate } from '@/hooks/common/table';
|
||||
import { SimpleScrollbar } from '~/packages/materials/src';
|
||||
import taskOperateDrawer from './modules/task-operate-drawer.vue';
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { SyncOutlined, SearchOutlined, ProfileOutlined, FormOutlined, DeleteOutlined, RocketOutlined, ContainerOutlined } from '@ant-design/icons-vue';
|
||||
import { enableStatusOptions } from '@/constants/business';
|
||||
|
||||
import { useAntdForm, useFormRules } from '@/hooks/common/form';
|
||||
const { defaultRequiredRule, formRules } = useFormRules();
|
||||
|
||||
const rules = {
|
||||
jonName: defaultRequiredRule,
|
||||
invokeTarget: defaultRequiredRule,
|
||||
cronExpression: defaultRequiredRule,
|
||||
};
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
const wrapperEl = shallowRef<HTMLElement | null>(null);
|
||||
const { height: wrapperElHeight } = useElementSize(wrapperEl);
|
||||
|
||||
|
||||
/**对话框对象信息状态 */
|
||||
let modalState: any = reactive({
|
||||
openByView: false,
|
||||
openByEdit: false,
|
||||
title: '任务',
|
||||
from: {
|
||||
jobId: undefined,
|
||||
jobName: '',
|
||||
invokeTarget: '',
|
||||
cronExpression: '',
|
||||
misfirePolicy: '3',
|
||||
concurrent: '0',
|
||||
jobGroup: 'DEFAULT',
|
||||
status: '0',
|
||||
saveLog: '0',
|
||||
targetParams: '',
|
||||
remark: '',
|
||||
},
|
||||
confirmLoading: false,
|
||||
openByCron: false,
|
||||
});
|
||||
|
||||
const scrollConfig = computed(() => {
|
||||
return {
|
||||
y: wrapperElHeight.value - 72,
|
||||
x: 1000
|
||||
};
|
||||
});
|
||||
|
||||
const { columns, columnChecks, data, loading, getData, mobilePagination, searchParams, resetSearchParams } = useTable({
|
||||
apiFn: doGetjobList,
|
||||
apiParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
rowKey: 'jobId',
|
||||
columns: () => [
|
||||
{
|
||||
key: 'jobId',
|
||||
dataIndex: 'jobId',
|
||||
title: t('page.manage.task.taskId'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'jobName',
|
||||
dataIndex: 'jobName',
|
||||
title: t('page.manage.task.taskName'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'jobGroup',
|
||||
dataIndex: 'jobGroup',
|
||||
title: t('page.manage.task.group'),
|
||||
align: 'center',
|
||||
// customRender: ({ record }: any) => {
|
||||
// if (record.operatorType === null) {
|
||||
// return null;
|
||||
// }
|
||||
// const tagMap: any = {
|
||||
// 0: t('page.manage.log.other'),
|
||||
// 1: t('page.manage.log.backUser'),
|
||||
// 2: t('page.manage.log.phoneUser'),
|
||||
// };
|
||||
// const tagColor: any = {
|
||||
// '0': 'pink',
|
||||
// '1': 'warning',
|
||||
// '2': 'blue',
|
||||
// };
|
||||
// return <Tag color={tagColor[record.operatorType]}>{tagMap[record.operatorType]}</Tag>;
|
||||
// }
|
||||
},
|
||||
{
|
||||
key: 'invokeTarget',
|
||||
dataIndex: 'invokeTarget',
|
||||
title: t('page.manage.task.invoke'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'cronExpression',
|
||||
dataIndex: 'cronExpression',
|
||||
title: t('page.manage.task.cron'),
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
dataIndex: 'status',
|
||||
align: 'center',
|
||||
customRender: ({ record }: any) => {
|
||||
if (record.status === null) {
|
||||
return null;
|
||||
}
|
||||
const tagMap: any = {
|
||||
'0': t('common.normal'),
|
||||
'1': t('common.abnormal'),
|
||||
};
|
||||
const tagColor: any = {
|
||||
'0': 'success',
|
||||
'1': 'error'
|
||||
};
|
||||
return <Tag color={tagColor[record.status]}>{tagMap[record.status]}</Tag>;
|
||||
},
|
||||
title: t('page.manage.log.operStatus'),
|
||||
},
|
||||
{
|
||||
key: 'operate',
|
||||
title: t('common.operate'),
|
||||
align: 'center',
|
||||
width: 200,
|
||||
// customRender: ({ record }: any) =>
|
||||
// !record.admin && (
|
||||
// <div class="flex justify-around gap-8px">
|
||||
// <Popconfirm onConfirm={() => handleDelete(record.operId)} title={t('common.confirmDelete')}>
|
||||
// <a-button danger size="small">
|
||||
// <template #icon>
|
||||
// <SyncOutlined />
|
||||
// </template>
|
||||
// </a-button>
|
||||
// </Popconfirm>
|
||||
// </div>
|
||||
// )
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const {
|
||||
drawerVisible,
|
||||
operateType,
|
||||
editingData,
|
||||
handleAdd,
|
||||
checkedRowKeys,
|
||||
onBatchDeleted,
|
||||
onDeleted
|
||||
// closeDrawer
|
||||
} = useTableOperate(data, { getData, idKey: 'jobId' });
|
||||
const deptTreeData = ref<Api.Common.CommonTree>([]);
|
||||
|
||||
onMounted(() => {
|
||||
});
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const { error } = await doDeleteLog(checkedRowKeys.value);
|
||||
if (!error) {
|
||||
onBatchDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
const { error } = await doDeleteLog([id]);
|
||||
if (!error) {
|
||||
onDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleUserSelectChange(selectedRowKeys: Key[], selectedRows: any[]) {
|
||||
checkedRowKeys.value = selectedRowKeys as number[];
|
||||
}
|
||||
|
||||
function fnTest() {
|
||||
searchParams.value = {
|
||||
|
||||
};
|
||||
|
||||
resetSearchParams();
|
||||
// 使用 nextTick 确保视图更新后检查
|
||||
nextTick(() => {
|
||||
console.log(operateType)
|
||||
});
|
||||
}
|
||||
|
||||
async function fnRecordView(jobId: number) {
|
||||
// const { error, data } = await doGetjobInfo(jobId);
|
||||
// // if (!error) {
|
||||
// // onDeleted();
|
||||
// // }
|
||||
// console.log(data);
|
||||
operateType.value = 'view';
|
||||
|
||||
if (modalState.confirmLoading) return;
|
||||
const hide = message.loading('Waiting...', 0);
|
||||
modalState.confirmLoading = true;
|
||||
doGetjobInfo(jobId).then(res => {
|
||||
modalState.confirmLoading = false;
|
||||
hide();
|
||||
console.log(res);
|
||||
if (!res.error) {
|
||||
modalState.from = Object.assign(modalState.from, res.data);
|
||||
modalState.openByView = true;
|
||||
modalState.title = t('page.manage.task.viewJob');
|
||||
console.log(modalState.openByView);
|
||||
}
|
||||
// if (res.code === RESULT_CODE_SUCCESS && res.data) {
|
||||
// modalState.from = Object.assign(modalState.from, res.data);
|
||||
// modalState.title = t('views.manage.job.viewJob');
|
||||
// modalState.openByView = true;
|
||||
// } else {
|
||||
// message.error(t('views.manage.job.viewInfoErr'), 2);
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 对话框弹出关闭执行函数
|
||||
* 进行表达规则校验
|
||||
*/
|
||||
function fnModalCancel() {
|
||||
modalState.openByEdit = false;
|
||||
modalState.openByView = false;
|
||||
// modalStateFrom.resetFields();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 对话框弹出显示为 新增或者修改
|
||||
* @param jobId 任务id, 不传为新增
|
||||
*/
|
||||
function fnModalVisibleByEdit(jobId?: string | number) {
|
||||
if (!jobId) {
|
||||
//modalStateFrom.resetFields();
|
||||
modalState.title = t('page.manage.task.addJob');
|
||||
modalState.openByEdit = true;
|
||||
} else {
|
||||
if (modalState.confirmLoading) return;
|
||||
const hide = message.loading(t('common.loading'), 0);
|
||||
modalState.confirmLoading = true;
|
||||
doGetjobInfo(jobId).then(res => {
|
||||
modalState.confirmLoading = false;
|
||||
hide();
|
||||
if (!res.error) {
|
||||
modalState.from = Object.assign(modalState.from, res.data);
|
||||
modalState.title = t('page.manage.task.editJob');
|
||||
modalState.openByEdit = true;
|
||||
} else {
|
||||
message.error(t('page.manage.task.getInfoError'), 2);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SimpleScrollbar>
|
||||
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||
<!-- 搜索框 -->
|
||||
<ACard :title="$t('common.search')" :bordered="false" class="card-wrapper">
|
||||
<AForm :model="searchParams" :label-width="80">
|
||||
<ARow :gutter="[16, 16]" wrap>
|
||||
<ACol :lg="6" :md="12" :xs="24">
|
||||
<AFormItem label="Name" name="operIp" class="m-0">
|
||||
<AInput v-model:value="searchParams.operIp" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
|
||||
<ACol :lg="6" :md="12" :xs="24">
|
||||
<AFormItem label="Group" name="title" class="m-0">
|
||||
<AInput v-model:value="searchParams.title" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
|
||||
|
||||
<ACol :lg="6" :md="12" :xs="24">
|
||||
<AFormItem :label="$t('page.manage.user.status')" name="status" class="m-0">
|
||||
<ASelect v-model:value="searchParams.status" :placeholder="$t('page.manage.user.form.status')"
|
||||
allow-clear>
|
||||
<ASelectOption v-for="option in enableStatusOptions" :key="option.value" :value="option.value">
|
||||
{{ $t(option.label) }}
|
||||
</ASelectOption>
|
||||
</ASelect>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
|
||||
|
||||
<ACol :lg="6" :md="12" :xs="24">
|
||||
<AFormItem class="m-0">
|
||||
<div class="w-full flex-y-center justify-end gap-8px">
|
||||
<a-space :size="8">
|
||||
<a-button @click="fnTest">
|
||||
<template #icon>
|
||||
<SyncOutlined />
|
||||
</template>
|
||||
{{ $t('common.reset') }}
|
||||
</a-button>
|
||||
<AButton type="primary" ghost @click="getData">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
{{ $t('common.search') }}
|
||||
</AButton>
|
||||
</a-space>
|
||||
|
||||
</div>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
</ARow>
|
||||
</AForm>
|
||||
</ACard>
|
||||
|
||||
<ACard title="Task Management" :bordered="false" :body-style="{ flex: 1, overflow: 'hidden' }"
|
||||
class="flex-col-stretch sm:flex-1-hidden card-wrapper">
|
||||
<template #extra>
|
||||
<!-- <TableHeaderOperation v-model:columns="columnChecks" :disabled-delete="checkedRowKeys.length === 0"
|
||||
:loading="loading" :show-delete="true" @add="handleAdd" @delete="handleBatchDelete" @refresh="getData"
|
||||
:showExport="true" :not-show-add="false" /> -->
|
||||
|
||||
<div class="flex flex-wrap justify-end gap-x-12px gap-y-8px lt-sm:(w-200px py-12px)">
|
||||
<slot name="prefix"></slot>
|
||||
<slot name="default">
|
||||
<AButton size="small" ghost type="primary" @click="fnModalVisibleByEdit()">
|
||||
<div class="flex-y-center gap-8px">
|
||||
<icon-ic-round-plus class="text-icon" />
|
||||
<span>{{ $t('common.add') }}</span>
|
||||
</div>
|
||||
</AButton>
|
||||
<APopconfirm :title="$t('common.confirmDelete')" :disabled="checkedRowKeys.length === 0"
|
||||
@confirm="handleBatchDelete">
|
||||
<AButton size="small" danger :disabled="checkedRowKeys.length <= 0">
|
||||
<div class="flex-y-center gap-8px">
|
||||
<icon-ic-round-delete class="text-icon" />
|
||||
<span>{{ $t('common.batchDelete') }}</span>
|
||||
</div>
|
||||
</AButton>
|
||||
</APopconfirm>
|
||||
</slot>
|
||||
<AButton size="small" @click="getData">
|
||||
<div class="flex-y-center gap-8px">
|
||||
<icon-mdi-refresh class="text-icon" :class="{ 'animate-spin': loading }" />
|
||||
<span>{{ $t('common.refresh') }}</span>
|
||||
</div>
|
||||
</AButton>
|
||||
<!-- <AButton size="small" type="primary" @click="handleExport" v-show="showExport">
|
||||
<div class="flex-y-center gap-8px">
|
||||
<icon-mdi-refresh class="text-icon" :class="{ 'animate-spin': loading }" />
|
||||
<span>{{ $t('common.export') }}</span>
|
||||
</div>
|
||||
</AButton> -->
|
||||
<TableColumnSetting v-model:columns="columns" />
|
||||
<slot name="suffix"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<ATable ref="wrapperEl" row-key="jobId" :columns="columns" :data-source="data" :loading="loading"
|
||||
:row-selection="{
|
||||
selectedRowKeys: checkedRowKeys,
|
||||
onChange: handleUserSelectChange,
|
||||
}" size="small" :pagination="mobilePagination" :scroll="scrollConfig" class="h-full">
|
||||
<template #bodyCell="{ column, record }">
|
||||
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-switch v-model:checked="record.status" checked-value="0" un-checked-value="1" size="small" />
|
||||
<!-- <DictTag :options="{
|
||||
'0': t('common.normal'),
|
||||
'1': t('common.abnormal'),
|
||||
}" :value="record.status" /> -->
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'operate'">
|
||||
<a-space :size="8" align="center">
|
||||
<a-tooltip>
|
||||
<template #title>{{ 'View' }}</template>
|
||||
<a-button type="link" @click.prevent="fnRecordView(record.jobId)">
|
||||
<template #icon>
|
||||
<ProfileOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template #title>{{ 'Edit' }}</template>
|
||||
<a-button type="link" @click.prevent="fnModalVisibleByEdit(record.jobId)">
|
||||
<template #icon>
|
||||
<FormOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip>
|
||||
<template #title>{{ 'Delete' }}</template>
|
||||
<a-button type="link">
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip>
|
||||
<template #title>{{ 'run' }}</template>
|
||||
<a-button type="link">
|
||||
<template #icon>
|
||||
<RocketOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip>
|
||||
<template #title>{{ 'Job Log' }}</template>
|
||||
<a-button type="link">
|
||||
<template #icon>
|
||||
<ContainerOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
</ATable>
|
||||
|
||||
|
||||
|
||||
<!-- <taskOperateDrawer v-model:visible="drawerVisible" :dept-tree-data="deptTreeData" :operate-type="operateType"
|
||||
:row-data="editingData" @submitted="getData" /> -->
|
||||
|
||||
|
||||
<!-- 详情框 -->
|
||||
<a-modal :width="800" v-model:open="modalState.openByView" :title="modalState.title" @cancel="fnModalCancel">
|
||||
<a-form layout=" horizontal" :label-col="{ span: 6 }" :label-wrap="true">
|
||||
<a-row>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item :label="$t('page.manage.task.taskName')" name="jobName">
|
||||
{{ modalState.from.jobName }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item :label="t('page.manage.task.status')" name="status">
|
||||
{{
|
||||
[{
|
||||
label: t('common.normal'),
|
||||
value: '0',
|
||||
}, {
|
||||
label: t('common.abnormal'),
|
||||
value: '1',
|
||||
}].find(s => s.value === modalState.from.status)
|
||||
?.label
|
||||
}}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item :label="t('page.manage.task.invoke')" name="invokeTarget">
|
||||
{{ modalState.from.invokeTarget }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item :label="t('page.manage.task.group')" name="jobGroup">
|
||||
<!-- <DictTag
|
||||
:options="dict.sysJobGroup"
|
||||
:value="modalState.from.jobGroup"
|
||||
/> -->
|
||||
{{ modalState.from.jobGroup }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item :label="t('page.manage.task.cron')" name="cronExpression">
|
||||
<a-tag color="default">
|
||||
{{ modalState.from.cronExpression }}
|
||||
</a-tag>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- <a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item :label="t('page.manage.task.log')" name="saveLog">
|
||||
<DictTag
|
||||
:options="dict.sysJobSaveLog"
|
||||
:value="modalState.from.saveLog"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col> -->
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item :label="t('page.manage.task.createTime')" name="createTime" :label-wrap="true">
|
||||
{{ modalState.from.createTime }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
|
||||
|
||||
<a-form-item :label="t('page.manage.task.targetParams')" name="targetParams" :label-col="{ span: 3 }"
|
||||
:label-wrap="true">
|
||||
<a-textarea v-model:value="modalState.from.targetParams" :auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
:disabled="true" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('page.manage.task.remark')" name="remark" :label-col="{ span: 3 }"
|
||||
:label-wrap="true">
|
||||
<a-textarea v-model:value="modalState.from.remark" :auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
:disabled="true" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-button key="cancel" @click="fnModalCancel">
|
||||
{{ t('common.close') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
|
||||
|
||||
<!-- 新增框或修改框 -->
|
||||
<!-- <a-modal :drag="true" :width="800" :destroyOnClose="true" :keyboard="false" :mask-closable="false"
|
||||
:open="modalState.openByEdit" :title="modalState.title" :confirm-loading="modalState.confirmLoading"
|
||||
@ok="fnModalOk" @cancel="fnModalCancel">
|
||||
<a-form name="modalStateFrom" layout="horizontal" :label-col="{ span: 6 }" :label-wrap="true" :rules="rules">
|
||||
<a-row>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item :label="t('page.manage.task.taskName')" name="jobName">
|
||||
<a-input v-model:value="modalState.from.jobName" allow-clear
|
||||
:placeholder="t('page.manage.task.jobNamePlease')"></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item :label="t('page.manage.task.status')" name="status">
|
||||
<a-select v-model:value="modalState.from.status" default-value="0"
|
||||
:placeholder="t('common.selectPlease')" :options="dict.sysJobStatus">
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item :label="t('page.manage.task.jobGroup')" name="jobGroup">
|
||||
<a-select v-model:value="modalState.from.jobGroup" default-value="DEFAULT"
|
||||
:placeholder="t('common.selectPlease')" :options="dict.sysJobGroup">
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-form-item :label="t('page.manage.task.saveLog')" name="saveLog">
|
||||
<a-select v-model:value="modalState.from.saveLog" default-value="0"
|
||||
:placeholder="t('common.selectPlease')" :options="dict.sysJobSaveLog">
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item :label="t('page.manage.task.invokeTarget')" name="invokeTarget" :label-col="{ span: 3 }"
|
||||
:label-wrap="true">
|
||||
<a-input v-model:value="modalState.from.invokeTarget" allow-clear
|
||||
:placeholder="t('page.manage.task.invokeTargetPlease')">
|
||||
<template #prefix>
|
||||
<a-tooltip placement="topLeft">
|
||||
<template #title>
|
||||
<div>{{ t('page.manage.task.invokeTargetTip') }}</div>
|
||||
</template>
|
||||
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('page.manage.task.cronExpression')" name="cronExpression" :label-col="{ span: 3 }"
|
||||
:label-wrap="true">
|
||||
<a-input v-model:value="modalState.from.cronExpression" allow-clear
|
||||
:placeholder="t('page.manage.task.cronExpressionPlease')">
|
||||
<template #prefix>
|
||||
<a-tooltip placement="topLeft">
|
||||
<template #title>
|
||||
<div>
|
||||
{{ t('page.manage.task.cronExpressionTip') }}<br />
|
||||
{{ t('page.manage.task.cronExpressionTip1') }}
|
||||
</div>
|
||||
</template>
|
||||
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template #addonAfter>
|
||||
<a-button type="text" size="small" @click.prevent="fnModalCron(true)">
|
||||
<template #icon>
|
||||
<FieldTimeOutlined />
|
||||
</template>
|
||||
{{ t('page.manage.task.cronExpressionNew') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('page.manage.task.targetParams')" name="targetParams" :label-col="{ span: 3 }"
|
||||
:label-wrap="true">
|
||||
<a-textarea v-model:value="modalState.from.targetParams" :auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
:maxlength="400" :placeholder="t('page.manage.task.targetParamsPlease')" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('page.manage.task.remark')" name="remark" :label-col="{ span: 3 }"
|
||||
:label-wrap="true">
|
||||
<a-textarea v-model:value="modalState.from.remark" :auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
:maxlength="400" :show-count="true" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal> -->
|
||||
|
||||
</ACard>
|
||||
|
||||
|
||||
</div>
|
||||
</SimpleScrollbar>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
222
src/views/manage/task/modules/task-operate-drawer.vue
Normal file
222
src/views/manage/task/modules/task-operate-drawer.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<script setup lang="ts">
|
||||
import { SimpleScrollbar } from '@sa/materials';
|
||||
import { useAntdForm, useFormRules } from '@/hooks/common/form';
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusOptions } from '@/constants/business';
|
||||
|
||||
defineOptions({
|
||||
name: 'logOperateDrawer'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** the type of operation */
|
||||
operateType: any;
|
||||
/** the edit row data */
|
||||
rowData?:any;
|
||||
deptTreeData:any;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const { formRef, validate, resetFields } = useAntdForm();
|
||||
const { defaultRequiredRule, formRules } = useFormRules();
|
||||
const [spinning, spin] = useToggle();
|
||||
|
||||
const title = computed(() => {
|
||||
const titles: any = {
|
||||
add: $t('page.manage.user.addUser'),
|
||||
edit: $t('page.manage.user.editUser')
|
||||
};
|
||||
return titles[props.operateType];
|
||||
});
|
||||
|
||||
type Model = Partial<
|
||||
Pick<Api.Auth.User, 'userName' | 'nickName' | 'email' | 'phonenumber' | 'status' | 'deptId' | 'remark'> & {
|
||||
postIds: number[];
|
||||
roleIds: number[];
|
||||
password?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
const model = ref<Model>(createDefaultModel());
|
||||
|
||||
const userPosts = ref<Api.SystemManage.Post[]>([]);
|
||||
const userRoles = ref<Api.SystemManage.Role[]>([]);
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
userName: '',
|
||||
nickName: '',
|
||||
email: '',
|
||||
phonenumber: '',
|
||||
status: '0',
|
||||
deptId: undefined,
|
||||
remark: '',
|
||||
postIds: [],
|
||||
roleIds: [],
|
||||
password: ''
|
||||
};
|
||||
}
|
||||
|
||||
//制定规则
|
||||
const rules = {
|
||||
userName: defaultRequiredRule,
|
||||
nickName: defaultRequiredRule,
|
||||
status: defaultRequiredRule,
|
||||
deptId: defaultRequiredRule,
|
||||
email: formRules.email,
|
||||
phonenumber: formRules.phone,
|
||||
password: formRules.pwd,
|
||||
postIds: defaultRequiredRule,
|
||||
roleIds: defaultRequiredRule
|
||||
};
|
||||
|
||||
// 修改时触发
|
||||
async function init(userId: number | undefined = undefined) {
|
||||
spin(true);
|
||||
try {
|
||||
await Promise.all([getUserPostAndRole(userId)]);
|
||||
spin(false);
|
||||
} catch (error) {
|
||||
spin(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserPostAndRole(userId: number | undefined) {
|
||||
const { error, data } = await doGetUserPostsAndRoles(userId);
|
||||
//渲染用户岗位和角色option
|
||||
if (!error) {
|
||||
const { postIds, posts, roleIds, roles } = data;
|
||||
userPosts.value = posts;
|
||||
userRoles.value = roles;
|
||||
model.value.postIds = postIds;
|
||||
model.value.roleIds = roleIds;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateModelWhenEdit() {
|
||||
if (props.operateType === 'add') {
|
||||
model.value = createDefaultModel(); //新增时赋值
|
||||
}
|
||||
|
||||
if (props.operateType === 'edit' && props.rowData) {
|
||||
await init(props.rowData.userId);
|
||||
model.value = Object.assign(model.value, omit(props.rowData, ['postIds', 'roleIds']));
|
||||
} else {
|
||||
await init();
|
||||
model.value = createDefaultModel();
|
||||
}
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
|
||||
const { error } = await (props.operateType === 'edit' ? doPutUser : doPostUser)(model.value as Api.Auth.User);
|
||||
|
||||
if (!error) {
|
||||
$message?.success($t(props.operateType === 'add' ? 'common.addSuccess' : 'common.updateSuccess'));
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
val => {
|
||||
if (val) {
|
||||
handleUpdateModelWhenEdit();
|
||||
resetFields();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// console.log(props.operateType)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ADrawer
|
||||
v-model:open="visible"
|
||||
:body-style="{ paddingRight: '0px', paddingTop: '0', paddingBottom: '0' }"
|
||||
:title="title"
|
||||
:width="460"
|
||||
>
|
||||
<SimpleScrollbar>
|
||||
<ASpin :spinning="spinning" size="small">
|
||||
<AForm ref="formRef" py-20px pr-20px layout="vertical" :model="model" :rules="rules">
|
||||
<AFormItem :label="$t('page.manage.user.userName')" name="userName">
|
||||
<AInput v-model:value="model.userName" :placeholder="$t('page.manage.user.form.userName')" />
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.user.nickName')" name="nickName">
|
||||
<AInput v-model:value="model.nickName" :placeholder="$t('page.manage.user.form.nickName')" />
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.user.email')" name="email">
|
||||
<AInput v-model:value="model.email" :placeholder="$t('page.manage.user.form.email')" />
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.user.phonenumber')" name="phonenumber">
|
||||
<AInput v-model:value="model.phonenumber" :placeholder="$t('page.manage.user.form.phonenumber')" />
|
||||
</AFormItem>
|
||||
<AFormItem v-if="props.operateType === 'add'" :label="$t('page.manage.user.password')" name="password">
|
||||
<AInput
|
||||
v-model:value="model.password"
|
||||
type="password"
|
||||
:placeholder="$t('page.manage.user.form.password')"
|
||||
/>
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.user.post')" name="postIds">
|
||||
<ASelect
|
||||
v-model:value="model.postIds"
|
||||
:field-names="{ label: 'postName', value: 'postId' }"
|
||||
mode="multiple"
|
||||
:options="userPosts"
|
||||
/>
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.user.role')" name="roleIds">
|
||||
<ASelect
|
||||
v-model:value="model.roleIds"
|
||||
:field-names="{ label: 'roleName', value: 'roleId' }"
|
||||
mode="multiple"
|
||||
:options="userRoles"
|
||||
/>
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.user.status')" name="status">
|
||||
<ARadioGroup v-model:value="model.status">
|
||||
<ARadio v-for="item in enableStatusOptions" :key="item.value" :value="item.value">
|
||||
{{ $t(item.label) }}
|
||||
</ARadio>
|
||||
</ARadioGroup>
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.user.dept')" name="deptId">
|
||||
<ATreeSelect v-model:value="model.deptId" :field-names="{ value: 'id' }" :tree-data="deptTreeData" />
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.user.remark')" name="remark">
|
||||
<ATextarea v-model:value="model.remark" :placeholder="$t('page.manage.user.form.remark')" />
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
</ASpin>
|
||||
</SimpleScrollbar>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex-y-center justify-end gap-12px">
|
||||
<AButton @click="closeDrawer">{{ $t('common.cancel') }}</AButton>
|
||||
<AButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</AButton>
|
||||
</div>
|
||||
</template>
|
||||
</ADrawer>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
109
src/views/manage/task/modules/task-search.vue
Normal file
109
src/views/manage/task/modules/task-search.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusOptions } from '@/constants/business';
|
||||
import { SyncOutlined, SearchOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'logSearch'
|
||||
});
|
||||
|
||||
|
||||
|
||||
interface Emits {
|
||||
(e: 'reset'): void;
|
||||
(e: 'search'): void;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const model = defineModel<any>('model', { required: true });
|
||||
|
||||
/**记录开始结束时间 */
|
||||
let queryRangePicker = ref<[string, string]>(['', '']);
|
||||
|
||||
function reset() {
|
||||
// 重置表单
|
||||
//formRef.value?.resetFields();
|
||||
|
||||
// 重置日期选择器
|
||||
queryRangePicker.value = ['', ''];
|
||||
|
||||
// 确保 model 也被重置
|
||||
model.value = {};
|
||||
|
||||
// 使用 nextTick 确保视图更新后检查
|
||||
nextTick(() => {
|
||||
console.log('视图更新后:', { ...model.value });
|
||||
console.log(model);
|
||||
});
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
function search() {
|
||||
if (!queryRangePicker.value) {
|
||||
queryRangePicker.value = ['', ''];
|
||||
}
|
||||
model.value.beginTime = queryRangePicker.value[0];
|
||||
model.value.endTime = queryRangePicker.value[1];
|
||||
emit('search');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard :title="$t('common.search')" :bordered="false" class="card-wrapper">
|
||||
<AForm :model="model" :label-width="80">
|
||||
<ARow :gutter="[16, 16]" wrap>
|
||||
<ACol :lg="6" :md="12" :xs="24">
|
||||
<AFormItem label="Name" name="operIp" class="m-0">
|
||||
<AInput v-model:value="model.operIp" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
|
||||
<ACol :lg="6" :md="12" :xs="24">
|
||||
<AFormItem label="Group" name="title" class="m-0">
|
||||
<AInput v-model:value="model.title" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
|
||||
|
||||
<ACol :lg="6" :md="12" :xs="24">
|
||||
<AFormItem :label="$t('page.manage.user.status')" name="status" class="m-0">
|
||||
<ASelect v-model:value="model.status" :placeholder="$t('page.manage.user.form.status')" allow-clear>
|
||||
<ASelectOption v-for="option in enableStatusOptions" :key="option.value" :value="option.value">
|
||||
{{ $t(option.label) }}
|
||||
</ASelectOption>
|
||||
</ASelect>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
|
||||
|
||||
<ACol :lg="6" :md="12" :xs="24">
|
||||
<AFormItem class="m-0">
|
||||
<div class="w-full flex-y-center justify-end gap-8px">
|
||||
|
||||
<a-space :size="8">
|
||||
<a-button @click="reset">
|
||||
<template #icon>
|
||||
<SyncOutlined />
|
||||
</template>
|
||||
{{ $t('common.reset') }}
|
||||
</a-button>
|
||||
<AButton type="primary" ghost @click="search">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
{{ $t('common.search') }}
|
||||
</AButton>
|
||||
</a-space>
|
||||
|
||||
</div>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
</ARow>
|
||||
</AForm>
|
||||
</ACard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user