提交 3b8ca420 作者: Vben

feat: add dept management page

上级 37669d06
import { MockMethod } from 'vite-plugin-mock'; import { MockMethod } from 'vite-plugin-mock';
import { resultPageSuccess } from '../_util'; import { resultPageSuccess, resultSuccess } from '../_util';
const list = (() => { const accountList = (() => {
const result: any[] = []; const result: any[] = [];
for (let index = 0; index < 20; index++) { for (let index = 0; index < 20; index++) {
result.push({ result.push({
...@@ -10,8 +10,40 @@ const list = (() => { ...@@ -10,8 +10,40 @@ const list = (() => {
email: '@email', email: '@email',
nickname: '@cname()', nickname: '@cname()',
role: '@first', role: '@first',
updateTime: '@datetime', createTime: '@datetime',
remark: '@cword(0,20)', remark: '@cword(10,20)',
'status|1': ['0', '1'],
});
}
return result;
})();
const deptList = (() => {
const result: any[] = [];
for (let index = 0; index < 3; index++) {
result.push({
id: `${index}`,
deptName: ['华东分部', '华南分部', '西北分部'][index],
orderNo: index + 1,
createTime: '@datetime',
remark: '@cword(10,20)',
'status|1': ['0', '0', '1'],
children: (() => {
const children: any[] = [];
for (let j = 0; j < 4; j++) {
children.push({
id: `${index}-${j}`,
deptName: ['研发部', '市场部', '商务部', '财务部'][j],
orderNo: j + 1,
createTime: '@datetime',
remark: '@cword(10,20)',
'status|1': ['0', '1'],
parentDept: `${index}`,
children: undefined,
});
}
return children;
})(),
}); });
} }
return result; return result;
...@@ -24,7 +56,15 @@ export default [ ...@@ -24,7 +56,15 @@ export default [
method: 'get', method: 'get',
response: ({ query }) => { response: ({ query }) => {
const { page = 1, pageSize = 20 } = query; const { page = 1, pageSize = 20 } = query;
return resultPageSuccess(page, pageSize, list); return resultPageSuccess(page, pageSize, accountList);
},
},
{
url: '/api/system/getDeptList',
timeout: 100,
method: 'get',
response: () => {
return resultSuccess(deptList);
}, },
}, },
] as MockMethod[]; ] as MockMethod[];
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
}, },
"dependencies": { "dependencies": {
"@iconify/iconify": "^2.0.0-rc.6", "@iconify/iconify": "^2.0.0-rc.6",
"@vueuse/core": "^4.3.0", "@vueuse/core": "^4.3.1",
"@zxcvbn-ts/core": "^0.2.0", "@zxcvbn-ts/core": "^0.2.0",
"ant-design-vue": "2.0.1", "ant-design-vue": "2.0.1",
"apexcharts": "^3.25.0", "apexcharts": "^3.25.0",
...@@ -80,10 +80,10 @@ ...@@ -80,10 +80,10 @@
"eslint-config-prettier": "^8.1.0", "eslint-config-prettier": "^8.1.0",
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^7.6.0", "eslint-plugin-vue": "^7.6.0",
"esno": "^0.4.4", "esno": "^0.4.5",
"fs-extra": "^9.1.0", "fs-extra": "^9.1.0",
"http-server": "^0.12.3", "http-server": "^0.12.3",
"husky": "^5.1.1", "husky": "^5.1.2",
"is-ci": "^3.0.0", "is-ci": "^3.0.0",
"less": "^4.1.1", "less": "^4.1.1",
"lint-staged": "^10.5.4", "lint-staged": "^10.5.4",
...@@ -103,7 +103,7 @@ ...@@ -103,7 +103,7 @@
"vite-plugin-imagemin": "^0.2.8", "vite-plugin-imagemin": "^0.2.8",
"vite-plugin-mock": "^2.1.5", "vite-plugin-mock": "^2.1.5",
"vite-plugin-purge-icons": "^0.7.0", "vite-plugin-purge-icons": "^0.7.0",
"vite-plugin-pwa": "^0.5.5", "vite-plugin-pwa": "^0.5.6",
"vite-plugin-style-import": "^0.7.5", "vite-plugin-style-import": "^0.7.5",
"vite-plugin-theme": "^0.4.8", "vite-plugin-theme": "^0.4.8",
"vite-plugin-windicss": "0.6.2", "vite-plugin-windicss": "0.6.2",
......
import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel'; import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel';
export type Params = BasicPageParams & { export type AccountParams = BasicPageParams & {
account?: string; account?: string;
nickname?: string; nickname?: string;
}; };
export interface DemoListItem { export type DeptParams = {
deptName?: string;
status?: string;
};
export interface AccountListItem {
id: string; id: string;
account: string; account: string;
email: string; email: string;
nickname: string; nickname: string;
role: number; role: number;
updateTime: string; createTime: string;
remark: string; remark: string;
status: number;
}
export interface DeptListItem {
id: string;
orderNo: string;
createTime: string;
remark: string;
status: number;
} }
/** /**
* @description: Request list return value * @description: Request list return value
*/ */
export type DemoListGetResultModel = BasicFetchResult<DemoListItem>; export type AccountListGetResultModel = BasicFetchResult<AccountListItem>;
export type DeptListGetResultModel = BasicFetchResult<DeptListItem>;
import { Params, DemoListGetResultModel } from './model/systemModel'; import {
AccountParams,
DeptListItem,
DeptListGetResultModel,
AccountListGetResultModel,
} from './model/systemModel';
import { defHttp } from '/@/utils/http/axios'; import { defHttp } from '/@/utils/http/axios';
enum Api { enum Api {
// The address does not exist // The address does not exist
AccountList = '/system/getAccountList', AccountList = '/system/getAccountList',
DeptList = '/system/getDeptList',
} }
export const getAccountList = (params: Params) => export const getAccountList = (params: AccountParams) =>
defHttp.get<DemoListGetResultModel>({ url: Api.AccountList, params }); defHttp.get<AccountListGetResultModel>({ url: Api.AccountList, params });
export const getDeptList = (params?: DeptListItem) =>
defHttp.get<DeptListGetResultModel>({ url: Api.DeptList, params });
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
--> -->
<template> <template>
<span :class="getClass"> <span :class="getClass">
<RightOutlined /> <Icon icon="ion:chevron-forward" :style="$attrs.iconStyle" />
</span> </span>
</template> </template>
<script lang="ts"> <script lang="ts">
...@@ -15,9 +15,11 @@ ...@@ -15,9 +15,11 @@
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '/@/utils/propTypes';
import { Icon } from '/@/components/Icon';
export default defineComponent({ export default defineComponent({
name: 'BasicArrow', name: 'BasicArrow',
components: { RightOutlined }, components: { RightOutlined, Icon },
props: { props: {
// Expand contract, expand by default // Expand contract, expand by default
expand: propTypes.bool, expand: propTypes.bool,
......
...@@ -21,6 +21,7 @@ import { ...@@ -21,6 +21,7 @@ import {
import RadioButtonGroup from './components/RadioButtonGroup.vue'; import RadioButtonGroup from './components/RadioButtonGroup.vue';
import ApiSelect from './components/ApiSelect.vue'; import ApiSelect from './components/ApiSelect.vue';
import { BasicUpload } from '/@/components/Upload'; import { BasicUpload } from '/@/components/Upload';
import { StrengthMeter } from '/@/components/StrengthMeter';
const componentMap = new Map<ComponentType, Component>(); const componentMap = new Map<ComponentType, Component>();
...@@ -51,6 +52,7 @@ componentMap.set('MonthPicker', DatePicker.MonthPicker); ...@@ -51,6 +52,7 @@ componentMap.set('MonthPicker', DatePicker.MonthPicker);
componentMap.set('RangePicker', DatePicker.RangePicker); componentMap.set('RangePicker', DatePicker.RangePicker);
componentMap.set('WeekPicker', DatePicker.WeekPicker); componentMap.set('WeekPicker', DatePicker.WeekPicker);
componentMap.set('TimePicker', TimePicker); componentMap.set('TimePicker', TimePicker);
componentMap.set('StrengthMeter', StrengthMeter);
componentMap.set('Upload', BasicUpload); componentMap.set('Upload', BasicUpload);
......
...@@ -91,7 +91,6 @@ export type ComponentType = ...@@ -91,7 +91,6 @@ export type ComponentType =
| 'Select' | 'Select'
| 'ApiSelect' | 'ApiSelect'
| 'SelectOptGroup' | 'SelectOptGroup'
| 'SelectOption'
| 'TreeSelect' | 'TreeSelect'
| 'Transfer' | 'Transfer'
| 'RadioButtonGroup' | 'RadioButtonGroup'
......
...@@ -65,6 +65,7 @@ ...@@ -65,6 +65,7 @@
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '/@/hooks/web/useDesign';
import { basicProps } from './props'; import { basicProps } from './props';
import expandIcon from './components/ExpandIcon';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import './style/index.less'; import './style/index.less';
...@@ -193,6 +194,7 @@ ...@@ -193,6 +194,7 @@
size: 'middle', size: 'middle',
...attrs, ...attrs,
customRow, customRow,
expandIcon: expandIcon(),
...unref(getProps), ...unref(getProps),
...unref(getHeaderProps), ...unref(getHeaderProps),
scroll: unref(getScrollRef), scroll: unref(getScrollRef),
......
import { BasicArrow } from '/@/components/Basic';
export default () => {
return (props: Recordable) => {
if (!props.expandable) {
return null;
}
return (
<BasicArrow
class="mr-1"
iconStyle="margin-top: -2px;"
onClick={(e: Event) => {
props.onExpand(props.record, e);
}}
expand={props.expanded}
/>
);
};
};
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
padding: 16px; padding: 16px;
.ant-form { .ant-form {
padding: 20px 20px 4px 12px; padding: 16px 16px 6px 12px;
margin-bottom: 18px; margin-bottom: 18px;
background: #fff; background: #fff;
border-radius: 4px; border-radius: 4px;
......
...@@ -2,4 +2,8 @@ export default { ...@@ -2,4 +2,8 @@ export default {
moduleName: 'System management', moduleName: 'System management',
account: 'Account management', account: 'Account management',
password: 'Change password',
dept: 'Department management',
}; };
...@@ -2,4 +2,8 @@ export default { ...@@ -2,4 +2,8 @@ export default {
moduleName: '系统管理', moduleName: '系统管理',
account: '账号管理', account: '账号管理',
password: '修改密码',
dept: '部门管理',
}; };
...@@ -11,6 +11,16 @@ const menu: MenuModule = { ...@@ -11,6 +11,16 @@ const menu: MenuModule = {
path: 'account', path: 'account',
name: t('routes.demo.system.account'), name: t('routes.demo.system.account'),
}, },
{
path: 'dept',
name: t('routes.demo.system.dept'),
},
{
path: 'changePassword',
name: t('routes.demo.system.password'),
},
], ],
}, },
}; };
......
...@@ -15,12 +15,31 @@ const system: AppRouteModule = { ...@@ -15,12 +15,31 @@ const system: AppRouteModule = {
children: [ children: [
{ {
path: 'account', path: 'account',
name: 'Account', name: 'AccountManagement',
meta: { meta: {
title: t('routes.demo.system.account'), title: t('routes.demo.system.account'),
ignoreKeepAlive: true,
}, },
component: () => import('/@/views/demo/system/account/index.vue'), component: () => import('/@/views/demo/system/account/index.vue'),
}, },
{
path: 'dept',
name: 'DeptManagement',
meta: {
title: t('routes.demo.system.dept'),
ignoreKeepAlive: true,
},
component: () => import('/@/views/demo/system/dept/index.vue'),
},
{
path: 'changePassword',
name: 'ChangePassword',
meta: {
title: t('routes.demo.system.password'),
ignoreKeepAlive: true,
},
component: () => import('/@/views/demo/system/password/index.vue'),
},
], ],
}; };
......
...@@ -83,9 +83,15 @@ export class VAxios { ...@@ -83,9 +83,15 @@ export class VAxios {
this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => { this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => {
// If cancel repeat request is turned on, then cancel repeat request is prohibited // If cancel repeat request is turned on, then cancel repeat request is prohibited
const { const {
headers: { ignoreCancelToken = false }, headers: { ignoreCancelToken },
} = config; } = config;
!ignoreCancelToken && axiosCanceler.addPending(config);
const ignoreCancel =
ignoreCancelToken !== undefined
? ignoreCancelToken
: this.options.requestOptions?.ignoreCancelToken;
!ignoreCancel && axiosCanceler.addPending(config);
if (requestInterceptors && isFunction(requestInterceptors)) { if (requestInterceptors && isFunction(requestInterceptors)) {
config = requestInterceptors(config); config = requestInterceptors(config);
} }
......
...@@ -202,6 +202,8 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) { ...@@ -202,6 +202,8 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
apiUrl: globSetting.apiUrl, apiUrl: globSetting.apiUrl,
// 是否加入时间戳 // 是否加入时间戳
joinTime: true, joinTime: true,
// 忽略重复请求
ignoreCancelToken: true,
}, },
}, },
opt || {} opt || {}
......
...@@ -17,6 +17,7 @@ export interface RequestOptions { ...@@ -17,6 +17,7 @@ export interface RequestOptions {
errorMessageMode?: ErrorMessageMode; errorMessageMode?: ErrorMessageMode;
// Whether to add a timestamp // Whether to add a timestamp
joinTime?: boolean; joinTime?: boolean;
ignoreCancelToken?: boolean;
} }
export interface CreateAxiosOptions extends AxiosRequestConfig { export interface CreateAxiosOptions extends AxiosRequestConfig {
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
}); });
const [registerModal, { setModalProps }] = useModalInner((data) => { const [registerModal, { setModalProps }] = useModalInner((data) => {
setModalProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate; isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) { if (unref(isUpdate)) {
...@@ -42,7 +43,7 @@ ...@@ -42,7 +43,7 @@
// TODO custom api // TODO custom api
console.log(values); console.log(values);
} finally { } finally {
setModalProps({ confirmLoading: true }); setModalProps({ confirmLoading: false });
} }
} }
......
...@@ -3,11 +3,6 @@ import { FormSchema } from '/@/components/Table'; ...@@ -3,11 +3,6 @@ import { FormSchema } from '/@/components/Table';
export const columns: BasicColumn[] = [ export const columns: BasicColumn[] = [
{ {
title: 'ID',
dataIndex: 'id',
width: 80,
},
{
title: '用户名', title: '用户名',
dataIndex: 'account', dataIndex: 'account',
width: 120, width: 120,
...@@ -23,8 +18,8 @@ export const columns: BasicColumn[] = [ ...@@ -23,8 +18,8 @@ export const columns: BasicColumn[] = [
width: 200, width: 200,
}, },
{ {
title: '更新时间', title: '创建时间',
dataIndex: 'updateTime', dataIndex: 'createTime',
width: 180, width: 180,
}, },
{ {
...@@ -35,7 +30,6 @@ export const columns: BasicColumn[] = [ ...@@ -35,7 +30,6 @@ export const columns: BasicColumn[] = [
{ {
title: '备注', title: '备注',
dataIndex: 'remark', dataIndex: 'remark',
width: 200,
}, },
]; ];
......
...@@ -8,11 +8,11 @@ ...@@ -8,11 +8,11 @@
<TableAction <TableAction
:actions="[ :actions="[
{ {
label: '编辑', icon: 'clarity:note-edit-line',
onClick: handleEdit.bind(null, record), onClick: handleEdit.bind(null, record),
}, },
{ {
label: '删除', icon: 'ant-design:delete-outlined',
color: 'error', color: 'error',
popConfirm: { popConfirm: {
title: '是否确认删除', title: '是否确认删除',
...@@ -55,8 +55,9 @@ ...@@ -55,8 +55,9 @@
}, },
useSearchForm: true, useSearchForm: true,
showTableSetting: true, showTableSetting: true,
bordered: true,
actionColumn: { actionColumn: {
width: 160, width: 80,
title: '操作', title: '操作',
dataIndex: 'action', dataIndex: 'action',
slots: { customRender: 'action' }, slots: { customRender: 'action' },
......
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="getTitle" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts">
import { defineComponent, ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formSchema } from './dept.data';
import { getDeptList } from '/@/api/demo/system';
export default defineComponent({
name: 'DeptModal',
components: { BasicModal, BasicForm },
emits: ['success', 'register'],
setup(_, { emit }) {
const isUpdate = ref(true);
const [registerForm, { resetFields, setFieldsValue, updateSchema, validate }] = useForm({
labelWidth: 100,
schemas: formSchema,
showActionButtonGroup: false,
actionColOptions: {
span: 23,
},
});
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
resetFields();
setModalProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
setFieldsValue({
...data.record,
});
}
const treeData = await getDeptList();
updateSchema({
field: 'parentDept',
componentProps: { treeData },
});
});
const getTitle = computed(() => (!unref(isUpdate) ? '新增部门' : '编辑部门'));
async function handleSubmit() {
try {
const values = await validate();
setModalProps({ confirmLoading: true });
// TODO custom api
console.log(values);
closeModal();
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
return { registerModal, registerForm, getTitle, handleSubmit };
},
});
</script>
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
import { h } from 'vue';
import { Tag } from 'ant-design-vue';
export const columns: BasicColumn[] = [
{
title: '部门名称',
dataIndex: 'deptName',
width: 300,
},
{
title: '排序',
dataIndex: 'orderNo',
width: 80,
},
{
title: '状态',
dataIndex: 'status',
width: 120,
customRender: ({ record }) => {
const status = record.status;
const enable = ~~status === 0;
const color = enable ? 'green' : 'red';
const text = enable ? '正常' : '停用';
return h(Tag, { color: color }, () => text);
},
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 180,
},
{
title: '备注',
dataIndex: 'remark',
},
];
export const searchFormSchema: FormSchema[] = [
{
field: 'deptName',
label: '部门名称',
component: 'Input',
colProps: { span: 8 },
},
{
field: 'status',
label: '状态',
component: 'Select',
componentProps: {
options: [
{ label: '启用', value: '0' },
{ label: '停用', value: '1' },
],
},
colProps: { span: 8 },
},
];
export const formSchema: FormSchema[] = [
{
field: 'deptName',
label: '部门名称',
component: 'Input',
required: true,
},
{
field: 'parentDept',
label: '上级部门',
component: 'TreeSelect',
componentProps: {
replaceFields: {
title: 'deptName',
key: 'id',
value: 'id',
},
getPopupContainer: () => document.body,
},
required: true,
},
{
field: 'orderNo',
label: '排序',
component: 'InputNumber',
required: true,
},
{
field: 'status',
label: '状态',
component: 'RadioButtonGroup',
componentProps: {
options: [
{ label: '正常', value: '0' },
{ label: '禁用', value: '1' },
],
},
required: true,
},
{
label: '备注',
field: 'remark',
component: 'InputTextArea',
},
];
<template>
<div>
<BasicTable @register="registerTable">
<template #toolbar>
<a-button type="primary" @click="handleCreate"> 新增部门 </a-button>
</template>
<template #action="{ record }">
<TableAction
:actions="[
{
icon: 'clarity:note-edit-line',
onClick: handleEdit.bind(null, record),
},
{
icon: 'ant-design:delete-outlined',
color: 'error',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
]"
/>
</template>
</BasicTable>
<DeptModal @register="registerModal" @success="handleSuccess" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { getDeptList } from '/@/api/demo/system';
import { useModal } from '/@/components/Modal';
import DeptModal from './DeptModal.vue';
import { columns, searchFormSchema } from './dept.data';
export default defineComponent({
name: 'DeptManagement',
components: { BasicTable, DeptModal, TableAction },
setup() {
const [registerModal, { openModal }] = useModal();
const [registerTable, { reload }] = useTable({
title: '部门列表',
api: getDeptList,
columns,
formConfig: {
labelWidth: 120,
schemas: searchFormSchema,
},
pagination: false,
striped: false,
useSearchForm: true,
showTableSetting: true,
bordered: true,
showIndexColumn: false,
indentSize: 20,
actionColumn: {
width: 80,
title: '操作',
dataIndex: 'action',
slots: { customRender: 'action' },
fixed: undefined,
},
});
function handleCreate() {
openModal(true, {
isUpdate: false,
});
}
function handleEdit(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
});
}
function handleDelete(record: Recordable) {
console.log(record);
}
function handleSuccess() {
reload();
}
return {
registerTable,
registerModal,
handleCreate,
handleEdit,
handleDelete,
handleSuccess,
};
},
});
</script>
<template>
<div class="p-4 flex flex-col justify-center items-center">
<BasicForm @register="register" />
<div class="flex justify-center">
<a-button @click="resetFields"> 重置 </a-button>
<a-button class="ml-4" type="primary" @click="handleSubmit"> 确认 </a-button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { BasicForm, useForm } from '/@/components/Form';
import { formSchema } from './pwd.data';
export default defineComponent({
name: 'ChangePassword',
components: { BasicForm },
setup() {
const [register, { validate, resetFields }] = useForm({
size: 'large',
labelWidth: 100,
showActionButtonGroup: false,
schemas: formSchema,
});
async function handleSubmit() {
try {
const values = await validate();
const { passwordOld, passwordNew } = values;
// TODO custom api
console.log(passwordOld, passwordNew);
// const { router } = useRouter();
// router.push(pageEnum.BASE_LOGIN);
} catch (error) {}
}
return { register, resetFields, handleSubmit };
},
});
</script>
import { FormSchema } from '/@/components/Form';
export const formSchema: FormSchema[] = [
{
field: 'passwordOld',
label: '当前密码',
component: 'InputPassword',
required: true,
},
{
field: 'passwordNew',
label: '新密码',
component: 'StrengthMeter',
componentProps: {
placeholder: '新密码',
},
rules: [
{
required: true,
message: '请输入新密码',
},
],
},
{
field: 'confirmPassword',
label: '确认密码',
component: 'InputPassword',
dynamicRules: ({ values }) => {
return [
{
required: true,
validator: (_, value) => {
if (!value) {
return Promise.reject('不能为空');
}
if (value !== values.passwordNew) {
return Promise.reject('两次输入的密码不一致!');
}
return Promise.resolve();
},
},
];
},
},
];
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论