feat: 设置模块用户列表功能90%

This commit is contained in:
Zzz1zz 2025-07-06 00:12:32 +08:00
parent 1cfa0b6e4c
commit 293beb9f5d
17 changed files with 1758 additions and 147 deletions

View file

@ -67,5 +67,7 @@ module.exports = {
'no-param-reassign': 0, 'no-param-reassign': 0,
'prefer-regex-literals': 0, 'prefer-regex-literals': 0,
'import/no-extraneous-dependencies': 0, 'import/no-extraneous-dependencies': 0,
// 禁用 prefer-destructuring 规则,因为它对对象解构和数组解构都产生错误警告
'prefer-destructuring': 0,
}, },
}; };

View file

@ -30,7 +30,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@arco-design/web-vue": "^2.44.7", "@arco-design/web-vue": "^2.57.0",
"@vueuse/core": "^9.3.0", "@vueuse/core": "^9.3.0",
"arco-design-pro-vue": "^2.7.3", "arco-design-pro-vue": "^2.7.3",
"axios": "^0.24.0", "axios": "^0.24.0",
@ -49,7 +49,6 @@
"vue-router": "^4.0.14" "vue-router": "^4.0.14"
}, },
"devDependencies": { "devDependencies": {
"@arco-design/web-vue": "^2.0.0-beta.7",
"@arco-plugins/vite-vue": "^1.4.5", "@arco-plugins/vite-vue": "^1.4.5",
"@commitlint/cli": "^17.1.2", "@commitlint/cli": "^17.1.2",
"@commitlint/config-conventional": "^17.1.0", "@commitlint/config-conventional": "^17.1.0",
@ -102,8 +101,5 @@
"bin-wrapper": "npm:bin-wrapper-china", "bin-wrapper": "npm:bin-wrapper-china",
"rollup": "^2.56.3", "rollup": "^2.56.3",
"gifsicle": "5.2.0" "gifsicle": "5.2.0"
},
"peerDependencies": {
"@arco-design/web-vue": ">=2.0.0-beta.7"
} }
} }

22
pnpm-lock.yaml generated
View file

@ -14,7 +14,7 @@ importers:
.: .:
dependencies: dependencies:
'@arco-design/web-vue': '@arco-design/web-vue':
specifier: ^2.44.7 specifier: ^2.57.0
version: 2.57.0(vue@3.5.17(typescript@4.9.5)) version: 2.57.0(vue@3.5.17(typescript@4.9.5))
'@vueuse/core': '@vueuse/core':
specifier: ^9.3.0 specifier: ^9.3.0
@ -115,7 +115,7 @@ importers:
version: 8.57.1 version: 8.57.1
eslint-config-airbnb-base: eslint-config-airbnb-base:
specifier: ^15.0.0 specifier: ^15.0.0
version: 15.0.0(eslint-plugin-import@2.32.0)(eslint@8.57.1) version: 15.0.0(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1))(eslint@8.57.1)
eslint-config-prettier: eslint-config-prettier:
specifier: ^8.5.0 specifier: ^8.5.0
version: 8.10.0(eslint@8.57.1) version: 8.10.0(eslint@8.57.1)
@ -770,41 +770,49 @@ packages:
resolution: {integrity: sha512-UYA0MA8ajkEDCFRQdng/FVx3F6szBvk3EPnkTTQuuO9lV1kPGuTB+V9TmbDxy5ikaEgyWKxa4CI3ySjklZ9lFA==} resolution: {integrity: sha512-UYA0MA8ajkEDCFRQdng/FVx3F6szBvk3EPnkTTQuuO9lV1kPGuTB+V9TmbDxy5ikaEgyWKxa4CI3ySjklZ9lFA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.9.2': '@unrs/resolver-binding-linux-arm64-musl@1.9.2':
resolution: {integrity: sha512-P/CO3ODU9YJIHFqAkHbquKtFst0COxdphc8TKGL5yCX75GOiVpGqd1d15ahpqu8xXVsqP4MGFP2C3LRZnnL5MA==} resolution: {integrity: sha512-P/CO3ODU9YJIHFqAkHbquKtFst0COxdphc8TKGL5yCX75GOiVpGqd1d15ahpqu8xXVsqP4MGFP2C3LRZnnL5MA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.9.2': '@unrs/resolver-binding-linux-ppc64-gnu@1.9.2':
resolution: {integrity: sha512-uKStFlOELBxBum2s1hODPtgJhY4NxYJE9pAeyBgNEzHgTqTiVBPjfTlPFJkfxyTjQEuxZbbJlJnMCrRgD7ubzw==} resolution: {integrity: sha512-uKStFlOELBxBum2s1hODPtgJhY4NxYJE9pAeyBgNEzHgTqTiVBPjfTlPFJkfxyTjQEuxZbbJlJnMCrRgD7ubzw==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.9.2': '@unrs/resolver-binding-linux-riscv64-gnu@1.9.2':
resolution: {integrity: sha512-LkbNnZlhINfY9gK30AHs26IIVEZ9PEl9qOScYdmY2o81imJYI4IMnJiW0vJVtXaDHvBvxeAgEy5CflwJFIl3tQ==} resolution: {integrity: sha512-LkbNnZlhINfY9gK30AHs26IIVEZ9PEl9qOScYdmY2o81imJYI4IMnJiW0vJVtXaDHvBvxeAgEy5CflwJFIl3tQ==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.9.2': '@unrs/resolver-binding-linux-riscv64-musl@1.9.2':
resolution: {integrity: sha512-vI+e6FzLyZHSLFNomPi+nT+qUWN4YSj8pFtQZSFTtmgFoxqB6NyjxSjAxEC1m93qn6hUXhIsh8WMp+fGgxCoRg==} resolution: {integrity: sha512-vI+e6FzLyZHSLFNomPi+nT+qUWN4YSj8pFtQZSFTtmgFoxqB6NyjxSjAxEC1m93qn6hUXhIsh8WMp+fGgxCoRg==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.9.2': '@unrs/resolver-binding-linux-s390x-gnu@1.9.2':
resolution: {integrity: sha512-sSO4AlAYhSM2RAzBsRpahcJB1msc6uYLAtP6pesPbZtptF8OU/CbCPhSRW6cnYOGuVmEmWVW5xVboAqCnWTeHQ==} resolution: {integrity: sha512-sSO4AlAYhSM2RAzBsRpahcJB1msc6uYLAtP6pesPbZtptF8OU/CbCPhSRW6cnYOGuVmEmWVW5xVboAqCnWTeHQ==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.9.2': '@unrs/resolver-binding-linux-x64-gnu@1.9.2':
resolution: {integrity: sha512-jkSkwch0uPFva20Mdu8orbQjv2A3G88NExTN2oPTI1AJ+7mZfYW3cDCTyoH6OnctBKbBVeJCEqh0U02lTkqD5w==} resolution: {integrity: sha512-jkSkwch0uPFva20Mdu8orbQjv2A3G88NExTN2oPTI1AJ+7mZfYW3cDCTyoH6OnctBKbBVeJCEqh0U02lTkqD5w==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.9.2': '@unrs/resolver-binding-linux-x64-musl@1.9.2':
resolution: {integrity: sha512-Uk64NoiTpQbkpl+bXsbeyOPRpUoMdcUqa+hDC1KhMW7aN1lfW8PBlBH4mJ3n3Y47dYE8qi0XTxy1mBACruYBaw==} resolution: {integrity: sha512-Uk64NoiTpQbkpl+bXsbeyOPRpUoMdcUqa+hDC1KhMW7aN1lfW8PBlBH4mJ3n3Y47dYE8qi0XTxy1mBACruYBaw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.9.2': '@unrs/resolver-binding-wasm32-wasi@1.9.2':
resolution: {integrity: sha512-EpBGwkcjDicjR/ybC0g8wO5adPNdVuMrNalVgYcWi+gYtC1XYNuxe3rufcO7dA76OHGeVabcO6cSkPJKVcbCXQ==} resolution: {integrity: sha512-EpBGwkcjDicjR/ybC0g8wO5adPNdVuMrNalVgYcWi+gYtC1XYNuxe3rufcO7dA76OHGeVabcO6cSkPJKVcbCXQ==}
@ -4753,7 +4761,7 @@ snapshots:
'@types/node': 20.5.1 '@types/node': 20.5.1
chalk: 4.1.2 chalk: 4.1.2
cosmiconfig: 8.3.6(typescript@4.9.5) cosmiconfig: 8.3.6(typescript@4.9.5)
cosmiconfig-typescript-loader: 4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@4.9.5))(ts-node@10.9.2(@types/node@24.0.3)(typescript@4.9.5))(typescript@4.9.5) cosmiconfig-typescript-loader: 4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@4.9.5))(ts-node@10.9.2(@types/node@20.5.1)(typescript@4.9.5))(typescript@4.9.5)
lodash.isplainobject: 4.0.6 lodash.isplainobject: 4.0.6
lodash.merge: 4.6.2 lodash.merge: 4.6.2
lodash.uniq: 4.5.0 lodash.uniq: 4.5.0
@ -5839,7 +5847,7 @@ snapshots:
core-util-is@1.0.3: {} core-util-is@1.0.3: {}
cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@4.9.5))(ts-node@10.9.2(@types/node@24.0.3)(typescript@4.9.5))(typescript@4.9.5): cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@4.9.5))(ts-node@10.9.2(@types/node@20.5.1)(typescript@4.9.5))(typescript@4.9.5):
dependencies: dependencies:
'@types/node': 20.5.1 '@types/node': 20.5.1
cosmiconfig: 8.3.6(typescript@4.9.5) cosmiconfig: 8.3.6(typescript@4.9.5)
@ -6424,7 +6432,7 @@ snapshots:
escape-string-regexp@5.0.0: {} escape-string-regexp@5.0.0: {}
eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.32.0)(eslint@8.57.1): eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1))(eslint@8.57.1):
dependencies: dependencies:
confusing-browser-globals: 1.0.11 confusing-browser-globals: 1.0.11
eslint: 8.57.1 eslint: 8.57.1
@ -6460,7 +6468,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1))(eslint@8.57.1):
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
@ -6482,7 +6490,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1))(eslint@8.57.1)
hasown: 2.0.2 hasown: 2.0.2
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3

View file

@ -13,7 +13,18 @@ export interface DepartmentIdData {
departmentId: number | string; departmentId: number | string;
} }
type DepartmentFormRes = HttpResponse<DepartmentIdData>; type DepartmentFormRes = HttpResponse<DepartmentIdData>;
// 创建部门
export function createDepartment(
params: DepartmentFormParams
): Promise<DepartmentFormRes> {
return axios.post('/department/createDepartment', params);
}
// 更新(编辑)部门
export function updateDepartment(
params: DepartmentFormParams
): Promise<DepartmentFormRes> {
return axios.post('/department/updateDepartment', params);
}
export interface DepartmentItem { export interface DepartmentItem {
id: number; id: number;
systemTenantId: number; systemTenantId: number;
@ -41,22 +52,11 @@ export interface DepartmentDetailData {
} }
export type DepartmentDetailRes = HttpResponse<DepartmentDetailData>; export type DepartmentDetailRes = HttpResponse<DepartmentDetailData>;
// 创建部门
export function createDepartment(
params: DepartmentFormParams
): Promise<DepartmentFormRes> {
return axios.post('/department/createDepartment', params);
}
// 获取部门列表 // 获取部门列表
export function getDepartmentList(): Promise<DepartmentListRes> { export function getDepartmentList(): Promise<DepartmentListRes> {
return axios.get('/department/getDepartmentList'); return axios.get('/department/getDepartmentList');
} }
// 更新(编辑)部门
export function updateDepartment(
params: DepartmentFormParams
): Promise<DepartmentFormRes> {
return axios.post('/department/updateDepartment', params);
}
export function getDepartmentDetail(id: number): Promise<DepartmentDetailRes> { export function getDepartmentDetail(id: number): Promise<DepartmentDetailRes> {
return axios.get(`/department/getDepartment?id=${id}`); return axios.get(`/department/getDepartment?id=${id}`);
} }
@ -68,19 +68,73 @@ export interface UserItem {
email: string; // 邮箱 email: string; // 邮箱
tel: string; // 手机号 tel: string; // 手机号
status: 1 | 2; // 状态:1 启用,2 禁用 status: 1 | 2; // 状态:1 启用,2 禁用
role_id: number; // 角色id roleId: number; // 角色id
department_id: number; // 部门id departmentId: number; // 部门id
role_name: string; // 角色名称 roleName: string; // 角色名称
dept_name: string; // 部门名称 deptName: string; // 部门名称
lastLoginTime: string; // 最后登录时间 lastLoginTime: string; // 最后登录时间
} }
export type UserList = UserItem[]; export type UserList = UserItem[];
export type UserListRes = HttpResponse<UserList>; export type UserListRes = HttpResponse<UserList>;
export interface Pagination {
page: number;
page_size: number;
total?: number;
total_pages?: number;
}
export interface AllUserList {
page: Pagination;
user: UserListRes;
}
export type AllUserListRes = HttpResponse<AllUserList>;
// 后台-用户列表(除主管外所有用户) // 后台-用户列表(除主管外所有用户)
export function getAllUserList(params): Promise<UserListRes> { export function getAllUserList(params): Promise<AllUserListRes> {
return axios.post('/user/getAllUserList', params); return axios.post('/user/getAllUserList', params);
} }
// 后台-用户列表(所有用户)
export function getUserList(params): Promise<AllUserListRes> {
return axios.post('/user/getUserList', params);
}
export interface FormUserItem {
username: string; // 用户名
realname: string; // 姓名
tel: string | number; // 手机号
password: string; // 密码
repassword: string; // 确认密码
role_id: number | null; // 角色id
department_id: number | null; // 部门id
email: string; // 邮件
is_manager: 1 | 2; // 是否为主管:1是,2否
}
// 创建用户
export function createUser(params: FormUserItem): Promise<HttpResponse> {
return axios.post('/user/createUser', params);
}
// 更新用户信息
export function updateUser(params: FormUserItem): Promise<HttpResponse> {
return axios.post('/user/updateUser', params);
}
interface UserInfo {
user: FormUserItem;
}
export type UserInfoRES = HttpResponse<UserInfo>;
// 获取用户信息
export function getUserInfo(id: number): Promise<UserInfoRES> {
return axios.get(`/user/getUser?id=${id}`);
}
// 删除用户
export function delUser(id: number): Promise<HttpResponse> {
return axios.get(`/user/delUser?id=${id}`);
}
export interface DisableUserParam {
id: number;
status: number;
}
// 禁用启用用户
export function disableUser(params: DisableUserParam): Promise<HttpResponse> {
return axios.post('/user/disableUser', params);
}
// 部门-向上移动 // 部门-向上移动
export function departmentSetUp(id: number): Promise<HttpResponse> { export function departmentSetUp(id: number): Promise<HttpResponse> {
return axios.get(`/department/setUp?id=${id}`); return axios.get(`/department/setUp?id=${id}`);
@ -93,6 +147,46 @@ export function departmentSetDown(id: number): Promise<HttpResponse> {
export function delDepartment(id: number): Promise<HttpResponse> { export function delDepartment(id: number): Promise<HttpResponse> {
return axios.get(`/department/delDepartment?id=${id}`); return axios.get(`/department/delDepartment?id=${id}`);
} }
export interface RoleItem {
id: number; // 角色id
systemTenantId: number; // 租户名称
name: string; // 角色名称
createUserId?: number; // 创建人id
isDelete?: number; // 是否删除
remark?: string;
dataType: number; // 数据权限类型:1全部数据权限,2自定义数据权限,3仅本人数据
createdAt: string;
updatedAt: string;
}
// 角色列表
export interface RoleList {
rolesList: RoleItem[];
}
export type RoleListRes = HttpResponse<RoleList>;
// 角色列表
export function getRoleList(): Promise<RoleListRes> {
return axios.get('/role/getRoleList');
}
// 创建角色
export interface RoleFormParam {
id?: number; // 角色id
name: string; // 姓名
remark?: string; // 备注
}
export function createRole(params: RoleFormParam): Promise<HttpResponse> {
return axios.post('/role/createRole', params);
}
// 更新角色
export function updateRole(params: RoleFormParam): Promise<HttpResponse> {
return axios.post('/role/updateRole', params);
}
// 删除角色
export function delRole(id: number): Promise<HttpResponse> {
return axios.get(`/role/delRole?id=${id}`);
}
// // 项目信息 // // 项目信息
// export function getProjectDetail(id: string) { // export function getProjectDetail(id: string) {
// return axios.get<any>(`/project/getProject?id=${id}`); // return axios.get<any>(`/project/getProject?id=${id}`);

View file

@ -0,0 +1,236 @@
# DataTable 组件使用指南
## 概述
`DataTable` 是一个基于 Arco Design Vue 的 `a-table` 组件封装,提供了统一的分页配置和事件处理,可以在多个页面中复用。
## 特性
- 🚀 统一的分页配置
- 📊 支持自定义列配置
- 🔄 内置加载状态管理
- 🎯 支持排序和筛选
- 📱 响应式设计
- 🎨 可自定义样式
## 基础用法
### 1. 使用 DataTable 组件
```vue
<template>
<DataTable
:data="tableData"
:columns="columns"
:loading="loading"
:total="total"
:current="current"
:page-size="pageSize"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<!-- 自定义列插槽 -->
<template #status="{ record }">
<a-tag :color="record.status === 'active' ? 'green' : 'red'">
{{ record.status }}
</a-tag>
</template>
<template #operate="{ record }">
<a-space>
<a-button size="small">查看</a-button>
<a-button size="small" type="primary">编辑</a-button>
</a-space>
</template>
</DataTable>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import type { TableColumnData, TableData } from '@arco-design/web-vue';
import useDataTable from '@/composables/useDataTable';
// 使用 DataTable composable
const {
data: tableData,
columns,
loading,
total,
current,
pageSize,
setData,
setColumns,
setLoading,
setTotal,
setCurrent,
setPageSize,
} = useDataTable({
initialPage: 1,
initialPageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
});
// 列配置
const columnConfig: TableColumnData[] = [
{ title: '姓名', dataIndex: 'name', key: 'name' },
{ title: '年龄', dataIndex: 'age', key: 'age' },
{ title: '状态', dataIndex: 'status', key: 'status', slotName: 'status' },
{ title: '操作', key: 'operate', slotName: 'operate', width: 200 },
];
// 模拟数据
const mockData: TableData[] = [
{ id: 1, name: '张三', age: 25, status: 'active' },
{ id: 2, name: '李四', age: 30, status: 'inactive' },
];
onMounted(() => {
setColumns(columnConfig);
setData(mockData);
setTotal(mockData.length);
});
// 分页事件处理
const handlePageChange = (page: number) => {
setCurrent(page);
// 调用 API 获取数据
fetchData(page, pageSize.value);
};
const handlePageSizeChange = (pageSize: number) => {
setPageSize(pageSize);
// 调用 API 获取数据
fetchData(1, pageSize);
};
const fetchData = async (page: number, size: number) => {
setLoading(true);
try {
// 调用 API
// const { data, total } = await api.getList({ page, size });
// setData(data);
// setTotal(total);
} finally {
setLoading(false);
}
};
</script>
```
### 2. 高级配置
```vue
<template>
<DataTable
:data="tableData"
:columns="columns"
:loading="loading"
:total="total"
:current="current"
:page-size="pageSize"
:bordered="true"
:stripe="true"
:size="'small'"
:scroll="{ x: 1200, y: 400 }"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
@selection-change="handleSelectionChange"
@sorter-change="handleSorterChange"
@filter-change="handleFilterChange"
>
<!-- 插槽内容 -->
</DataTable>
</template>
```
## Props 说明
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| data | TableData[] | [] | 表格数据 |
| columns | TableColumnData[] | [] | 列配置 |
| loading | boolean | false | 加载状态 |
| rowKey | string | 'id' | 行键 |
| pagination | boolean \| PaginationProps | true | 分页配置 |
| bordered | boolean | false | 是否显示边框 |
| size | 'mini' \| 'small' \| 'medium' \| 'large' | 'medium' | 表格大小 |
| stripe | boolean | false | 是否显示斑马纹 |
| tableLayout | 'auto' \| 'fixed' | 'auto' | 表格布局 |
| scroll | object | - | 滚动配置 |
| current | number | 1 | 当前页码 |
| pageSize | number | 10 | 每页条数 |
| total | number | 0 | 总条数 |
| showTotal | boolean | true | 是否显示总数 |
| pageSizeOptions | number[] | [10, 20, 50, 100] | 每页条数选项 |
| showJumper | boolean | true | 是否显示快速跳转 |
| showPageSize | boolean | true | 是否显示每页条数选择器 |
## Events 说明
| 事件名 | 参数 | 说明 |
|--------|------|------|
| page-change | (page: number) | 页码变化 |
| page-size-change | (pageSize: number) | 每页条数变化 |
| selection-change | (selection: TableData[]) | 选择变化 |
| sorter-change | (sorter: any) | 排序变化 |
| filter-change | (filters: any) | 筛选变化 |
## useDataTable Composable
`useDataTable` 是一个用于管理表格状态的 composable,提供了以下功能:
### 返回值
| 属性 | 类型 | 说明 |
|------|------|------|
| data | TableData[] | 表格数据 |
| columns | TableColumnData[] | 列配置 |
| loading | boolean | 加载状态 |
| pagination | PaginationProps | 分页配置 |
| current | number | 当前页码 |
| pageSize | number | 每页条数 |
| total | number | 总条数 |
| setData | (data: TableData[]) => void | 设置数据 |
| setColumns | (columns: TableColumnData[]) => void | 设置列配置 |
| setLoading | (loading: boolean) => void | 设置加载状态 |
| setTotal | (total: number) => void | 设置总条数 |
| setCurrent | (current: number) => void | 设置当前页码 |
| setPageSize | (pageSize: number) => void | 设置每页条数 |
| resetPagination | () => void | 重置分页 |
### 配置选项
```typescript
interface UseDataTableOptions {
initialPage?: number; // 初始页码
initialPageSize?: number; // 初始每页条数
pageSizeOptions?: number[]; // 每页条数选项
showTotal?: boolean; // 是否显示总数
showJumper?: boolean; // 是否显示快速跳转
showPageSize?: boolean; // 是否显示每页条数选择器
rowKey?: string; // 行键
bordered?: boolean; // 是否显示边框
size?: 'mini' | 'small' | 'medium' | 'large'; // 表格大小
stripe?: boolean; // 是否显示斑马纹
tableLayout?: 'auto' | 'fixed'; // 表格布局
}
```
## 最佳实践
1. **统一使用 DataTable 组件**:在项目中统一使用 DataTable 组件,避免重复配置分页逻辑。
2. **使用 useDataTable composable**:利用 composable 管理表格状态,提高代码复用性。
3. **合理配置列插槽**:对于需要自定义渲染的列,使用插槽进行自定义。
4. **处理加载状态**:在数据请求时正确设置 loading 状态。
5. **分页事件处理**:在分页事件中调用 API 获取对应页的数据。
## 注意事项
1. 确保在 `main.ts` 中注册了 `DataTable` 组件。
2. 列配置中的 `slotName` 要与模板中的插槽名称一致。
3. 数据源需要包含 `rowKey` 指定的字段。
4. 分页事件处理中应该调用实际的 API 接口。

View file

@ -0,0 +1,175 @@
<template>
<a-table
:row-key="rowKey"
:loading="loading"
:pagination="paginationConfig"
:columns="columns"
:data="data"
:bordered="bordered"
:scroll="scroll"
:size="size"
:stripe="stripe"
:table-layout="tableLayout"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
@selection-change="handleSelectionChange"
@sorter-change="handleSorterChange"
@filter-change="handleFilterChange"
>
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData"></slot>
</template>
</a-table>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import type {
TableColumnData,
TableData,
PaginationProps,
} from '@arco-design/web-vue';
interface DataTableProps {
// 表格数据
data: TableData[];
// 列配置
columns: TableColumnData[];
// 加载状态
loading?: boolean;
// 行键
rowKey?: string;
// 分页配置
pagination?: boolean | PaginationProps;
// 是否显示边框
bordered?: boolean;
// 表格大小
size?: 'mini' | 'small' | 'medium' | 'large';
// 是否显示斑马纹
stripe?: boolean;
// 表格布局
tableLayout?: 'auto' | 'fixed';
// 滚动配置
scroll?: {
x?: number | string;
y?: number | string;
};
// 当前页码
current?: number;
// 每页条数
pageSize?: number;
// 总条数
total?: number;
// 是否显示总数
showTotal?: boolean;
// 每页条数选项
pageSizeOptions?: number[];
// 是否显示快速跳转
showJumper?: boolean;
// 是否显示每页条数选择器
showPageSize?: boolean;
}
interface SorterInfo {
field: string;
direction: 'ascend' | 'descend';
}
interface FilterInfo {
[key: string]: string[];
}
interface DataTableEmits {
(e: 'pageChange', page: number): void;
(e: 'pageSizeChange', pageSize: number): void;
(e: 'selectionChange', selection: TableData[]): void;
(e: 'sorterChange', sorter: SorterInfo): void;
(e: 'filterChange', filters: FilterInfo): void;
}
const props = withDefaults(defineProps<DataTableProps>(), {
loading: false,
rowKey: 'id',
bordered: false,
size: 'medium',
stripe: false,
tableLayout: 'auto',
current: 1,
pageSize: 10,
total: 0,
showTotal: true,
pageSizeOptions: () => [10, 20, 50, 100],
showJumper: true,
showPageSize: true,
});
const emit = defineEmits<DataTableEmits>();
// 内部分页状态
const internalCurrent = ref(props.current);
const internalPageSize = ref(props.pageSize);
// 监听外部传入的分页参数变化
watch(
() => props.current,
(newVal) => {
internalCurrent.value = newVal;
}
);
watch(
() => props.pageSize,
(newVal) => {
internalPageSize.value = newVal;
}
);
// 计算分页配置
const paginationConfig = computed(() => {
if (props.pagination === false) {
return false;
}
const defaultConfig: PaginationProps = {
current: internalCurrent.value,
pageSize: internalPageSize.value,
total: props.total,
showTotal: props.showTotal,
showJumper: props.showJumper,
showPageSize: props.showPageSize,
pageSizeOptions: props.pageSizeOptions,
};
if (typeof props.pagination === 'object') {
return { ...defaultConfig, ...props.pagination };
}
return defaultConfig;
});
// 事件处理函数
const handlePageChange = (page: number) => {
internalCurrent.value = page;
emit('pageChange', page);
};
const handlePageSizeChange = (pageSize: number) => {
internalPageSize.value = pageSize;
internalCurrent.value = 1; // 切换每页条数时重置到第一页
emit('pageSizeChange', pageSize);
};
const handleSelectionChange = (selection: TableData[]) => {
emit('selectionChange', selection);
};
const handleSorterChange = (sorter: SorterInfo) => {
emit('sorterChange', sorter);
};
const handleFilterChange = (filters: FilterInfo) => {
emit('filterChange', filters);
};
</script>
<style scoped></style>

View file

@ -11,6 +11,7 @@ import {
} from 'echarts/components'; } from 'echarts/components';
import Chart from './chart/index.vue'; import Chart from './chart/index.vue';
import Breadcrumb from './breadcrumb/index.vue'; import Breadcrumb from './breadcrumb/index.vue';
import DataTable from './common/DataTable.vue';
// Manually introduce ECharts modules to reduce packing size // Manually introduce ECharts modules to reduce packing size
@ -31,5 +32,6 @@ export default {
install(Vue: App) { install(Vue: App) {
Vue.component('Chart', Chart); Vue.component('Chart', Chart);
Vue.component('Breadcrumb', Breadcrumb); Vue.component('Breadcrumb', Breadcrumb);
Vue.component('DataTable', DataTable);
}, },
}; };

196
src/composables/README.md Normal file
View file

@ -0,0 +1,196 @@
# 分页处理 Composable 使用指南
## 概述
本项目提供了两个用于处理表格分页的 composable:
1. `usePagination` - 基础分页事件处理
2. `useTableWithPagination` - 完整的分页和数据获取集成
## usePagination
### 基础用法
```typescript
import usePagination from '@/composables/usePagination';
const { handlePageChange, handlePageSizeChange } = usePagination({
onPageChange: () => getUserListData(),
onPageSizeChange: () => getUserListData(),
resetToFirstPage: true,
});
// 在分页事件中使用
const onPageChange = (page: number) => {
handlePageChange(page, setCurrent);
};
const onPageSizeChange = (pageSize: number) => {
handlePageSizeChange(pageSize, setPageSize, setCurrent);
};
```
### 配置选项
| 选项 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| onPageChange | Function | - | 页码变化时的回调函数 |
| onPageSizeChange | Function | - | 每页条数变化时的回调函数 |
| resetToFirstPage | boolean | true | 切换每页条数时是否重置到第一页 |
## useTableWithPagination
### 完整用法
```typescript
import useTableWithPagination from '@/composables/useTableWithPagination';
const {
data: renderData,
columns: projectColumn,
loading,
total,
current,
pageSize,
setColumns,
onPageChange,
onPageSizeChange,
refresh,
updateParams,
} = useTableWithPagination({
// 数据获取函数
fetchData: async (params) => {
const { code, data } = await getUserList({
dept_id: selectedKeys.value[0],
keyword: searchKeyWord.value,
page: params.page,
page_size: params.pageSize,
});
if (code === 200) {
const { page, user } = data;
return {
data: Array.isArray(user) ? user : [],
total: typeof page.total === 'number' ? page.total : 0,
};
}
return { data: [], total: 0 };
},
initialPage: 1,
initialPageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
autoFetch: false, // 不自动获取,等选中部门后再获取
});
```
### 配置选项
| 选项 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| fetchData | Function | - | 数据获取函数,必须返回 { data, total } |
| initialPage | number | 1 | 初始页码 |
| initialPageSize | number | 10 | 初始每页条数 |
| pageSizeOptions | number[] | [10, 20, 50, 100] | 每页条数选项 |
| extraParams | object | {} | 额外的查询参数 |
| autoFetch | boolean | true | 是否自动获取数据 |
| resetToFirstPage | boolean | true | 切换每页条数时是否重置到第一页 |
### 返回值
| 属性 | 类型 | 说明 |
|------|------|------|
| data | Ref<TableData[]> | 表格数据 |
| columns | Ref<TableColumnData[]> | 列配置 |
| loading | Ref<boolean> | 加载状态 |
| total | Ref<number> | 总条数 |
| current | Ref<number> | 当前页码 |
| pageSize | Ref<number> | 每页条数 |
| setColumns | Function | 设置列配置 |
| onPageChange | Function | 页码变化处理 |
| onPageSizeChange | Function | 每页条数变化处理 |
| refresh | Function | 刷新数据 |
| updateParams | Function | 更新查询参数 |
## 最佳实践
### 1. 使用 usePagination(推荐用于简单场景)
```typescript
// 在现有的 useDataTable 基础上添加分页处理
const { handlePageChange, handlePageSizeChange } = usePagination({
onPageChange: () => getUserListData(),
onPageSizeChange: () => getUserListData(),
});
const onPageChange = (page: number) => {
handlePageChange(page, setCurrent);
};
const onPageSizeChange = (pageSize: number) => {
handlePageSizeChange(pageSize, setPageSize, setCurrent);
};
```
### 2. 使用 useTableWithPagination(推荐用于新页面)
```typescript
// 完整的分页和数据获取集成
const {
data: renderData,
columns: projectColumn,
loading,
total,
current,
pageSize,
onPageChange,
onPageSizeChange,
refresh,
} = useTableWithPagination({
fetchData: async (params) => {
// 你的数据获取逻辑
return { data: [], total: 0 };
},
autoFetch: false,
});
// 搜索时刷新数据
const handleSearch = () => {
refresh();
};
// 删除后刷新数据
const handleDelete = async (id: number) => {
await deleteItem(id);
refresh();
};
```
### 3. 处理搜索和筛选
```typescript
// 使用 updateParams 更新查询参数
const handleSearch = () => {
updateParams({ keyword: searchKeyWord.value });
};
// 或者使用 refresh 直接刷新
const handleSearch = () => {
refresh();
};
```
## 优势
1. **代码复用**:避免在每个页面重复编写分页逻辑
2. **统一处理**:确保所有页面的分页行为一致
3. **类型安全**:完整的 TypeScript 类型支持
4. **易于维护**:分页逻辑集中管理,便于修改和扩展
5. **自动优化**:自动处理页码重置、加载状态等细节
## 注意事项
1. `fetchData` 函数必须返回 `{ data, total }` 格式
2. 使用 `useTableWithPagination` 时,确保 `fetchData` 函数能正确处理错误
3. 如果需要自定义分页行为,可以使用 `usePagination` 进行更精细的控制
4. `autoFetch: false` 适用于需要等待用户操作后再获取数据的场景

View file

@ -0,0 +1,172 @@
import { ref, reactive, computed, Ref, ComputedRef } from 'vue';
import type {
TableColumnData,
TableData,
PaginationProps,
} from '@arco-design/web-vue';
interface UseDataTableOptions {
// 初始页码
initialPage?: number;
// 初始每页条数
initialPageSize?: number;
// 每页条数选项
pageSizeOptions?: number[];
// 是否显示总数
showTotal?: boolean;
// 是否显示快速跳转
showJumper?: boolean;
// 是否显示每页条数选择器
showPageSize?: boolean;
// 行键
rowKey?: string;
// 是否显示边框
bordered?: boolean;
// 表格大小
size?: 'mini' | 'small' | 'medium' | 'large';
// 是否显示斑马纹
stripe?: boolean;
// 表格布局
tableLayout?: 'auto' | 'fixed';
}
interface UseDataTableReturn {
// 表格数据
data: Ref<TableData[]>;
// 列配置
columns: Ref<TableColumnData[]>;
// 加载状态
loading: Ref<boolean>;
// 分页配置
pagination: ComputedRef<PaginationProps>;
// 当前页码
current: Ref<number>;
// 每页条数
pageSize: Ref<number>;
// 总条数
total: Ref<number>;
// 设置数据
setData: (data: TableData[]) => void;
// 设置列配置
setColumns: (columns: TableColumnData[]) => void;
// 设置加载状态
setLoading: (loading: boolean) => void;
// 设置总条数
setTotal: (total: number) => void;
// 设置当前页码
setCurrent: (current: number) => void;
// 设置每页条数
setPageSize: (pageSize: number) => void;
// 重置分页
resetPagination: () => void;
// 表格配置
tableConfig: {
rowKey: string;
bordered: boolean;
size: 'mini' | 'small' | 'medium' | 'large';
stripe: boolean;
tableLayout: 'auto' | 'fixed';
};
}
export default function useDataTable(
options: UseDataTableOptions = {}
): UseDataTableReturn {
const {
initialPage = 1,
initialPageSize = 10,
pageSizeOptions = [10, 20, 50, 100],
showTotal = true,
showJumper = true,
showPageSize = true,
rowKey = 'id',
bordered = false,
size = 'medium',
stripe = false,
tableLayout = 'auto',
} = options;
// 响应式数据
const data = ref<TableData[]>([]);
const columns = ref<TableColumnData[]>([]);
const loading = ref(false);
const current = ref(initialPage);
const pageSize = ref(initialPageSize);
const total = ref(0);
// 分页配置
const pagination = computed<PaginationProps>(() => ({
current: current.value,
pageSize: pageSize.value,
total: total.value,
showTotal,
showJumper,
showPageSize,
pageSizeOptions,
}));
// 表格配置
const tableConfig = reactive({
rowKey,
bordered,
size,
stripe,
tableLayout,
});
// 设置数据
const setData = (newData: TableData[]) => {
data.value = newData;
};
// 设置列配置
const setColumns = (newColumns: TableColumnData[]) => {
columns.value = newColumns;
};
// 设置加载状态
const setLoading = (newLoading: boolean) => {
loading.value = newLoading;
};
// 设置总条数
const setTotal = (newTotal: number) => {
total.value = newTotal;
};
// 设置当前页码
const setCurrent = (newCurrent: number) => {
current.value = newCurrent;
};
// 设置每页条数
const setPageSize = (newPageSize: number) => {
pageSize.value = newPageSize;
// 切换每页条数时重置到第一页
current.value = 1;
};
// 重置分页
const resetPagination = () => {
current.value = initialPage;
pageSize.value = initialPageSize;
};
return {
data,
columns,
loading,
pagination,
current,
pageSize,
total,
setData,
setColumns,
setLoading,
setTotal,
setCurrent,
setPageSize,
resetPagination,
tableConfig,
};
}

View file

@ -4,6 +4,7 @@ import {
DepartmentItem, DepartmentItem,
getDepartmentDetail, getDepartmentDetail,
} from '@/api/setting'; } from '@/api/setting';
import useLoading from '@/hooks/loading';
export default function useDepartmentList(autoFetch = true) { export default function useDepartmentList(autoFetch = true) {
const departmentListData = ref<DepartmentItem[]>([]); const departmentListData = ref<DepartmentItem[]>([]);

View file

@ -35,7 +35,10 @@ export default {
'menu.finance': '财务', 'menu.finance': '财务',
'menu.property': '物业', 'menu.property': '物业',
'menu.setting': '设置', 'menu.setting': '设置',
'search.placeholder': '请输入内容', 'please.enter': '请输入{placeholder}',
'please.select': '请选择{placeholder}',
'please.confirm': '请确认{placeholder}',
'content': '内容',
'table.filter': '筛选', 'table.filter': '筛选',
'table.export': '导出', 'table.export': '导出',
'watch-detail': '查看详情', 'watch-detail': '查看详情',
@ -61,6 +64,7 @@ export default {
'company-info': '公司信息', 'company-info': '公司信息',
'appearance-setting': '外观设置', 'appearance-setting': '外观设置',
'add-department': '新建部门', 'add-department': '新建部门',
'edit-department': '编辑部门',
'department-name': '部门名称', 'department-name': '部门名称',
'belongto-department': '所属部门', 'belongto-department': '所属部门',
'department-head': '部门主管', 'department-head': '部门主管',
@ -73,6 +77,26 @@ export default {
'delete': '删除', 'delete': '删除',
'confirm': '确定', 'confirm': '确定',
'cancel': '取消', 'cancel': '取消',
'search.username.name': '搜索用户名、姓名',
'add-user': '新建用户',
'edit-user': '编辑用户',
'username': '用户名',
'name': '姓名',
'role': '角色',
'status': '状态',
'last-login': '最后登录',
'operate': '操作',
'password': '密码',
'confirm.password': '确认密码',
'tel': '手机号',
'email': '邮箱',
'lastLogin': '最后登录',
'please.enter.correct': '请输入正确的{field}',
'enable': '启用',
'disable': '禁用',
'edit': '编辑',
'more': '更多',
'user': '用户',
...localeSettings, ...localeSettings,
...localeMessageBox, ...localeMessageBox,

View file

@ -4,43 +4,46 @@
<a-card class="general-card"> <a-card class="general-card">
<template #title> <template #title>
<div class="card-title_between"> <div class="card-title_between">
<p class="title">{{ $t('menu.business.project') }}</p> <p class="title">{{ t('menu.business.project') }}</p>
<div class="flex_center_between"> <div class="flex_center_between">
<a-space> <a-space>
<a-input-search <a-input-search
:style="{ width: '320px' }" :style="{ width: '320px' }"
:placeholder="$t('search.placeholder')" :placeholder="t('please.enter', { placeholder: t('content') })"
/> />
<a-button> <a-button>
<template #icon> <template #icon>
<icon-filter /> <icon-filter />
</template> </template>
{{ $t('table.filter') }} {{ t('table.filter') }}
</a-button> </a-button>
<a-button> <a-button>
<template #icon> <template #icon>
<icon-download /> <icon-download />
</template> </template>
{{ $t('table.export') }} {{ t('table.export') }}
</a-button> </a-button>
<a-button type="primary"> <a-button type="primary">
<template #icon> <template #icon>
<icon-plus /> <icon-plus />
</template> </template>
{{ $t('add-project') }} {{ t('add-project') }}
</a-button> </a-button>
</a-space> </a-space>
</div> </div>
</div> </div>
</template> </template>
<a-table
row-key="id" <!-- 使用新的 DataTable 组件 -->
:loading="loading" <DataTable
:pagination="pagination"
:columns="projectColumn"
:data="renderData" :data="renderData"
:bordered="false" :columns="projectColumn"
:loading="loading"
:total="total"
:current="current"
:page-size="pageSize"
@page-change="onPageChange" @page-change="onPageChange"
@page-size-change="onPageSizeChange"
> >
<template #type="{ record }"> <template #type="{ record }">
<a-tag checkable :color="getTypeColor(record.type)">{{ <a-tag checkable :color="getTypeColor(record.type)">{{
@ -61,15 +64,16 @@
<a-button>更多</a-button> <a-button>更多</a-button>
</a-space> </a-space>
</template> </template>
</a-table> </DataTable>
</a-card> </a-card>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, reactive, watch, nextTick } from 'vue'; import { onMounted } from 'vue';
import useLoading from '@/hooks/loading'; import useLoading from '@/hooks/loading';
import { Pagination } from '@/types/global'; import { useI18n } from 'vue-i18n';
import type { TableColumnData } from '@arco-design/web-vue';
import { import {
ProjectRecord, ProjectRecord,
ProjectType, ProjectType,
@ -77,11 +81,35 @@
PROJECT_TYPE_COLORS, PROJECT_TYPE_COLORS,
PROJECT_TYPE_LABELS, PROJECT_TYPE_LABELS,
} from '@/types/business'; } from '@/types/business';
import { ProjectParams, getAllProjectList } from '@/api/business'; // import { getAllProjectList } from '@/api/business';
import useDataTable from '@/composables/useDataTable';
const { loading, setLoading } = useLoading(false); const { t } = useI18n();
const renderData = ref<ProjectRecord[]>([ const { loading } = useLoading(false);
// setLoading
// 使用 DataTable composable
const {
data: renderData,
columns: projectColumn,
total,
current,
pageSize,
setData,
setColumns,
setTotal,
setCurrent,
setPageSize,
} = useDataTable({
initialPage: 1,
initialPageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
});
// 模拟数据
const mockData: ProjectRecord[] = [
{ {
id: '1',
name: '星河商务中心', name: '星河商务中心',
type: PROJECT_TYPE.COMMERCIAL_COMPLEX, type: PROJECT_TYPE.COMMERCIAL_COMPLEX,
size: '1200m²', size: '1200m²',
@ -91,6 +119,7 @@
create_time: '2025-06-24 00:51', create_time: '2025-06-24 00:51',
}, },
{ {
id: '2',
name: '南山科学科学院', name: '南山科学科学院',
type: PROJECT_TYPE.OFFICE_BUILDING, type: PROJECT_TYPE.OFFICE_BUILDING,
size: '120m²', size: '120m²',
@ -100,6 +129,7 @@
create_time: '2025-06-24 00:58', create_time: '2025-06-24 00:58',
}, },
{ {
id: '3',
name: '前海科学科学院', name: '前海科学科学院',
type: PROJECT_TYPE.INDUSTRIAL_PARK, type: PROJECT_TYPE.INDUSTRIAL_PARK,
size: '1520m²', size: '1520m²',
@ -108,24 +138,25 @@
count: '120', count: '120',
create_time: '2025-06-24 01:16', create_time: '2025-06-24 01:16',
}, },
]);
const basePagination: Pagination = {
current: 1,
pageSize: 20,
total: 3,
};
const projectColumn = [
{ title: '项目信息', dataIndex: 'name' },
{ title: '项目类型', dataIndex: 'type', slotName: 'type' },
{ title: '管理面积', dataIndex: 'size' },
{ title: '可租面积', dataIndex: 'kezu' },
{ title: '出租率', dataIndex: 'rate', slotName: 'rate' },
{ title: '房源数量', dataIndex: 'count' },
{ title: '创建时间', dataIndex: 'create_time' },
{ title: '操作', dataIndex: 'operate', slotName: 'operate' },
]; ];
const pagination = reactive({
...basePagination, // 列配置
const columns: TableColumnData[] = [
{ title: '项目信息', dataIndex: 'name', key: 'name' },
{ title: '项目类型', dataIndex: 'type', key: 'type', slotName: 'type' },
{ title: '管理面积', dataIndex: 'size', key: 'size' },
{ title: '可租面积', dataIndex: 'kezu', key: 'kezu' },
{ title: '出租率', dataIndex: 'rate', key: 'rate', slotName: 'rate' },
{ title: '房源数量', dataIndex: 'count', key: 'count' },
{ title: '创建时间', dataIndex: 'create_time', key: 'create_time' },
{ title: '操作', key: 'operate', slotName: 'operate', width: 200 },
];
// 初始化数据
onMounted(() => {
setColumns(columns);
setData(mockData);
setTotal(mockData.length);
}); });
const getTypeColor = (type: ProjectType): string => { const getTypeColor = (type: ProjectType): string => {
@ -135,21 +166,28 @@
const getTypeText = (type: ProjectType): string => { const getTypeText = (type: ProjectType): string => {
return PROJECT_TYPE_LABELS[type] || '未知'; return PROJECT_TYPE_LABELS[type] || '未知';
}; };
const onPageChange = (current: number) => {
pagination.current = current; // 分页事件处理
const onPageChange = (page: number) => {
setCurrent(page);
// TODO: 这里应该调用API获取对应页码的数据 // TODO: 这里应该调用API获取对应页码的数据
}; };
const getAllProjectListData = async () => { const onPageSizeChange = (newPageSize: number) => {
setLoading(true); setPageSize(newPageSize);
try { // TODO: 这里应该调用API获取对应页码的数据
const { data } = await getAllProjectList();
console.log(data, 'getAllProjectList');
} catch (err) {
// you can report use errorHandler or other
} finally {
setLoading(false);
}
}; };
// const getAllProjectListData = async () => {
// setLoading(true);
// try {
// await getAllProjectList();
// // 处理数据
// } catch (err) {
// // you can report use errorHandler or other
// } finally {
// setLoading(false);
// }
// };
// getAllProjectListData() // getAllProjectListData()
</script> </script>

View file

@ -1,7 +1,7 @@
<template> <template>
<a-modal <a-modal
:visible="visible" :visible="visible"
:title="isEdit ? '编辑部门' : '新增部门'" :title="isEdit ? $t('edit-department') : $t('add-department')"
unmount-on-close unmount-on-close
@ok="handleSubmit" @ok="handleSubmit"
@cancel="handleCancel" @cancel="handleCancel"
@ -14,36 +14,48 @@
layout="vertical" layout="vertical"
> >
<!-- 部门名称 --> <!-- 部门名称 -->
<a-form-item :label="t('department-name')" field="name" required> <a-form-item :label="$t('department-name')" field="name" required>
<a-input v-model="form.name" placeholder="请输入部门名称" /> <a-input
v-model="form.name"
:placeholder="
$t('please.enter', { placeholder: $t('department-name') })
"
/>
</a-form-item> </a-form-item>
<!-- 所属部门 --> <!-- 所属部门 -->
<a-form-item :label="t('belongto-department')" field="parent_id" required> <a-form-item
:label="$t('belongto-department')"
field="parent_id"
required
>
<a-tree-select <a-tree-select
v-model="form.parent_id" v-model="form.parent_id"
:data="departmentListData" :data="departmentListData"
:field-names="{ key: 'id', title: 'name', children: 'children' }" :field-names="{ key: 'id', title: 'name', children: 'children' }"
placeholder="请选择所属部门" :placeholder="
$t('please.select', { placeholder: $t('belongto-department') })
"
allow-clear allow-clear
/> />
</a-form-item> </a-form-item>
<!-- 部门主管 --> <!-- 部门主管 -->
<a-form-item :label="t('department-head')" field="user_id"> <a-form-item :label="$t('department-head')" field="user_id">
<a-select <a-select
v-model="form.user_id" v-model="form.user_id"
:options="props.userListData" :options="props.userListData"
placeholder="请选择部门主管" :placeholder="
$t('please.select', { placeholder: $t('department-head') })
"
allow-clear allow-clear
/> />
</a-form-item> </a-form-item>
<!-- 备注 --> <!-- 备注 -->
<a-form-item :label="t('remark')" field="remark"> <a-form-item :label="$t('remark')" field="remark">
<a-textarea <a-textarea
v-model="form.remark" v-model="form.remark"
placeholder="请输入备注" :placeholder="$t('please.enter', { placeholder: $t('remark') })"
allow-clear allow-clear
/> />
</a-form-item> </a-form-item>
@ -75,17 +87,26 @@
user_id?: number; user_id?: number;
remark?: string; remark?: string;
} }
interface TreeNode {
id: string | number;
name: string;
children?: TreeNode[];
}
const props = defineProps<{ const props = defineProps<{
visible: boolean; visible: boolean;
isEdit: boolean; isEdit: boolean;
record?: DepartmentItem | null; record?: DepartmentItem | null;
departmentListData: any[]; // 新增 departmentListData: TreeNode[]; // 修复类型
userListData: UserList; userListData: UserList;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:visible', val: boolean): void; (e: 'update:visible', val: boolean): void;
(e: 'submitted'): void; (e: 'submitted'): void;
}>(); }>();
// 表单数据 // 表单数据
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const form = reactive<DepartmentForm>({ const form = reactive<DepartmentForm>({
@ -99,7 +120,12 @@
const { t } = useI18n(); const { t } = useI18n();
// 校验规则 // 校验规则
const rules = { const rules = {
name: [{ required: true, message: '请输入部门名称' }], name: [
{
required: true,
message: t('please.enter', { placeholder: t('department') }),
},
],
parent_id: [{ required: true, message: '请选择所属部门' }], parent_id: [{ required: true, message: '请选择所属部门' }],
}; };

View file

@ -33,8 +33,9 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { import {
IconSettings, IconSettings,
IconSafe, IconSafe,
@ -45,7 +46,23 @@
import organization from './organization.vue'; import organization from './organization.vue';
// import { CreateDepartmentParams, createDepartment, getDepartmentList, DepartmentItem } from '@/api/setting' // import { CreateDepartmentParams, createDepartment, getDepartmentList, DepartmentItem } from '@/api/setting'
const { t } = useI18n(); const { t } = useI18n();
const currentActive = ref(4); const route = useRoute();
const currentActive = ref(1);
// 监听路由query参数中的active参数
watch(
() => route.query.active,
(newActive) => {
if (newActive) {
const activeValue = parseInt(newActive as string, 10);
if (!Number.isNaN(activeValue)) {
currentActive.value = activeValue;
}
}
},
{ immediate: true }
);
const systemSettingList = [ const systemSettingList = [
{ id: 1, name: t('general-setting'), icon: IconSettings }, { id: 1, name: t('general-setting'), icon: IconSettings },
{ id: 2, name: t('security-setting'), icon: IconSafe }, { id: 2, name: t('security-setting'), icon: IconSafe },
@ -56,6 +73,8 @@
const handleChooseSystemItem = (id: number) => { const handleChooseSystemItem = (id: number) => {
currentActive.value = id; currentActive.value = id;
}; };
// watch()
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View file

@ -93,9 +93,9 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref, watch, onMounted } from 'vue'; import { ref, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import useDepartmentList from '@/views/setting/composables/useDepartmentList'; import useDepartmentList from '@/composables/useDepartmentList';
import { import {
DepartmentItem, DepartmentItem,
getAllUserList, getAllUserList,
@ -114,9 +114,10 @@
getDepartmentListData, getDepartmentListData,
getDepartmentDetailData, getDepartmentDetailData,
} = useDepartmentList(); } = useDepartmentList();
const emit = defineEmits(['onSelectTree']);
const expandedKeys = ref<(string | number)[]>([]); const expandedKeys = ref<(string | number)[]>([]);
const { t } = useI18n(); const { t } = useI18n();
watch( watch(
allDepartmentKeys, allDepartmentKeys,
(val) => { (val) => {
@ -129,7 +130,14 @@
const isEdit = ref<boolean>(false); const isEdit = ref<boolean>(false);
const departmentDetailInfo = ref<DepartmentItem | null>(null); const departmentDetailInfo = ref<DepartmentItem | null>(null);
const resetEditIcon = (nodes: any[]) => { interface TreeNode {
id: string | number;
isShowEditIcon?: boolean;
children?: TreeNode[];
level?: number;
}
const resetEditIcon = (nodes: TreeNode[]) => {
nodes.forEach((node) => { nodes.forEach((node) => {
node.isShowEditIcon = false; node.isShowEditIcon = false;
if (node.children && node.children.length > 0) { if (node.children && node.children.length > 0) {
@ -137,8 +145,11 @@
} }
}); });
}; };
const currentNode = ref<any>(null);
const onSelect = debounce(async (_: (string | number)[], event: any) => { const currentNode = ref<TreeNode | null>(null);
const onSelect = debounce(
async (_: (string | number)[], event: { node: TreeNode }) => {
const { node } = event; const { node } = event;
if (node.id === currentNode.value?.id) return; if (node.id === currentNode.value?.id) return;
currentNode.value = node; currentNode.value = node;
@ -149,9 +160,11 @@
node.isShowEditIcon = true; node.isShowEditIcon = true;
} }
departmentDetailInfo.value = await getDepartmentDetailData( departmentDetailInfo.value = await getDepartmentDetailData(
currentNode.value.id Number(currentNode.value.id)
);
},
300
); );
}, 300);
// 所有用户数据 // 所有用户数据
const allUserList = ref<UserList>([]); const allUserList = ref<UserList>([]);
@ -162,39 +175,43 @@
const { code, data } = await getAllUserList({ const { code, data } = await getAllUserList({
keyword: userKeyword.value, keyword: userKeyword.value,
}); });
if (code === 200 && data?.length) { if (code === 200) {
allUserList.value = data; const { user } = data;
allUserList.value = Array.isArray(user) ? user : [];
} }
} catch (error) { } catch (error) {
console.error('获取所以用户数据失败:', error); // 处理错误
} }
}; };
const handleAdd = () => { const handleAdd = () => {
showModal.value = true;
isEdit.value = false; isEdit.value = false;
departmentDetailInfo.value = null;
showModal.value = true;
}; };
const handleEdit = () => { const handleEdit = () => {
showModal.value = true; showModal.value = true;
isEdit.value = true; isEdit.value = true;
}; };
const handleDelete = async (item) => {
const handleDelete = async (item: { id: string | number }) => {
const { id } = item; const { id } = item;
try { try {
const { code, message } = await delDepartment(id); const { code, message } = await delDepartment(Number(id));
if (code === 200) { if (code === 200) {
Message.success(message); Message.success(message);
getDepartmentListData(); getDepartmentListData();
} }
} catch (error) { } catch (error) {
console.log('删除失败:', error); // 处理错误
} }
}; };
const handleMoveUp = async (item) => { const handleMoveUp = async (item: { id: string | number }) => {
const { id } = item; const { id } = item;
try { try {
const { code, message } = await departmentSetUp(id); const { code, message } = await departmentSetUp(Number(id));
if (code === 200) { if (code === 200) {
Message.success(message); Message.success(message);
getDepartmentListData(); getDepartmentListData();
@ -202,13 +219,14 @@
Message.info(message); Message.info(message);
} }
} catch (error) { } catch (error) {
console.log('向上移动失败:', error); // 处理错误
} }
}; };
const handleMoveDown = async (item) => {
const handleMoveDown = async (item: { id: string | number }) => {
const { id } = item; const { id } = item;
try { try {
const { code, message } = await departmentSetDown(id); const { code, message } = await departmentSetDown(Number(id));
if (code === 200) { if (code === 200) {
Message.success(message); Message.success(message);
getDepartmentListData(); getDepartmentListData();
@ -216,7 +234,7 @@
Message.info(message); Message.info(message);
} }
} catch (error) { } catch (error) {
console.log('向下移动失败:', error); // 处理错误
} }
}; };

View file

@ -0,0 +1,300 @@
<template>
<a-drawer
:width="340"
:visible="visible"
:title="isEdit ? $t('edit-user') : $t('add-user')"
unmount-on-close
@ok="handleOk"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
auto-label-width
layout="vertical"
>
<!-- 用户名 -->
<a-form-item :label="$t('username')" field="username" required>
<a-input
v-model="form.username"
:placeholder="$t('please.enter', { placeholder: $t('username') })"
/>
</a-form-item>
<!-- 姓名 -->
<a-form-item :label="$t('name')" field="realname" required>
<a-input
v-model="form.realname"
:placeholder="$t('please.enter', { placeholder: $t('name') })"
/>
</a-form-item>
<a-form-item field="password" :label="$t('password')" class="form-item">
<a-input-password
v-model="form.password"
:disabled="isEdit"
:placeholder="$t('please.enter', { placeholder: $t('password') })"
allow-clear
class="input-field"
>
</a-input-password>
</a-form-item>
<!-- 确认密码 -->
<a-form-item
field="repassword"
:label="$t('confirm.password')"
class="form-item"
>
<a-input-password
v-model="form.repassword"
:disabled="isEdit"
:placeholder="$t('please.confirm', { placeholder: $t('password') })"
allow-clear
class="input-field"
>
</a-input-password>
</a-form-item>
<!-- 手机号 -->
<a-form-item :label="$t('tel')" field="tel" required>
<a-input
v-model="form.tel"
:placeholder="$t('please.enter', { placeholder: $t('tel') })"
/>
</a-form-item>
<!-- 邮箱 -->
<a-form-item :label="$t('email')" field="email">
<a-input
v-model="form.email"
:placeholder="$t('please.enter', { placeholder: $t('email') })"
/>
</a-form-item>
<!-- 角色 -->
<a-form-item :label="$t('role')" field="role_id" required>
<a-select
v-model="form.role_id"
:options="props.roleList"
:field-names="{ value: 'id', label: 'name' }"
:placeholder="$t('please.select', { placeholder: $t('role') })"
/>
</a-form-item>
<!-- 部门主管 -->
<a-form-item field="department_id">
<template #label>
<div class="label_wrap">
<div class="left">
<span class="required">*</span>
{{ $t('department') }}
</div>
<div class="right">
<a-checkbox
:default-checked="form.is_manager === 1"
:checked="form.is_manager === 1"
@change="form.is_manager = $event ? 1 : 2"
>
{{ $t('department-head') }}
</a-checkbox>
<icon-exclamation-circle-fill />
</div>
</div>
</template>
<a-tree-select
v-model="form.department_id"
:data="props.departmentListData"
:field-names="{ key: 'id', title: 'name', children: 'children' }"
:placeholder="$t('please.select', { placeholder: $t('department') })"
/>
</a-form-item>
</a-form>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, watch, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import type { FormInstance } from '@arco-design/web-vue';
import {
DepartmentItem,
RoleItem,
FormUserItem,
createUser,
updateUser,
} from '@/api/setting';
const props = defineProps<{
visible: boolean;
isEdit: boolean;
record?: FormUserItem | null;
departmentListData?: DepartmentItem[];
roleList?: RoleItem[];
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'submitted'): void;
}>();
// 表单数据
const formRef = ref<FormInstance>();
const defaultForm: FormUserItem = {
username: '',
realname: '',
tel: '',
password: '',
repassword: '',
role_id: null,
department_id: null,
email: '',
is_manager: 1,
};
const form = reactive<FormUserItem>({ ...defaultForm });
const { t } = useI18n();
// 校验规则
const rules = reactive({
username: [
{
required: true,
message: t('please.enter', { placeholder: t('username') }),
},
],
realname: [
{
required: true,
message: t('please.enter', { placeholder: t('name') }),
},
],
password: [
{
required: true,
message: t('please.enter', { placeholder: t('password') }),
},
],
repassword: [
{
required: true,
message: t('please.confirm', { placeholder: t('password') }),
},
{
validator: (value, cb) => {
if (value !== form.password) {
cb('two passwords do not match');
} else {
cb();
}
},
},
],
tel: [
{ required: true, message: t('please.enter', { placeholder: t('tel') }) },
{
validator: (value, cb) => {
if (!/^1\d{10}$/.test(String(value))) {
cb(t('please.enter.correct', { field: t('tel') }));
} else {
cb();
}
},
},
],
email: [
{
validator: (value, cb) => {
if (!value) {
cb();
return;
}
const emailReg = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
if (!emailReg.test(value)) {
cb(t('please.enter.correct', { field: t('email') }));
} else {
cb();
}
},
},
],
role_id: [
{
required: true,
message: t('please.select', { placeholder: t('role') }),
},
],
department_id: [
{ message: t('please.select', { placeholder: t('department-head') }) },
],
});
watch(
() => props.visible,
(val) => {
if (val) {
if (props.isEdit && props.record) {
const record = props.record as FormUserItem & {
roleId?: number;
departmentId?: number;
isManager?: number;
};
Object.assign(form, {
...record,
});
form.role_id = record?.roleId ?? null;
form.department_id = record?.departmentId ?? null;
form.password = record?.password;
form.repassword = record?.password;
form.is_manager =
record?.isManager === 1 || record?.isManager === 2
? record.isManager
: 1;
} else {
Object.assign(form, defaultForm);
}
}
},
{ immediate: true }
);
const handleOk = async () => {
try {
const valid = await formRef.value.validate();
if (valid) return;
const params = { ...form };
if (props.isEdit) {
await updateUser(params);
} else {
await createUser(params);
}
emit('submitted');
emit('update:visible', false);
} catch (err) {
// 处理错误
}
};
const handleCancel = () => {
emit('update:visible', false);
};
</script>
<style lang="less" scoped>
:deep(label.arco-form-item-label) {
width: 100% !important;
}
.label_wrap {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
.left {
display: flex;
align-items: center;
.required {
color: #f53f3f;
margin-right: 4px;
font-size: 16px;
line-height: 1;
}
}
.right {
display: flex;
align-items: center;
gap: 4px;
}
}
</style>

View file

@ -3,17 +3,21 @@
<Breadcrumb <Breadcrumb
:items="['menu.setting', 'menu.setting.user', 'menu.setting.user-list']" :items="['menu.setting', 'menu.setting.user', 'menu.setting.user-list']"
/> />
<a-row class="" :gutter="12"> <a-row :gutter="12" :wrap="false">
<a-col flex="310px"> <a-col flex="310px">
<a-card :bordered="false"> <a-card class="full_height" :bordered="false">
<template #title> <template #title>
<div class="card-title_between"> <div class="card-title_between">
<p class="title">{{ $t('organization') }}</p> <p class="title">{{ $t('organization') }}</p>
<a-button @click="onClick"> {{ $t('watch-detail') }} </a-button> <a-button @click="toOrganization">
{{ $t('watch-detail') }}
</a-button>
</div> </div>
</template> </template>
<a-tree <a-tree
:field-names="{ key: 'ID', title: 'Name' }" v-model:expanded-keys="expandedKeys"
v-model:selected-keys="selectedKeys"
:field-names="{ key: 'id', title: 'name' }"
:data="departmentListData" :data="departmentListData"
> >
<template #icon> <template #icon>
@ -23,35 +27,335 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col flex="auto"> <a-col flex="auto">
<a-card title="Arco Card" :bordered="false"> <a-card class="full_height" title="Arco Card" :bordered="false">
<template #extra> <template #title>
<a-link>More</a-link> <div class="card-title_between">
<p class="title">{{ $t('menu.business.project') }}</p>
<div class="flex_center_between">
<a-space>
<a-input-search
v-model="searchKeyWord"
allow-clear
:style="{ width: '320px' }"
:placeholder="$t('search.username.name')"
@clear="handleSearchUser"
@search="handleSearchUser"
@press-enter="handleSearchUser"
/>
<a-button>
<template #icon>
<icon-filter />
</template> </template>
Card content {{ $t('table.filter') }}
<br /> </a-button>
Card content <a-button type="primary" @click="handleAddUser">
<template #icon>
<icon-plus />
</template>
{{ $t('add-user') }}
</a-button>
</a-space>
</div>
</div>
</template>
<!-- 使用新的 DataTable 组件 -->
<DataTable
:data="renderData"
:columns="projectColumn"
:loading="loading"
:total="total"
:current="current"
:page-size="pageSize"
pagination
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
>
<template #realname="{ record }">
<span>{{ record.realname }}</span>
<icon-idcard v-if="record.isManager === 1" />
</template>
<template #status="{ record }">
<a-tag :color="record.status == 1 ? 'green' : 'red'">
{{ record.status == 1 ? $t('enable') : $t('disable') }}</a-tag
>
</template>
<template #operate="{ record }">
<a-space>
<!-- <a-button>查看</a-button> -->
<a-button type="primary" @click="handleEditUser(record.id)">{{
$t('edit')
}}</a-button>
<a-popover trigger="click" position="bottom">
<a-button>{{ $t('more') }}</a-button>
<template #content>
<a-space direction="vertical">
<a-button
@click="handleDisableUser(record.id, record.status)"
>
{{ record.status == 1 ? $t('disable') : $t('enable') }}
</a-button>
<a-button
status="danger"
@click="handleDelUser(record.id)"
>{{ $t('delete') }}</a-button
>
</a-space>
</template>
</a-popover>
</a-space>
</template>
</DataTable>
</a-card> </a-card>
<userFormDrawer
v-model:visible="showDrawer"
:record="userInfo"
:is-edit="isEdit"
:department-list-data="departmentListData"
:role-list="roleList"
@submitted="getUserListData"
/>
</a-col> </a-col>
</a-row> </a-row>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { DepartmentFormParams, createDepartment } from '@/api/setting'; import { ref, watch, onMounted } from 'vue';
import useDepartmentList from '@/views/setting/composables/useDepartmentList'; import { useI18n } from 'vue-i18n';
// import { ref } from 'vue' import { useRouter } from 'vue-router';
const onClick = async () => { import type { TableColumnData } from '@arco-design/web-vue';
const params: DepartmentFormParams = { import {
name: '冰一下很开心', // 部门名称 getUserList,
parent_id: 34, // 上一级id UserList,
getRoleList,
RoleItem,
getUserInfo,
FormUserItem,
disableUser,
delUser,
} from '@/api/setting';
import useDepartmentList from '@/composables/useDepartmentList';
import useDataTable from '@/composables/useDataTable';
import { Message } from '@arco-design/web-vue';
import userFormDrawer from './components/user-form-drawer.vue';
const { t } = useI18n();
const router = useRouter();
const { departmentListData, allDepartmentKeys } = useDepartmentList();
const expandedKeys = ref<(string | number)[]>([]);
const selectedKeys = ref<(string | number)[]>([]);
const showDrawer = ref<boolean>(false);
const isEdit = ref<boolean>(false);
const userInfo = ref<FormUserItem | null>(null);
const roleList = ref<RoleItem[]>([]);
const roleListLoaded = ref(false);
// 使用 DataTable composable
const {
data: renderData,
columns: projectColumn,
loading,
total,
current,
pageSize,
setData,
setColumns,
setLoading,
setTotal,
setCurrent,
setPageSize,
} = useDataTable({
initialPage: 1,
initialPageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
});
const userListData = ref<UserList>([]);
// 列配置
const columns: TableColumnData[] = [
{
title: t('username'),
dataIndex: 'username',
key: 'username',
width: 120,
ellipsis: true,
},
{
title: t('name'),
dataIndex: 'realname',
key: 'realname',
width: 150,
ellipsis: true,
slotName: 'realname',
},
{
title: t('department'),
dataIndex: 'deptName',
key: 'departmentId',
},
{
title: t('role'),
dataIndex: 'roleName',
key: 'roleName',
},
{
title: t('status'),
dataIndex: 'status',
key: 'status',
slotName: 'status',
},
{
title: t('lastLogin'),
dataIndex: 'lastLoginTime',
key: 'lastLoginTime',
render: ({ record }) => record.lastLoginTime || '-',
},
{
title: '操作',
key: 'operate',
slotName: 'operate',
width: 200,
},
];
watch(
allDepartmentKeys,
(val) => {
expandedKeys.value = val;
},
{ immediate: true }
);
const toOrganization = () => {
router.push({ path: '/setting/system', query: { active: 4 } });
}; };
const searchKeyWord = ref<string>('');
// const pageNo = ref<number>(1)
const getUserListData = async () => {
try { try {
const res = await createDepartment(params); const params = {
console.log(res, 'res'); dept_id: selectedKeys.value[0],
keyword: searchKeyWord.value,
page: current.value,
page_size: pageSize.value,
};
setLoading(true);
const { code, data } = await getUserList(params);
if (code === 200) {
const { page, user } = data;
userListData.value = Array.isArray(user) ? user : [];
setData(userListData.value);
setTotal(typeof page.total === 'number' ? page.total : 0);
}
} catch (error) { } catch (error) {
console.log(error, 'res'); // 处理错误
} finally {
setLoading(false);
} }
}; };
const { departmentListData, getDepartmentListData } = useDepartmentList();
getDepartmentListData(); const openDrawer = async () => {
if (!roleListLoaded.value) {
try {
const { code, data } = await getRoleList();
if (code === 200) {
roleList.value = Array.isArray(data?.rolesList) ? data.rolesList : [];
roleListLoaded.value = true;
}
} catch (error) {
// 处理错误
}
}
showDrawer.value = true;
};
watch(
departmentListData,
(val) => {
if (Array.isArray(val) && val.length > 0) {
const [firstRoot] = val;
if (firstRoot.children && firstRoot.children.length > 0) {
selectedKeys.value = [firstRoot.children[0].id];
} else {
selectedKeys.value = [];
}
}
},
{ immediate: true, deep: true }
);
let lastSelectedKey: string | number = '';
watch(selectedKeys, (val) => {
// 只处理有选中项的情况
if (val.length > 0) {
// 判断新旧 key 是否相同
if (val[0] !== lastSelectedKey) {
lastSelectedKey = val[0];
getUserListData();
}
}
});
// 分页事件处理
const onPageChange = (page: number) => {
setCurrent(page);
current.value = page;
getUserListData();
};
const onPageSizeChange = (size: number) => {
setPageSize(size);
pageSize.value = size;
getUserListData();
};
const handleSearchUser = () => {
getUserListData();
};
const handleAddUser = async () => {
isEdit.value = false;
userInfo.value = null;
openDrawer();
};
const getUserInfoData = async (id: number) => {
try {
const { code, data } = await getUserInfo(id);
if (code === 200) userInfo.value = data?.user;
} catch (error) {
// 处理错误
}
};
const handleEditUser = async (id: number) => {
isEdit.value = true;
userInfo.value = null;
await getUserInfoData(id);
openDrawer();
};
const handleDisableUser = async (id: number, currentStatus: number) => {
try {
const param = { id, status: currentStatus === 1 ? 2 : 1 };
const { code, message } = await disableUser(param);
if (code === 200) {
Message.success(message);
getUserListData();
} else {
Message.error(message);
}
} catch (error) {
// 处理错误
}
};
const handleDelUser = async (id: number) => {
try {
const { code, message } = await delUser(id);
if (code === 200) {
Message.success(message);
getUserListData();
} else {
Message.error(message);
}
} catch (error) {
// 处理错误
}
};
// 初始化数据
onMounted(() => {
setColumns(columns);
});
</script> </script>