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
+113 -19
View File
@@ -13,7 +13,18 @@ export interface DepartmentIdData {
departmentId: number | string;
}
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 {
id: number;
systemTenantId: number;
@@ -41,22 +52,11 @@ export interface DepartmentDetailData {
}
export type DepartmentDetailRes = HttpResponse<DepartmentDetailData>;
// 创建部门
export function createDepartment(
params: DepartmentFormParams
): Promise<DepartmentFormRes> {
return axios.post('/department/createDepartment', params);
}
// 获取部门列表
export function getDepartmentList(): Promise<DepartmentListRes> {
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> {
return axios.get(`/department/getDepartment?id=${id}`);
}
@@ -68,19 +68,73 @@ export interface UserItem {
email: string; // 邮箱
tel: string; // 手机号
status: 1 | 2; // 状态:1 启用,2 禁用
role_id: number; // 角色id
department_id: number; // 部门id
role_name: string; // 角色名称
dept_name: string; // 部门名称
roleId: number; // 角色id
departmentId: number; // 部门id
roleName: string; // 角色名称
deptName: string; // 部门名称
lastLoginTime: string; // 最后登录时间
}
export type UserList = UserItem[];
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);
}
// 后台-用户列表(所有用户)
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> {
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> {
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) {
// return axios.get<any>(`/project/getProject?id=${id}`);
+236
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 接口。
+175
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>
+2
View File
@@ -11,6 +11,7 @@ import {
} from 'echarts/components';
import Chart from './chart/index.vue';
import Breadcrumb from './breadcrumb/index.vue';
import DataTable from './common/DataTable.vue';
// Manually introduce ECharts modules to reduce packing size
@@ -31,5 +32,6 @@ export default {
install(Vue: App) {
Vue.component('Chart', Chart);
Vue.component('Breadcrumb', Breadcrumb);
Vue.component('DataTable', DataTable);
},
};
+196
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` 适用于需要等待用户操作后再获取数据的场景
+172
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,
};
}
@@ -4,6 +4,7 @@ import {
DepartmentItem,
getDepartmentDetail,
} from '@/api/setting';
import useLoading from '@/hooks/loading';
export default function useDepartmentList(autoFetch = true) {
const departmentListData = ref<DepartmentItem[]>([]);
+25 -1
View File
@@ -35,7 +35,10 @@ export default {
'menu.finance': '财务',
'menu.property': '物业',
'menu.setting': '设置',
'search.placeholder': '请输入内容',
'please.enter': '请输入{placeholder}',
'please.select': '请选择{placeholder}',
'please.confirm': '请确认{placeholder}',
'content': '内容',
'table.filter': '筛选',
'table.export': '导出',
'watch-detail': '查看详情',
@@ -61,6 +64,7 @@ export default {
'company-info': '公司信息',
'appearance-setting': '外观设置',
'add-department': '新建部门',
'edit-department': '编辑部门',
'department-name': '部门名称',
'belongto-department': '所属部门',
'department-head': '部门主管',
@@ -73,6 +77,26 @@ export default {
'delete': '删除',
'confirm': '确定',
'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,
...localeMessageBox,
+84 -46
View File
@@ -4,43 +4,46 @@
<a-card class="general-card">
<template #title>
<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">
<a-space>
<a-input-search
:style="{ width: '320px' }"
:placeholder="$t('search.placeholder')"
:placeholder="t('please.enter', { placeholder: t('content') })"
/>
<a-button>
<template #icon>
<icon-filter />
</template>
{{ $t('table.filter') }}
{{ t('table.filter') }}
</a-button>
<a-button>
<template #icon>
<icon-download />
</template>
{{ $t('table.export') }}
{{ t('table.export') }}
</a-button>
<a-button type="primary">
<template #icon>
<icon-plus />
</template>
{{ $t('add-project') }}
{{ t('add-project') }}
</a-button>
</a-space>
</div>
</div>
</template>
<a-table
row-key="id"
:loading="loading"
:pagination="pagination"
:columns="projectColumn"
<!-- 使用新的 DataTable 组件 -->
<DataTable
:data="renderData"
:bordered="false"
:columns="projectColumn"
:loading="loading"
:total="total"
:current="current"
:page-size="pageSize"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
>
<template #type="{ record }">
<a-tag checkable :color="getTypeColor(record.type)">{{
@@ -61,15 +64,16 @@
<a-button>更多</a-button>
</a-space>
</template>
</a-table>
</DataTable>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, reactive, watch, nextTick } from 'vue';
import { onMounted } from 'vue';
import useLoading from '@/hooks/loading';
import { Pagination } from '@/types/global';
import { useI18n } from 'vue-i18n';
import type { TableColumnData } from '@arco-design/web-vue';
import {
ProjectRecord,
ProjectType,
@@ -77,11 +81,35 @@
PROJECT_TYPE_COLORS,
PROJECT_TYPE_LABELS,
} 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 renderData = ref<ProjectRecord[]>([
const { t } = useI18n();
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: '星河商务中心',
type: PROJECT_TYPE.COMMERCIAL_COMPLEX,
size: '1200m²',
@@ -91,6 +119,7 @@
create_time: '2025-06-24 00:51',
},
{
id: '2',
name: '南山科学科学院',
type: PROJECT_TYPE.OFFICE_BUILDING,
size: '120m²',
@@ -100,6 +129,7 @@
create_time: '2025-06-24 00:58',
},
{
id: '3',
name: '前海科学科学院',
type: PROJECT_TYPE.INDUSTRIAL_PARK,
size: '1520m²',
@@ -108,24 +138,25 @@
count: '120',
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 => {
@@ -135,21 +166,28 @@
const getTypeText = (type: ProjectType): string => {
return PROJECT_TYPE_LABELS[type] || '未知';
};
const onPageChange = (current: number) => {
pagination.current = current;
// 分页事件处理
const onPageChange = (page: number) => {
setCurrent(page);
// TODO: 这里应该调用API获取对应页码的数据
};
const getAllProjectListData = async () => {
setLoading(true);
try {
const { data } = await getAllProjectList();
console.log(data, 'getAllProjectList');
} catch (err) {
// you can report use errorHandler or other
} finally {
setLoading(false);
}
const onPageSizeChange = (newPageSize: number) => {
setPageSize(newPageSize);
// TODO: 这里应该调用API获取对应页码的数据
};
// const getAllProjectListData = async () => {
// setLoading(true);
// try {
// await getAllProjectList();
// // 处理数据
// } catch (err) {
// // you can report use errorHandler or other
// } finally {
// setLoading(false);
// }
// };
// getAllProjectListData()
</script>
@@ -1,7 +1,7 @@
<template>
<a-modal
:visible="visible"
:title="isEdit ? '编辑部门' : '新增部门'"
:title="isEdit ? $t('edit-department') : $t('add-department')"
unmount-on-close
@ok="handleSubmit"
@cancel="handleCancel"
@@ -14,36 +14,48 @@
layout="vertical"
>
<!-- 部门名称 -->
<a-form-item :label="t('department-name')" field="name" required>
<a-input v-model="form.name" placeholder="请输入部门名称" />
<a-form-item :label="$t('department-name')" field="name" required>
<a-input
v-model="form.name"
:placeholder="
$t('please.enter', { placeholder: $t('department-name') })
"
/>
</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
v-model="form.parent_id"
:data="departmentListData"
:field-names="{ key: 'id', title: 'name', children: 'children' }"
placeholder="请选择所属部门"
:placeholder="
$t('please.select', { placeholder: $t('belongto-department') })
"
allow-clear
/>
</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
v-model="form.user_id"
:options="props.userListData"
placeholder="请选择部门主管"
:placeholder="
$t('please.select', { placeholder: $t('department-head') })
"
allow-clear
/>
</a-form-item>
<!-- 备注 -->
<a-form-item :label="t('remark')" field="remark">
<a-form-item :label="$t('remark')" field="remark">
<a-textarea
v-model="form.remark"
placeholder="请输入备注"
:placeholder="$t('please.enter', { placeholder: $t('remark') })"
allow-clear
/>
</a-form-item>
@@ -75,17 +87,26 @@
user_id?: number;
remark?: string;
}
interface TreeNode {
id: string | number;
name: string;
children?: TreeNode[];
}
const props = defineProps<{
visible: boolean;
isEdit: boolean;
record?: DepartmentItem | null;
departmentListData: any[]; // 新增
departmentListData: TreeNode[]; // 修复类型
userListData: UserList;
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'submitted'): void;
}>();
// 表单数据
const formRef = ref<FormInstance>();
const form = reactive<DepartmentForm>({
@@ -99,7 +120,12 @@
const { t } = useI18n();
// 校验规则
const rules = {
name: [{ required: true, message: '请输入部门名称' }],
name: [
{
required: true,
message: t('please.enter', { placeholder: t('department') }),
},
],
parent_id: [{ required: true, message: '请选择所属部门' }],
};
+21 -2
View File
@@ -33,8 +33,9 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import {
IconSettings,
IconSafe,
@@ -45,7 +46,23 @@
import organization from './organization.vue';
// import { CreateDepartmentParams, createDepartment, getDepartmentList, DepartmentItem } from '@/api/setting'
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 = [
{ id: 1, name: t('general-setting'), icon: IconSettings },
{ id: 2, name: t('security-setting'), icon: IconSafe },
@@ -56,6 +73,8 @@
const handleChooseSystemItem = (id: number) => {
currentActive.value = id;
};
// watch()
</script>
<style lang="less" scoped>
+50 -32
View File
@@ -93,9 +93,9 @@
</template>
<script lang="ts" setup>
import { reactive, ref, watch, onMounted } from 'vue';
import { ref, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import useDepartmentList from '@/views/setting/composables/useDepartmentList';
import useDepartmentList from '@/composables/useDepartmentList';
import {
DepartmentItem,
getAllUserList,
@@ -114,9 +114,10 @@
getDepartmentListData,
getDepartmentDetailData,
} = useDepartmentList();
const emit = defineEmits(['onSelectTree']);
const expandedKeys = ref<(string | number)[]>([]);
const { t } = useI18n();
watch(
allDepartmentKeys,
(val) => {
@@ -129,7 +130,14 @@
const isEdit = ref<boolean>(false);
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) => {
node.isShowEditIcon = false;
if (node.children && node.children.length > 0) {
@@ -137,21 +145,26 @@
}
});
};
const currentNode = ref<any>(null);
const onSelect = debounce(async (_: (string | number)[], event: any) => {
const { node } = event;
if (node.id === currentNode.value?.id) return;
currentNode.value = node;
// 递归重置所有节点的 isShowEditIcon
resetEditIcon(departmentListData.value);
// 只设置当前节点
if (currentNode.value.level !== 1) {
node.isShowEditIcon = true;
}
departmentDetailInfo.value = await getDepartmentDetailData(
currentNode.value.id
);
}, 300);
const currentNode = ref<TreeNode | null>(null);
const onSelect = debounce(
async (_: (string | number)[], event: { node: TreeNode }) => {
const { node } = event;
if (node.id === currentNode.value?.id) return;
currentNode.value = node;
// 递归重置所有节点的 isShowEditIcon
resetEditIcon(departmentListData.value);
// 只设置当前节点
if (currentNode.value.level !== 1) {
node.isShowEditIcon = true;
}
departmentDetailInfo.value = await getDepartmentDetailData(
Number(currentNode.value.id)
);
},
300
);
// 所有用户数据
const allUserList = ref<UserList>([]);
@@ -162,39 +175,43 @@
const { code, data } = await getAllUserList({
keyword: userKeyword.value,
});
if (code === 200 && data?.length) {
allUserList.value = data;
if (code === 200) {
const { user } = data;
allUserList.value = Array.isArray(user) ? user : [];
}
} catch (error) {
console.error('获取所以用户数据失败:', error);
// 处理错误
}
};
const handleAdd = () => {
showModal.value = true;
isEdit.value = false;
departmentDetailInfo.value = null;
showModal.value = true;
};
const handleEdit = () => {
showModal.value = true;
isEdit.value = true;
};
const handleDelete = async (item) => {
const handleDelete = async (item: { id: string | number }) => {
const { id } = item;
try {
const { code, message } = await delDepartment(id);
const { code, message } = await delDepartment(Number(id));
if (code === 200) {
Message.success(message);
getDepartmentListData();
}
} catch (error) {
console.log('删除失败:', error);
// 处理错误
}
};
const handleMoveUp = async (item) => {
const handleMoveUp = async (item: { id: string | number }) => {
const { id } = item;
try {
const { code, message } = await departmentSetUp(id);
const { code, message } = await departmentSetUp(Number(id));
if (code === 200) {
Message.success(message);
getDepartmentListData();
@@ -202,13 +219,14 @@
Message.info(message);
}
} catch (error) {
console.log('向上移动失败:', error);
// 处理错误
}
};
const handleMoveDown = async (item) => {
const handleMoveDown = async (item: { id: string | number }) => {
const { id } = item;
try {
const { code, message } = await departmentSetDown(id);
const { code, message } = await departmentSetDown(Number(id));
if (code === 200) {
Message.success(message);
getDepartmentListData();
@@ -216,7 +234,7 @@
Message.info(message);
}
} catch (error) {
console.log('向下移动失败:', error);
// 处理错误
}
};
@@ -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>
+327 -23
View File
@@ -3,17 +3,21 @@
<Breadcrumb
: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-card :bordered="false">
<a-card class="full_height" :bordered="false">
<template #title>
<div class="card-title_between">
<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>
</template>
<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"
>
<template #icon>
@@ -23,35 +27,335 @@
</a-card>
</a-col>
<a-col flex="auto">
<a-card title="Arco Card" :bordered="false">
<template #extra>
<a-link>More</a-link>
<a-card class="full_height" title="Arco Card" :bordered="false">
<template #title>
<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>
{{ $t('table.filter') }}
</a-button>
<a-button type="primary" @click="handleAddUser">
<template #icon>
<icon-plus />
</template>
{{ $t('add-user') }}
</a-button>
</a-space>
</div>
</div>
</template>
Card content
<br />
Card content
<!-- 使用新的 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>
<userFormDrawer
v-model:visible="showDrawer"
:record="userInfo"
:is-edit="isEdit"
:department-list-data="departmentListData"
:role-list="roleList"
@submitted="getUserListData"
/>
</a-col>
</a-row>
</div>
</template>
<script lang="ts" setup>
import { DepartmentFormParams, createDepartment } from '@/api/setting';
import useDepartmentList from '@/views/setting/composables/useDepartmentList';
// import { ref } from 'vue'
const onClick = async () => {
const params: DepartmentFormParams = {
name: '冰一下很开心', // 部门名称
parent_id: 34, // 上一级id
};
import { ref, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import type { TableColumnData } from '@arco-design/web-vue';
import {
getUserList,
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 {
const res = await createDepartment(params);
console.log(res, 'res');
const params = {
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) {
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>