2
0

日志管理+任务管理

This commit is contained in:
lai
2024-12-13 19:14:32 +08:00
parent 57e33781d1
commit e0d991da15
20 changed files with 1472 additions and 39 deletions

11
.prettierrc.json Normal file
View File

@@ -0,0 +1,11 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"semi": true,
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "avoid"
}

View File

@@ -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:*",

View File

@@ -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
View File

@@ -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

View File

@@ -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>

View File

@@ -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'
};

View File

@@ -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);

View File

@@ -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',
}
}
},

View File

@@ -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
View 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
View 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' });
}

View File

@@ -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']

View File

@@ -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']

View 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>

View 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>

View File

@@ -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') && (

View File

@@ -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>();

View 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>

View 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>

View 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>