Browse Source

feat: 字典模块初始化

master
AaronWu 1 year ago
parent
commit
dde0fe9f8d
  1. 2427
      package-lock.json
  2. 1
      package.json
  3. 18
      src/api/account/index.ts
  4. 156
      src/api/dict/index.ts
  5. 26
      src/api/dict/model.d.ts
  6. 3
      src/api/typings.d.ts
  7. 83
      src/api/user/index.ts
  8. 13
      src/api/user/model.d.ts
  9. 769
      src/assets/DTO.md
  10. 7
      src/components/core/Drawer/index.ts
  11. 258
      src/components/core/Drawer/src/BasicDrawer.vue
  12. 82
      src/components/core/Drawer/src/components/DrawerFooter.vue
  13. 74
      src/components/core/Drawer/src/components/DrawerHeader.vue
  14. 44
      src/components/core/Drawer/src/props.ts
  15. 195
      src/components/core/Drawer/src/typing.ts
  16. 161
      src/components/core/Drawer/src/useDrawer.ts
  17. 4
      src/components/core/Tree/src/BasicTree.vue
  18. 22
      src/shims-vue.d.ts
  19. 38
      src/utils/env.ts
  20. 9
      src/utils/log.ts
  21. 151
      src/views/dashboard/welcome/index.vue
  22. 2
      src/views/login/index.vue
  23. 14
      src/views/system/dictionary/Edit.vue
  24. 330
      src/views/system/dictionary/Tree.vue
  25. 154
      src/views/system/dictionary/columns.tsx
  26. 62
      src/views/system/dictionary/formSchemas.ts
  27. 344
      src/views/system/dictionary/index.vue
  28. 14
      src/views/system/dictionary/mockData.ts
  29. 34
      src/views/system/user/columns.tsx
  30. 49
      src/views/system/user/formSchemas.ts
  31. 12
      src/views/system/user/index.vue
  32. 6
      vite.config.ts
  33. 1491
      yarn.lock

2427
package-lock.json

File diff suppressed because it is too large

1
package.json

@ -21,6 +21,7 @@
"pinia": "^2.1.3",
"qs": "^6.11.2",
"sortablejs": "^1.15.0",
"vite-plugin-md": "^0.22.5",
"vite-plugin-windicss": "^1.9.0",
"vue": "^3.2.47",
"vue-i18n": "9.2.2",

18
src/api/account/index.ts

@ -1,9 +1,17 @@
/*
* @Author: AaronWu 2463371514@qq.com
* @Date: 2025-03-31 15:12:17
* @LastEditors: AaronWu 2463371514@qq.com
* @LastEditTime: 2025-04-01 10:50:39
* @FilePath: /IssueSupportManage/src/api/account/index.ts
* @Description: ,`customMade`, koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import type { BaseResponse } from '@/utils/request';
import { request } from '@/utils/request';
export function updateAccountInfo(data: any) {
return request<BaseResponse<any>>({
url: 'account/update',
url: '/update',
method: 'post',
data,
});
@ -11,7 +19,7 @@ export function updateAccountInfo(data: any) {
export function updatePassword(data: any) {
return request({
url: 'account/password',
url: '/password',
method: 'post',
data,
});
@ -19,21 +27,21 @@ export function updatePassword(data: any) {
export function getInfo() {
return request<API.AdminUserInfo>({
url: 'account/info',
url: '/info',
method: 'get',
});
}
export function permmenu() {
return request<API.PermMenu>({
url: 'account/permmenu',
url: '/permmenu',
method: 'get',
});
}
export function logout() {
return request({
url: 'account/logout',
url: '/logout',
method: 'post',
});
}

156
src/api/dict/index.ts

@ -0,0 +1,156 @@
import type { BaseResponse } from '@/utils/request';
import { request } from '@/utils/request';
/**
* @description
* @param {SearchPageListParams} data
* @returns
*/
export function fetchDictList() {
return request<BaseResponse<API.SearchPageListResult>>({
url: `/fieldType/list`,
method: 'post',
});
}
/**
* @description
* @param {DictType} data
* @returns
*/
export function createDict(data: API.DictType) {
return request({
url: `/fieldType/create`,
method: 'post',
data,
});
}
/**
* @description
* @param {DictType} data
* @returns
*/
export function updateDict(data: API.DictType) {
return request({
url: `/fieldType/update`,
method: 'put',
data,
});
}
/**
* @description
*/
export function deleteBatchDictById(data: API.DeleteBatchDictParams) {
return request({
url: `/fieldType/deleteBatch`,
method: 'delete',
data,
});
}
/**
* @description
* @param { fieldTypeId:string} data
* @returns
*/
export function fetchDictValueListByType(data: { fieldTypeName: string }) {
return request({
url: `/dict/query`,
method: 'post',
data,
});
}
/**
* @description
* @param {SearchPageListParams} data
* @returns
*/
export function fetchDictValuePageList(data: API.SearchPageListParams) {
return request<BaseResponse<API.SearchPageListResult>>(
{
url: `/dict/page`,
method: 'post',
data,
},
{
isGetDataDirectly: false,
},
);
}
/**
* @description
* @param {DictValueType} data
* @returns
*/
export function createDictValue(data: API.DictValueType) {
return request({
url: `/dict/create`,
method: 'post',
data,
});
}
/**
* @description
* @param {id: string } data
* @returns
*/
export function enableDictValue(data: { id: string }) {
return request({
url: `/dict/enable`,
method: 'post',
params: data,
});
}
/**
* @description
* @param {id: string } data
* @returns
*/
export function disableDictValue(data: { id: string }) {
return request({
url: `/dict/disable`,
method: 'post',
params: data,
});
}
/**
* @description
* @param {DictValueType} data
* @returns
*/
export function updateDictValue(data: API.DictValueType) {
return request({
url: `/dict/update`,
method: 'put',
data,
});
}
/**
* @description
*/
export function deleteDictValueById(params: API.DeleteDictValueParams) {
return request({
url: `/dict/delete`,
method: 'delete',
params: params,
});
}
/**
* @description
*/
export function deleteBatchDictValueById(data: API.DeleteBatchDictValueParams) {
return request({
url: `/dict/deleteBatch`,
method: 'delete',
data,
});
}

26
src/api/dict/model.d.ts

@ -0,0 +1,26 @@
declare namespace API {
type DictType = {
id?: string;
name: string;
};
type DeleteDictParams = {
id: string;
};
type DeleteBatchDictParams = string[];
type DictValueType = {
id?: string;
fieldTypeId: string;
fieldValue: string;
enable: string;
pendingStatus?: boolean;
};
type DeleteDictValueParams = {
id: string;
};
type DeleteBatchDictValueParams = string[];
}

3
src/api/typings.d.ts

@ -2,7 +2,7 @@
* @Author: AaronWu 2463371514@qq.com
* @Date: 2025-03-31 15:12:17
* @LastEditors: AaronWu 2463371514@qq.com
* @LastEditTime: 2025-04-01 09:14:02
* @LastEditTime: 2025-04-01 14:41:59
* @FilePath: /IssueSupportManage/src/api/typings.d.ts
* @Description: ,`customMade`, koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
@ -17,6 +17,7 @@ declare namespace API {
/** 查询分页列表参数 */
type SearchPageListParams = {
[key: string]: any;
model: SearchListParams;
current: number;
size: number;

83
src/api/user/index.ts

@ -1,3 +1,11 @@
/*
* @Author: AaronWu 2463371514@qq.com
* @Date: 2025-04-01 09:09:04
* @LastEditors: AaronWu 2463371514@qq.com
* @LastEditTime: 2025-04-01 11:34:26
* @FilePath: /IssueSupportManage/src/api/user/index.ts
* @Description: ,`customMade`, koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import type { BaseResponse } from '@/utils/request';
import { request } from '@/utils/request';
/**
@ -8,43 +16,7 @@ import { request } from '@/utils/request';
export function fetchUserList(data: API.SearchListParams) {
return request<BaseResponse<API.SearchListResult>>(
{
url: 'user/query',
method: 'post',
data,
},
{
isGetDataDirectly: false,
},
);
}
/**
* @description
* @param {SearchPageListParams} data
* @returns
*/
export function fetchTenantBindUsers(data: any) {
return request<BaseResponse<API.SearchPageListResult>>(
{
url: `tenantUser/bindPage?tenantId=${data.tenantId}`,
method: 'post',
data,
},
{
isGetDataDirectly: false,
},
);
}
/**
* @description
* @param {SearchPageListParams} data
* @returns
*/
export function fetchUnTenantBindUsers(data: any) {
return request<BaseResponse<API.SearchPageListResult>>(
{
url: `tenantUser/unBindPage?tenantId=${data.tenantId}`,
url: 'user/list',
method: 'post',
data,
},
@ -54,19 +26,6 @@ export function fetchUnTenantBindUsers(data: any) {
);
}
/**
* @description
* @param data
* @returns
*/
export function assignTenantAdmin(data: { tenantId?: string; userId?: string; isAdmin?: boolean }) {
return request({
url: `tenantUser/assignTenantAdmin`,
method: 'post',
data,
});
}
/**
* @description
* @param {SearchPageListParams} data
@ -91,16 +50,11 @@ export function fetchUserPageList(data: API.SearchPageListParams) {
* @returns
*/
export function createUser(data: API.UserInfoType) {
return request<BaseResponse<API.SearchListResult>>(
{
return request({
url: 'user/create',
method: 'post',
data,
},
{
isGetDataDirectly: false,
},
);
});
}
/**
@ -109,23 +63,18 @@ export function createUser(data: API.UserInfoType) {
* @returns
*/
export function updateUser(data: API.UserInfoType) {
return request<BaseResponse<API.SearchListResult>>(
{
return request({
url: 'user/update',
method: 'post',
data,
},
{
isGetDataDirectly: false,
},
);
});
}
/**
* @description
*/
export function findOneById(params: { id: string }) {
return request<API.SearchListResult>({
return request({
url: `user/getById`,
method: 'get',
params,
@ -136,7 +85,7 @@ export function findOneById(params: { id: string }) {
* @description ?id=${data.id}
*/
export function deleteUserById(params: API.DeleteUserParams) {
return request<API.SearchListResult>({
return request({
url: `user/delById`,
method: 'post',
params,
@ -147,7 +96,7 @@ export function deleteUserById(params: API.DeleteUserParams) {
* @description
*/
export function deleteBatchUserById(data: API.DeleteBatchUserParams) {
return request<API.SearchListResult>({
return request({
url: `user/deleteBatch`,
method: 'delete',
data,

13
src/api/user/model.d.ts

@ -2,7 +2,7 @@
* @Author: AaronWu 2463371514@qq.com
* @Date: 2025-04-01 09:09:04
* @LastEditors: AaronWu 2463371514@qq.com
* @LastEditTime: 2025-04-01 09:14:00
* @LastEditTime: 2025-04-01 16:48:06
* @FilePath: /IssueSupportManage/src/api/user/model.d.ts
* @Description: ,`customMade`, koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
@ -10,27 +10,22 @@ declare namespace API {
/** 用户信息类型 */
type UserInfoType = {
id?: string;
userId?: string;
pendingStatus?: boolean;
username: string;
realName: any;
account: string;
mobile: string;
idCard: string;
sex: string;
avatar: string;
state: number;
remark: string;
createTime: string;
isAdmin?: boolean;
pendingStatus?: boolean;
};
type CreateUserParams = {
username: string;
realName: any;
account: string;
mobile: string;
idCard: string;
sex: string;
avatar: string;
state: number;
remark: string;
isAdmin?: boolean;

769
src/assets/DTO.md

@ -0,0 +1,769 @@
<!--
* @Author: AaronWu 2463371514@qq.com
* @Date: 2025-04-01 10:28:36
* @LastEditors: AaronWu 2463371514@qq.com
* @LastEditTime: 2025-04-01 16:51:20
* @FilePath: /IssueSupportManage/src/assets/DTO.md
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
# 接口文档
<details open>
<summary><h2>用户认证模块</h2></summary>
<details open>
<summary><h3>1. 用户登录</h3></summary>
#### 接口说明
- 接口描述:用户登录接口,用于验证用户身份并获取访问令牌
- 请求方式:POST
- 接口路径:/api/login
#### 请求参数
##### LoginParams
| 参数名 | 类型 | 必填 | 说明 |
| -------- | ------ | ---- | ------ |
| username | string | 是 | 用户名 |
| password | string | 是 | 密码 |
#### 响应结果
##### LoginResult
| 参数名 | 类型 | 说明 |
| ------------ | ------ | -------- |
| accessToken | string | 访问令牌 |
| expiresTime | string | 过期时间 |
| refreshToken | string | 刷新令牌 |
| userId | string | 用户 ID |
#### 响应示例
```json
{
"code": 200,
"message": "登录成功",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresTime": "2024-04-01 11:30:00",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"userId": "12345"
}
}
```
### 2. 用户登出
#### 接口说明
- 接口描述:用户登出接口,用于注销当前用户的登录状态
- 请求方式:POST
- 接口路径:/api/logout
#### 请求参数
#### 响应结果
| 参数名 | 类型 | 说明 |
| ------- | ------ | -------- |
| code | number | 状态码 |
| message | string | 响应消息 |
#### 响应示例
```json
{
"code": 200,
"message": "登出成功"
}
```
<details open>
<summary><h2>用户管理模块</h2></summary>
<details open>
<summary><h3>1. 用户列表查询</h3></summary>
#### 接口说明
- 接口描述:查询用户列表数据,支持条件搜索
- 请求方式:POST
- 接口路径:/api/user/list
#### 请求参数
##### SearchListParams
| 参数名 | 类型 | 必填 | 说明 |
| ------- | ------ | ---- | ---------------------------------------- |
| keyword | string | 否 | 搜索关键词(支持用户名、手机号模糊搜索) |
| state | number | 否 | 用户状态(0:禁用,1:启用) |
| sex | string | 否 | 性别 |
#### 响应结果
##### SearchListResult
| 参数名 | 类型 | 说明 |
| ------ | --------------- | ------------ |
| list | Array<UserInfo> | 用户列表数据 |
##### UserInfo
| 参数名 | 类型 | 说明 |
| ---------- | ------- | ------------------------ |
| id | string | 用户 ID |
| account | string | 账号 |
| username | string | 用户名 |
| mobile | string | 手机号 |
| sex | string | 性别 |
| state | number | 状态(0:禁用,1:启用) |
| remark | string | 备注 |
| createTime | string | 创建时间 |
| isAdmin | boolean | 是否管理员 |
#### 响应示例
```json
{
"code": 200,
"message": "操作成功",
"data": {
"list": [
{
"id": "1",
"username": "zhangsan",
"mobile": "13800138000",
"sex": "男",
"state": 1,
"remark": "技术部员工",
"createTime": "2024-04-01 10:00:00",
"isAdmin": false
}
]
}
}
```
<details open>
<summary><h3>2. 用户分页列表</h3></summary>
#### 接口说明
- 接口描述:分页查询用户列表数据
- 请求方式:POST
- 接口路径:/api/user/page
#### 请求参数
##### SearchPageListParams
| 参数名 | 类型 | 必填 | 说明 |
|-------|------|------|------|
| name | string | 否 | 搜索关键词 |
| keyword | string | 否 | 搜索关键词(支持用户名、手机号模糊搜索) |
| state | number | 否 | 用户状态(0:禁用,1:启用) |
| sex | string | 否 | 性别 |
| current | number | 是 | 当前页码 |
| size | number | 是 | 每页条数 |
| sort | string | 否 | 排序字段 |
| order | string | 否 | 排序方式(asc:升序,desc:降序) |
##### SearchListParams
| 参数名 | 类型 | 必填 | 说明 |
| --------- | ------ | ---- | ---------------------------------------- |
| name | string | 否 | 搜索关键词 |
| keyword | string | 否 | 搜索关键词(支持用户名、手机号模糊搜索) |
| state | number | 否 | 用户状态(0:禁用,1:启用) |
| sex | string | 否 | 性别 |
| pageSize | number | 是 | 每页条数 |
| pageNum | number | 是 | 当前页码 |
| sortField | string | 否 | 排序字段(默认:createTime) |
| sortOrder | string | 否 | 排序方式(asc:升序,desc:降序) |
#### 响应结果
##### SearchPageListResult
| 参数名 | 类型 | 说明 |
| -------- | --------------- | ------------ |
| list | Array<UserInfo> | 用户列表数据 |
| total | number | 总记录数 |
| pageSize | number | 每页条数 |
| pageNum | number | 当前页码 |
##### UserInfo
| 参数名 | 类型 | 说明 |
| ---------- | ------- | ------------------------ |
| id | string | 用户 ID |
| account | string | 账号 |
| username | string | 用户名 |
| mobile | string | 手机号 |
| sex | string | 性别 |
| state | number | 状态(0:禁用,1:启用) |
| remark | string | 备注 |
| createTime | string | 创建时间 |
| isAdmin | boolean | 是否管理员 |
#### 响应示例
```json
{
"code": 200,
"message": "操作成功",
"data": {
"list": [
{
"id": "1",
"username": "zhangsan",
"mobile": "13800138000",
"sex": "男",
"state": 1,
"remark": "技术部员工",
"createTime": "2024-04-01 10:00:00",
"isAdmin": false
}
],
"total": 100,
"pageSize": 10,
"pageNum": 1
}
}
```
<details open>
<summary><h3>3. 新增用户</h3></summary>
#### 接口说明
- 接口描述:新增用户信息
- 请求方式:POST
- 接口路径:/api/user/create
#### 请求参数
##### UserInfoType
| 参数名 | 类型 | 必填 | 说明 |
| -------- | ------- | ---- | ------------------------ |
| account | string |是 | 账号 |
| username | string | 是 | 用户名 |
| mobile | string | 是 | 手机号 |
| sex | string | 是 | 性别 |
| state | number | 是 | 状态(0:禁用,1:启用) |
| remark | string | 是 | 备注 |
| isAdmin | boolean | 否 | 是否管理员 |
#### 响应结果
| 参数名 | 类型 | 说明 |
| ------- | ------ | -------- |
| code | number | 状态码 |
| message | string | 响应消息 |
#### 响应示例
```json
{
"code": 200,
"message": "创建成功"
}
```
<details open>
<summary><h3>4. 修改用户</h3></summary>
#### 接口说明
- 接口描述:修改用户信息
- 请求方式:POST
- 接口路径:/api/user/update
#### 请求参数
##### UserInfoType
| 参数名 | 类型 | 必填 | 说明 |
| -------- | ------- | ---- | ------------------------ |
| id | string | 是 | 用户 ID |
| account | string | 是 | 账号 |
| username | string | 是 | 用户名 |
| mobile | string | 是 | 手机号 |
| sex | string | 是 | 性别 |
| state | number | 是 | 状态(0:禁用,1:启用) |
| remark | string | 是 | 备注 |
| isAdmin | boolean | 否 | 是否管理员 |
#### 响应结果
| 参数名 | 类型 | 说明 |
| ------- | ------ | -------- |
| code | number | 状态码 |
| message | string | 响应消息 |
#### 响应示例
```json
{
"code": 200,
"message": "修改成功"
}
```
<details open>
<summary><h3>5. 查询用户详情</h3></summary>
#### 接口说明
- 接口描述:根据 ID 查询用户详细信息
- 请求方式:GET
- 接口路径:/api/user/getById
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
| ------ | ------ | ---- | ------- |
| id | string | 是 | 用户 ID |
#### 响应结果
##### UserInfo
| 参数名 | 类型 | 说明 |
| ---------- | ------- | ------------------------ |
| id | string | 用户 ID |
| account | string | 账号 |
| username | string | 用户名 |
| mobile | string | 手机号 |
| sex | string | 性别 |
| state | number | 状态(0:禁用,1:启用) |
| remark | string | 备注 |
| createTime | string | 创建时间 |
| isAdmin | boolean | 是否管理员 |
#### 响应示例
```json
{
"code": 200,
"message": "操作成功",
"data": {
"id": "1",
"username": "zhangsan",
"mobile": "13800138000",
"sex": "男",
"state": 1,
"remark": "技术部员工",
"createTime": "2024-04-01 10:00:00",
"isAdmin": false
}
}
```
<details open>
<summary><h2>字典管理模块</h2></summary>
<details open>
<summary><h3>1. 字典类别列表查询</h3></summary>
#### 接口说明
- 接口描述:查询所有字典类别列表
- 请求方式:POST
- 接口路径:/api/fieldType/list
#### 请求参数
#### 响应结果
##### SearchPageListResult
| 参数名 | 类型 | 说明 |
| ------ | --------------- | ------------ |
| list | Array<DictType> | 字典类别列表 |
##### DictType
| 参数名 | 类型 | 必填 | 说明 |
| ------ | ------ | ---- | -------- |
| id | string | 否 | 类别 ID |
| name | string | 是 | 类别名称 |
#### 响应示例
```json
{
"code": 200,
"message": "操作成功",
"data": {
"list": [
{
"id": "1",
"name": "性别"
}
]
}
}
```
<details open>
<summary><h3>2. 新增字典类别</h3></summary>
#### 接口说明
- 接口描述:新增字典类别信息
- 请求方式:POST
- 接口路径:/api/fieldType/create
#### 请求参数
##### DictType
| 参数名 | 类型 | 必填 | 说明 |
| ------ | ------ | ---- | -------- |
| id | string | 否 | 类别 ID |
| name | string | 是 | 类别名称 |
#### 响应结果
| 参数名 | 类型 | 说明 |
| ------- | ------ | -------- |
| code | number | 状态码 |
| message | string | 响应消息 |
#### 响应示例
```json
{
"code": 200,
"message": "创建成功"
}
```
<details open>
<summary><h3>3. 修改字典类别</h3></summary>
#### 接口说明
- 接口描述:修改字典类别信息
- 请求方式:PUT
- 接口路径:/api/fieldType/update
#### 请求参数
##### DictType
| 参数名 | 类型 | 必填 | 说明 |
| ------ | ------ | ---- | -------- |
| id | string | 是 | 类别 ID |
| name | string | 是 | 类别名称 |
#### 响应结果
| 参数名 | 类型 | 说明 |
| ------- | ------ | -------- |
| code | number | 状态码 |
| message | string | 响应消息 |
#### 响应示例
```json
{
"code": 200,
"message": "修改成功"
}
```
<details open>
<summary><h3>4. 批量删除字典类别</h3></summary>
#### 接口说明
- 接口描述:批量删除字典类别信息
- 请求方式:DELETE
- 接口路径:/api/fieldType/deleteBatch
#### 请求参数
##### DeleteBatchDictParams
| 参数名 | 类型 | 必填 | 说明 |
| ------ | -------- | ---- | ---------------- |
| - | string[] | 是 | 字典类别 ID 数组 |
#### 请求示例
```json
["1", "2", "3"]
```
<details open>
<summary><h3>5. 根据字典类型查询字典值列表</h3></summary>
#### 接口说明
- 接口描述:根据字典类型名称查询对应的字典值列表
- 请求方式:POST
- 接口路径:/api/dict/query
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
| ------------- | ------ | ---- | ------------ |
| fieldTypeName | string | 是 | 字典类型名称 |
#### 响应结果
| 参数名 | 类型 | 说明 |
| ------ | -------------------- | ---------- |
| list | Array<DictValueType> | 字典值列表 |
##### DictValueType
| 参数名 | 类型 | 说明 |
| ------------- | ------- | ----------- |
| id | string | 字典值 ID |
| fieldTypeId | string | 字典类型 ID |
| fieldValue | string | 字典值 |
| enable | string | 启用状态 |
| pendingStatus | boolean | 待处理状态 |
#### 响应示例
```json
{
"code": 200,
"message": "操作成功",
"data": {
"list": [
{
"id": "1",
"fieldTypeId": "type_1",
"fieldValue": "男",
"enable": "1",
"pendingStatus": false
},
{
"id": "2",
"fieldTypeId": "type_1",
"fieldValue": "女",
"enable": "1",
"pendingStatus": false
}
]
}
}
```
<details open>
<summary><h3>6. 字典值分页列表查询</h3></summary>
#### 接口说明
- 接口描述:分页查询字典值列表数据
- 请求方式:POST
- 接口路径:/api/dict/page
#### 请求参数
##### SearchPageListParams
| 参数名 | 类型 | 必填 | 说明 |
|-------|------|------|------|
| fieldValue | string | 否 | 字典值 |
| current | number | 是 | 当前页码 |
| size | number | 是 | 每页条数 |
| sort | string | 否 | 排序字段 |
| order | string | 否 | 排序方式(asc:升序,desc:降序) |
##### SearchListParams
| 参数名 | 类型 | 必填 | 说明 |
| ---------- | ------ | ---- | --------------------------------- |
| fieldValue | string | 否 | 字典值 |
| sortField | string | 否 | 排序字段(默认:createTime) |
| sortOrder | string | 否 | 排序方式(asc:升序,desc:降序) |
#### 响应结果
##### SearchPageListResult
| 参数名 | 类型 | 说明 |
| -------- | -------------------- | ---------- |
| list | Array<DictValueType> | 字典值列表 |
| total | number | 总记录数 |
| pageSize | number | 每页条数 |
| pageNum | number | 当前页码 |
##### DictValueType
| 参数名 | 类型 | 说明 |
| ------------- | ------- | ----------- |
| id | string | 字典值 ID |
| fieldTypeId | string | 字典类型 ID |
| fieldValue | string | 字典值 |
| enable | string | 启用状态 |
| pendingStatus | boolean | 待处理状态 |
#### 响应示例
```json
{
"code": 200,
"message": "操作成功",
"data": {
"list": [
{
"id": "1",
"fieldTypeId": "type_1",
"fieldValue": "男",
"enable": "1",
"pendingStatus": false
}
],
"total": 10,
"pageSize": 10,
"pageNum": 1
}
}
```
<details open>
<summary><h3>7. 新增字典值</h3></summary>
#### 接口说明
- 接口描述:新增字典值信息
- 请求方式:POST
- 接口路径:/api/dict/create
#### 请求参数
##### DictValueType
| 参数名 | 类型 | 必填 | 说明 |
|-------|------|------|------|
| id | string | 否 | 字典值ID |
| fieldTypeId | string | 是 | 字典类型ID |
| fieldValue | string | 是 | 字典值 |
| enable | string | 是 | 启用状态 |
| pendingStatus | boolean | 否 | 待处理状态 |
#### 响应结果
| 参数名 | 类型 | 说明 |
|-------|------|------|
| code | number | 状态码 |
| message | string | 响应消息 |
#### 响应示例
```json
{
"code": 200,
"message": "创建成功"
}
```
<details open>
<summary><h3>8. 修改字典值</h3></summary>
#### 接口说明
- 接口描述:修改字典值信息
- 请求方式:PUT
- 接口路径:/api/dict/update
#### 请求参数
##### DictValueType
| 参数名 | 类型 | 必填 | 说明 |
|-------|------|------|------|
| id | string | 是 | 字典值ID |
| fieldTypeId | string | 是 | 字典类型ID |
| fieldValue | string | 是 | 字典值 |
| enable | string | 是 | 启用状态 |
| pendingStatus | boolean | 否 | 待处理状态 |
#### 响应结果
| 参数名 | 类型 | 说明 |
|-------|------|------|
| code | number | 状态码 |
| message | string | 响应消息 |
#### 响应示例
```json
{
"code": 200,
"message": "修改成功"
}
```
<details open>
<summary><h3>9. 启用字典值</h3></summary>
#### 接口说明
- 接口描述:启用指定的字典值
- 请求方式:POST
- 接口路径:/api/dict/enable
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|-------|------|------|------|
| id | string | 是 | 字典值ID |
#### 响应结果
| 参数名 | 类型 | 说明 |
|-------|------|------|
| code | number | 状态码 |
| message | string | 响应消息 |
#### 响应示例
```json
{
"code": 200,
"message": "启用成功"
}
```
<details open>
<summary><h3>10. 禁用字典值</h3></summary>
#### 接口说明
- 接口描述:禁用指定的字典值
- 请求方式:POST
- 接口路径:/api/dict/disable
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|-------|------|------|------|
| id | string | 是 | 字典值ID |
#### 响应结果
| 参数名 | 类型 | 说明 |
|-------|------|------|
| code | number | 状态码 |
| message | string | 响应消息 |
#### 响应示例
```json
{
"code": 200,
"message": "禁用成功"
}
```
<details open>
<summary><h3>11. 批量删除字典值</h3></summary>
#### 接口说明
- 接口描述:批量删除字典值信息
- 请求方式:DELETE
- 接口路径:/api/dict/deleteBatch
#### 请求参数
##### DeleteBatchDictValueParams
| 参数名 | 类型 | 必填 | 说明 |
|-------|------|------|------|
| - | string[] | 是 | 字典值ID数组 |
#### 请求示例
```json
["1", "2", "3"]
```

7
src/components/core/Drawer/index.ts

@ -0,0 +1,7 @@
import { withInstall } from '@/utils'
import basicDrawer from './src/BasicDrawer.vue';
export const BasicDrawer = withInstall(basicDrawer);
export * from './src/typing';
export { useDrawer, useDrawerInner } from './src/useDrawer';

258
src/components/core/Drawer/src/BasicDrawer.vue

@ -0,0 +1,258 @@
<template>
<Drawer :class="prefixCls" @close="onClose" v-bind="getBindValues">
<template #title v-if="!$slots.title">
<DrawerHeader
:title="getMergeProps.title"
:isDetail="isDetail"
:showDetailBack="showDetailBack"
@close="onClose"
>
<template #titleToolbar>
<slot name="titleToolbar"></slot>
</template>
</DrawerHeader>
</template>
<template v-else #title>
<slot name="title"></slot>
</template>
<ScrollContainer
:style="getScrollContentStyle"
:loading-tip="loadingText || t('common.loadingText')"
>
<!-- v-loading="getLoading" -->
<slot></slot>
</ScrollContainer>
<DrawerFooter v-bind="getProps" @close="onClose" @ok="handleOk" :height="getFooterHeight">
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</DrawerFooter>
</Drawer>
</template>
<script lang="ts">
import type { DrawerInstance, DrawerProps } from './typing';
import type { CSSProperties } from 'vue';
import {
defineComponent,
ref,
computed,
watch,
unref,
nextTick,
toRaw,
getCurrentInstance,
} from 'vue';
import { Drawer } from 'ant-design-vue';
import { useI18n } from '@/hooks/useI18n';
import { isFunction, isNumber } from '@/utils/is';
import { deepMerge } from '@/utils';
import DrawerFooter from './components/DrawerFooter.vue';
import DrawerHeader from './components/DrawerHeader.vue';
import { ScrollContainer } from '@/components/core/Container';
import { basicProps } from './props';
// import { useDesign } from '/@/hooks/web/useDesign';
import { useAttrs } from '@/hooks/core/useAttrs';
export default defineComponent({
components: { Drawer, ScrollContainer, DrawerFooter, DrawerHeader },
inheritAttrs: false,
props: basicProps,
emits: ['visible-change', 'ok', 'close', 'register'],
setup(props, { emit }) {
const visibleRef = ref(false);
const attrs = useAttrs();
const propsRef = ref<Partial<Nullable<DrawerProps>>>(null);
const { t } = useI18n();
const prefixVar= 'basic-drawer',
prefixCls ='basic-drawer';
const drawerInstance: DrawerInstance = {
setDrawerProps: setDrawerProps,
emitVisible: undefined,
};
const instance = getCurrentInstance();
instance && emit('register', drawerInstance, instance.uid);
const getMergeProps = computed((): DrawerProps => {
return deepMerge(toRaw(props), unref(propsRef));
});
const getProps = computed((): DrawerProps => {
const opt = {
placement: 'right',
...unref(attrs),
...unref(getMergeProps),
visible: unref(visibleRef),
};
opt.title = undefined;
const { isDetail, width, wrapClassName, getContainer } = opt;
if (isDetail) {
if (!width) {
opt.width = '100%';
}
const detailCls = `${prefixCls}__detail`;
opt.class = wrapClassName ? `${wrapClassName} ${detailCls}` : detailCls;
if (!getContainer) {
// TODO type error?
opt.getContainer = `.${prefixVar}-layout-content` as any;
}
}
return opt as DrawerProps;
});
const getBindValues = computed((): DrawerProps => {
return {
...attrs,
...unref(getProps),
};
});
// Custom implementation of the bottom button,
const getFooterHeight = computed(() => {
const { footerHeight, showFooter } = unref(getProps);
if (showFooter && footerHeight) {
return isNumber(footerHeight)
? `${footerHeight}px`
: `${footerHeight.replace('px', '')}px`;
}
return `0px`;
});
const getScrollContentStyle = computed((): CSSProperties => {
const footerHeight = unref(getFooterHeight);
return {
position: 'relative',
height: `calc(100% - ${footerHeight})`,
};
});
const getLoading = computed(() => {
return !!unref(getProps)?.loading;
});
watch(
() => props.visible,
(newVal, oldVal) => {
if (newVal !== oldVal) visibleRef.value = newVal;
},
{ deep: true },
);
watch(
() => visibleRef.value,
(visible) => {
nextTick(() => {
emit('visible-change', visible);
instance && drawerInstance.emitVisible?.(visible, instance.uid);
});
},
);
// Cancel event
async function onClose(e: Recordable) {
const { closeFunc } = unref(getProps);
emit('close', e);
if (closeFunc && isFunction(closeFunc)) {
const res = await closeFunc();
visibleRef.value = !res;
return;
}
visibleRef.value = false;
}
function setDrawerProps(props: Partial<DrawerProps>): void {
// Keep the last setDrawerProps
propsRef.value = deepMerge(unref(propsRef) || ({} as any), props);
if (Reflect.has(props, 'visible')) {
visibleRef.value = !!props.visible;
}
}
function handleOk() {
emit('ok');
}
return {
onClose,
t,
prefixCls,
getMergeProps: getMergeProps as any,
getScrollContentStyle,
getProps: getProps as any,
getLoading,
getBindValues,
getFooterHeight,
handleOk,
};
},
});
</script>
<style lang="less">
@header-height: 60px;
@detail-header-height: 40px;
@prefix-cls: ~'basic-drawer';
@prefix-cls-detail: ~'basic-drawer__detail';
.@{prefix-cls} {
.ant-drawer-wrapper-body {
overflow: hidden;
}
.ant-drawer-close {
&:hover {
color: @error-color;
}
}
.ant-drawer-body {
height: calc(100% - @header-height);
padding: 0;
background-color: @component-background;
.scrollbar__wrap {
padding: 16px !important;
margin-bottom: 0 !important;
}
> .scrollbar > .scrollbar__bar.is-horizontal {
display: none;
}
}
}
.@{prefix-cls-detail} {
position: absolute;
.ant-drawer-header {
width: 100%;
height: @detail-header-height;
padding: 0;
border-top: 1px solid @border-color-base;
box-sizing: border-box;
}
.ant-drawer-title {
height: 100%;
}
.ant-drawer-close {
height: @detail-header-height;
line-height: @detail-header-height;
}
.scrollbar__wrap {
padding: 0 !important;
}
.ant-drawer-body {
height: calc(100% - @detail-header-height);
}
}
</style>

82
src/components/core/Drawer/src/components/DrawerFooter.vue

@ -0,0 +1,82 @@
<template>
<div :class="prefixCls" :style="getStyle" v-if="showFooter || $slots.footer">
<template v-if="!$slots.footer">
<slot name="insertFooter"></slot>
<a-button v-bind="cancelButtonProps" @click="handleClose" class="mr-2" v-if="showCancelBtn">
{{ cancelText }}
</a-button>
<slot name="centerFooter"></slot>
<a-button
:type="okType"
@click="handleOk"
v-bind="okButtonProps"
class="mr-2"
:loading="confirmLoading"
v-if="showOkBtn"
>
{{ okText }}
</a-button>
<slot name="appendFooter"></slot>
</template>
<template v-else>
<slot name="footer"></slot>
</template>
</div>
</template>
<script lang="ts">
import type { CSSProperties } from 'vue';
import { defineComponent, computed } from 'vue';
// import { useDesign } from '/@/hooks/web/useDesign';
import { footerProps } from '../props';
export default defineComponent({
name: 'BasicDrawerFooter',
props: {
...footerProps,
height: {
type: String,
default: '60px',
},
},
emits: ['ok', 'close'],
setup(props, { emit }) {
const prefixCls = 'basic-drawer-footer';
const getStyle = computed((): CSSProperties => {
const heightStr = `${props.height}`;
return {
height: heightStr,
lineHeight: `calc(${heightStr} - 1px)`,
};
});
function handleOk() {
emit('ok');
}
function handleClose() {
emit('close');
}
return { handleOk, prefixCls, handleClose, getStyle };
},
});
</script>
<style lang="less">
@prefix-cls: ~'basic-drawer-footer';
@footer-height: 60px;
.@{prefix-cls} {
position: absolute;
bottom: 0;
width: 100%;
padding: 0 12px 0 20px;
text-align: right;
background-color: @component-background;
border-top: 1px solid @border-color-base;
> * {
margin-right: 8px;
}
}
</style>

74
src/components/core/Drawer/src/components/DrawerHeader.vue

@ -0,0 +1,74 @@
<template>
<BasicTitle v-if="!isDetail" :class="prefixCls">
<slot name="title"></slot>
{{ !$slots.title ? title : '' }}
</BasicTitle>
<div :class="[prefixCls, `${prefixCls}--detail`]" v-else>
<span :class="`${prefixCls}__twrap`">
<span @click="handleClose" v-if="showDetailBack">
<ArrowLeftOutlined :class="`${prefixCls}__back`" />
</span>
<span v-if="title">{{ title }}</span>
</span>
<span :class="`${prefixCls}__toolbar`">
<slot name="titleToolbar"></slot>
</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import BasicTitle from '@/components/basic/basic-title/index.vue';
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
// import { useDesign } from '/@/hooks/web/useDesign';
import { propTypes } from '@/utils/propTypes';
export default defineComponent({
name: 'BasicDrawerHeader',
components: { BasicTitle, ArrowLeftOutlined },
props: {
isDetail: propTypes.bool,
showDetailBack: propTypes.bool,
title: propTypes.string,
},
emits: ['close'],
setup(_, { emit }) {
const prefixCls = 'basic-drawer-header';
function handleClose() {
emit('close');
}
return { prefixCls, handleClose };
},
});
</script>
<style lang="less">
@prefix-cls: ~'basic-drawer-header';
@footer-height: 60px;
.@{prefix-cls} {
display: flex;
height: 100%;
align-items: center;
&__back {
padding: 0 12px;
cursor: pointer;
&:hover {
color: @primary-color;
}
}
&__twrap {
flex: 1;
}
&__toolbar {
padding-right: 50px;
}
}
</style>

44
src/components/core/Drawer/src/props.ts

@ -0,0 +1,44 @@
import type { PropType } from 'vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
export const footerProps = {
confirmLoading: { type: Boolean },
/**
* @description: Show close button
*/
showCancelBtn: { type: Boolean, default: true },
cancelButtonProps: Object as PropType<Recordable>,
cancelText: { type: String, default: t('common.cancelText') },
/**
* @description: Show confirmation button
*/
showOkBtn: { type: Boolean, default: true },
okButtonProps: Object as PropType<Recordable>,
okText: { type: String, default: t('common.okText') },
okType: { type: String, default: 'primary' },
showFooter: { type: Boolean },
footerHeight: {
type: [String, Number] as PropType<string | number>,
default: 60,
},
};
export const basicProps = {
isDetail: { type: Boolean },
title: { type: String, default: '' },
loadingText: { type: String },
showDetailBack: { type: Boolean, default: true },
visible: { type: Boolean },
loading: { type: Boolean },
maskClosable: { type: Boolean, default: true },
getContainer: {
type: [Object, String] as PropType<any>,
},
closeFunc: {
type: [Function, Object] as PropType<any>,
default: null,
},
destroyOnClose: { type: Boolean },
...footerProps,
};

195
src/components/core/Drawer/src/typing.ts

@ -0,0 +1,195 @@
import type { ButtonProps } from 'ant-design-vue/lib/button/buttonTypes';
import type { ScrollContainerOptions } from '@/components/core/Container/index';
import type { CSSProperties, VNodeChild, ComputedRef } from 'vue';
export interface DrawerInstance {
setDrawerProps: (props: Partial<DrawerProps> | boolean) => void;
emitVisible?: (visible: boolean, uid: number) => void;
}
export interface ReturnMethods extends DrawerInstance {
openDrawer: <T = any>(visible?: boolean, data?: T, openOnSet?: boolean) => void;
closeDrawer: () => void;
getVisible?: ComputedRef<boolean>;
}
export type RegisterFn = (drawerInstance: DrawerInstance, uuid?: string) => void;
export interface ReturnInnerMethods extends DrawerInstance {
closeDrawer: () => void;
changeLoading: (loading: boolean) => void;
changeOkLoading: (loading: boolean) => void;
getVisible?: ComputedRef<boolean>;
}
export type UseDrawerReturnType = [RegisterFn, ReturnMethods];
export type UseDrawerInnerReturnType = [RegisterFn, ReturnInnerMethods];
export interface DrawerActionType {
scrollBottom: () => void;
scrollTo: (to: number) => void;
getScrollWrap: () => Element | null;
}
export interface DrawerFooterProps {
showOkBtn: boolean;
showCancelBtn: boolean;
/**
* Text of the Cancel button
* @default 'cancel'
* @type string
*/
cancelText: string;
/**
* Text of the OK button
* @default 'OK'
* @type string
*/
okText: string;
/**
* Button type of the OK button
* @default 'primary'
* @type string
*/
okType: 'primary' | 'danger' | 'dashed' | 'ghost' | 'default';
/**
* The ok button props, follow jsx rules
* @type object
*/
okButtonProps: { props: ButtonProps; on: {} };
/**
* The cancel button props, follow jsx rules
* @type object
*/
cancelButtonProps: { props: ButtonProps; on: {} };
/**
* Whether to apply loading visual effect for OK button or not
* @default false
* @type boolean
*/
confirmLoading: boolean;
showFooter: boolean;
footerHeight: string | number;
}
export interface DrawerProps extends DrawerFooterProps {
isDetail?: boolean;
loading?: boolean;
showDetailBack?: boolean;
visible?: boolean;
/**
* Built-in ScrollContainer component configuration
* @type ScrollContainerOptions
*/
scrollOptions?: ScrollContainerOptions;
closeFunc?: () => Promise<any>;
triggerWindowResize?: boolean;
/**
* Whether a close (x) button is visible on top right of the Drawer dialog or not.
* @default true
* @type boolean
*/
closable?: boolean;
/**
* Whether to unmount child components on closing drawer or not.
* @default false
* @type boolean
*/
destroyOnClose?: boolean;
/**
* Return the mounted node for Drawer.
* @default 'body'
* @type any ( HTMLElement| () => HTMLElement | string)
*/
getContainer?: () => HTMLElement | string;
/**
* Whether to show mask or not.
* @default true
* @type boolean
*/
mask?: boolean;
/**
* Clicking on the mask (area outside the Drawer) to close the Drawer or not.
* @default true
* @type boolean
*/
maskClosable?: boolean;
/**
* Style for Drawer's mask element.
* @default {}
* @type object
*/
maskStyle?: CSSProperties;
/**
* The title for Drawer.
* @type any (string | slot)
*/
title?: VNodeChild | JSX.Element;
/**
* The class name of the container of the Drawer dialog.
* @type string
*/
wrapClassName?: string;
class?: string;
/**
* Style of wrapper element which **contains mask** compare to `drawerStyle`
* @type object
*/
wrapStyle?: CSSProperties;
/**
* Style of the popup layer element
* @type object
*/
drawerStyle?: CSSProperties;
/**
* Style of floating layer, typically used for adjusting its position.
* @type object
*/
bodyStyle?: CSSProperties;
headerStyle?: CSSProperties;
/**
* Width of the Drawer dialog.
* @default 256
* @type string | number
*/
width?: string | number;
/**
* placement is top or bottom, height of the Drawer dialog.
* @type string | number
*/
height?: string | number;
/**
* The z-index of the Drawer.
* @default 1000
* @type number
*/
zIndex?: number;
/**
* The placement of the Drawer.
* @default 'right'
* @type string
*/
placement?: 'top' | 'right' | 'bottom' | 'left';
afterVisibleChange?: (visible?: boolean) => void;
keyboard?: boolean;
/**
* Specify a callback that will be called when a user clicks mask, close button or Cancel button.
*/
onClose?: (e?: Event) => void;
}

161
src/components/core/Drawer/src/useDrawer.ts

@ -0,0 +1,161 @@
import type {
UseDrawerReturnType,
DrawerInstance,
ReturnMethods,
DrawerProps,
UseDrawerInnerReturnType,
} from './typing';
import {
ref,
getCurrentInstance,
unref,
reactive,
watchEffect,
nextTick,
toRaw,
computed,
} from 'vue';
import { isProdMode } from '@/utils/env';
import { isFunction } from '@/utils/is';
import { tryOnUnmounted } from '@vueuse/core';
import { isEqual } from 'lodash-es';
import { error } from '@/utils/log';
const dataTransferRef = reactive<any>({});
const visibleData = reactive<{ [key: number]: boolean }>({});
/**
* @description: Applicable to separate drawer and call outside
*/
export function useDrawer(): UseDrawerReturnType {
if (!getCurrentInstance()) {
throw new Error('useDrawer() can only be used inside setup() or functional components!');
}
const drawer = ref<DrawerInstance | null>(null);
const loaded = ref<Nullable<boolean>>(false);
const uid = ref<string>('');
function register(drawerInstance: DrawerInstance, uuid: string) {
isProdMode() &&
tryOnUnmounted(() => {
drawer.value = null;
loaded.value = null;
dataTransferRef[unref(uid)] = null;
});
if (unref(loaded) && isProdMode() && drawerInstance === unref(drawer)) {
return;
}
uid.value = uuid;
drawer.value = drawerInstance;
loaded.value = true;
drawerInstance.emitVisible = (visible: boolean, uid: number) => {
visibleData[uid] = visible;
};
}
const getInstance = () => {
const instance = unref(drawer);
if (!instance) {
error('useDrawer instance is undefined!');
}
return instance;
};
const methods: ReturnMethods = {
setDrawerProps: (props: Partial<DrawerProps>): void => {
getInstance()?.setDrawerProps(props);
},
getVisible: computed((): boolean => {
return visibleData[~~unref(uid)];
}),
openDrawer: <T = any>(visible = true, data?: T, openOnSet = true): void => {
getInstance()?.setDrawerProps({
visible: visible,
});
if (!data) return;
if (openOnSet) {
dataTransferRef[unref(uid)] = null;
dataTransferRef[unref(uid)] = toRaw(data);
return;
}
const equal = isEqual(toRaw(dataTransferRef[unref(uid)]), toRaw(data));
if (!equal) {
dataTransferRef[unref(uid)] = toRaw(data);
}
},
closeDrawer: () => {
getInstance()?.setDrawerProps({ visible: false });
},
};
return [register, methods];
}
export const useDrawerInner = (callbackFn?: Fn): UseDrawerInnerReturnType => {
const drawerInstanceRef = ref<Nullable<DrawerInstance>>(null);
const currentInstance = getCurrentInstance();
const uidRef = ref<string>('');
if (!getCurrentInstance()) {
throw new Error('useDrawerInner() can only be used inside setup() or functional components!');
}
const getInstance = () => {
const instance = unref(drawerInstanceRef);
if (!instance) {
error('useDrawerInner instance is undefined!');
return;
}
return instance;
};
const register = (modalInstance: DrawerInstance, uuid: string) => {
isProdMode() &&
tryOnUnmounted(() => {
drawerInstanceRef.value = null;
});
uidRef.value = uuid;
drawerInstanceRef.value = modalInstance;
currentInstance?.emit('register', modalInstance, uuid);
};
watchEffect(() => {
const data = dataTransferRef[unref(uidRef)];
if (!data) return;
if (!callbackFn || !isFunction(callbackFn)) return;
nextTick(() => {
callbackFn(data);
});
});
return [
register,
{
changeLoading: (loading = true) => {
getInstance()?.setDrawerProps({ loading });
},
changeOkLoading: (loading = true) => {
getInstance()?.setDrawerProps({ confirmLoading: loading });
},
getVisible: computed((): boolean => {
return visibleData[~~unref(uidRef)];
}),
closeDrawer: () => {
getInstance()?.setDrawerProps({ visible: false });
},
setDrawerProps: (props: Partial<DrawerProps>) => {
getInstance()?.setDrawerProps(props);
},
},
];
};

4
src/components/core/Tree/src/BasicTree.vue

@ -355,7 +355,7 @@
if (!nodeShow) return null;
if (!hasPermission(item.auth as string)) {
if (item.auth && !hasPermission(item.auth as string)) {
return null;
}
@ -427,7 +427,7 @@
const showTitle = title || toolbar || search || slots.headerTitle;
const scrollStyle: CSSProperties = { height: 'calc(100% - 38px)' };
return (
<div class={['basic-tree', 'h-full', attrs.class]}>
<div class={['basic-tree', 'h-[90%]', attrs.class]}>
{showTitle && (
<TreeHeader
checkable={checkable}

22
src/shims-vue.d.ts

@ -1,7 +1,21 @@
/*
* @Author: AaronWu 2463371514@qq.com
* @Date: 2025-03-31 15:12:17
* @LastEditors: AaronWu 2463371514@qq.com
* @LastEditTime: 2025-04-01 10:39:52
* @FilePath: /IssueSupportManage/src/shims-vue.d.ts
* @Description: ,`customMade`, koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
declare module 'nprogress'
declare module '*.md' {
import type { ComponentOptions } from 'vue';
const Component: ComponentOptions;
export default Component;
}
declare module 'nprogress';

38
src/utils/env.ts

@ -0,0 +1,38 @@
/**
* @description: Development mode
*/
export const devMode = 'development';
/**
* @description: Production mode
*/
export const prodMode = 'production';
/**
* @description: Get environment variables
* @returns:
* @example:
*/
export function getEnv(): string {
return import.meta.env.MODE;
}
/**
* @description: Is it a development mode
* @returns:
* @example:
*/
export function isDevMode(): boolean {
return import.meta.env.DEV;
}
/**
* @description: Is it a production mode
* @returns:
* @example:
*/
export function isProdMode(): boolean {
return import.meta.env.PROD;
}

9
src/utils/log.ts

@ -0,0 +1,9 @@
const projectName = import.meta.env.VITE_GLOB_APP_TITLE;
export function warn(message: string) {
console.warn(`[${projectName} warn]:${message}`);
}
export function error(message: string) {
throw new Error(`[${projectName} error]:${message}`);
}

151
src/views/dashboard/welcome/index.vue

@ -1,85 +1,90 @@
<!--
功能功能描述
作者Aaron.Wu
时间2023年05月25日 17:00:26
版本v1.0
修改记录
修改内容
修改人员
修改时间
* @Author: AaronWu 2463371514@qq.com
* @Date: 2025-03-31 15:12:17
* @LastEditors: AaronWu 2463371514@qq.com
* @LastEditTime: 2025-04-01 10:40:23
* @FilePath: /IssueSupportManage/src/views/dashboard/welcome/index.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<div class="w-200px">
<!-- <button @click="handleClick">222</button>
<BasicTree
ref="treeRef"
:clickRowToExpand="false"
:beforeRightClick="getRightMenuList"
:loading="treeLoading"
title="77777"
:treeData="treeData"
checkStrictly
checkable
highlight
search
toolbar
@select="handleSelect"
:checkedKeys="['0-0']"
>
</BasicTree> -->
<div class="welcome-container">
<a-card class="doc-card">
<template #title>
<div class="card-title">
<span>接口文档</span>
<a-tag color="blue">DTO</a-tag>
</div>
</template>
<!-- <div class="markdown-body" v-html="mdContent"></div> -->
<DTO></DTO>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { ContextMenuItem } from '@/components/basic/ContextMenu';
import { BasicTree } from '@/components/core/Tree';
import { PlusOutlined } from '@ant-design/icons-vue';
import { Menu } from 'ant-design-vue';
import { h, ref } from 'vue';
import { ref, onMounted } from 'vue';
import DTO from '@/assets/DTO.md';
// console.log('DTO: ', DTO);
// const mdContent = ref(DTO);
</script>
const treeRef = ref<Nullable<any>>(null);
const treeData = ref<any[]>([
{
title: 'parent 1',
key: '0-0',
children: [
{
title: 'parent 1-0',
key: '0-0-0',
disabled: true,
children: [
{ title: 'leaf', key: '0-0-0-0', disableCheckbox: true },
{ title: 'leaf', key: '0-0-0-1' },
],
},
{
title: 'parent 1-1',
key: '0-0-1',
children: [{ key: '0-0-1-0', title: 'sss' }],
},
],
},
]);
const treeLoading = ref<boolean>(false);
<style lang="less" scoped>
.welcome-container {
padding: 24px;
const handleSelect = (key, row) => {
console.log('row: ', row);
console.log('key: ', key);
};
.doc-card {
background: #fff;
border-radius: 8px;
const handleClick = () => {
console.log(treeRef.value.checkAll(true));
treeRef.value.setCheckedKeys(['0-0'])
};
.card-title {
display: flex;
align-items: center;
gap: 12px;
//
function getRightMenuList(node: any): ContextMenuItem[] {
return [
{
label: '新增',
handler: () => {},
},
];
span {
font-size: 16px;
font-weight: 500;
}
</script>
<style lang="less" scoped></style>
}
}
:deep(.markdown-body) {
padding: 16px 0;
table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
th,
td {
padding: 12px;
border: 1px solid #e8e8e8;
}
th {
background: #fafafa;
font-weight: 500;
}
tr:hover {
background: #fafafa;
}
}
h2 {
font-size: 24px;
margin: 24px 0 16px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
}
h3 {
font-size: 18px;
margin: 20px 0 12px;
color: #1a1a1a;
}
}
}
</style>

2
src/views/login/index.vue

@ -4,7 +4,7 @@
<div class="login-content">
<div class="login-header">
<div class="logo-wrapper">
<img src="@/assets/logo.png" alt="logo" class="logo-img" />
<!-- <img src="@/assets/logo.png" alt="logo" class="logo-img" /> -->
</div>
<h1 class="login-title">问题工单管理系统</h1>
<div class="title-divider">

14
src/views/system/dictionary/Edit.vue

@ -1,14 +0,0 @@
<!--
功能功能描述
作者Aaron
时间2023年06月16日 14:53:40
版本v1.0
修改记录
修改内容
修改人员
修改时间
-->
<template> </template>
<script lang="tsx" setup></script>
<style lang="less" scoped></style>

330
src/views/system/dictionary/Tree.vue

@ -0,0 +1,330 @@
<template>
<div class="bg-white m-4 mr-2 overflow-hidden h-full">
<BasicTree
ref="treeRef"
:actionList="actionList"
:beforeRightClick="getRightMenuList"
:checkable="!query"
:clickRowToExpand="false"
:treeData="treeData"
checkStrictly
:defaultExpandAll="true"
search
toolbar
:highlight="true"
@select="handleSelect"
:toolbarStrictly="false"
>
<!-- <template #titleBefore="item"> </template> -->
</BasicTree>
<div v-if="query" class="m-4 flex justify-center">
<a-button class="w-[80%]" type="primary" @click="handleReset()">重置</a-button>
<!-- <Checkbox v-model:checked="recursion" @change="handleQuery()">本级及子级</Checkbox> -->
</div>
<div v-else class="m-4 flex justify-center">
<a-button
type="primary"
class="w-[80%]"
v-permission="{ action: RoleEnum.DICT_ADD, effect: 'disabled' }"
@click="openTypeModal({})"
>
{{ '新增类型' }}
</a-button>
<!-- <a-button
v-permission="{ action: RoleEnum.DICT_DELETE, effect: 'disabled' }"
class="mr-2"
@click="handleBatchDelete()"
>
{{ '删除' }}
</a-button> -->
</div>
<!-- <OrgRole @register="registerModal" @success="handleSuccess" /> -->
</div>
</template>
<script lang="tsx">
import { defineComponent, h, onMounted, ref, unref } from 'vue';
import { Checkbox, message, Tag } from 'ant-design-vue'; // antd
import { useI18n } from '@/hooks/useI18n';
import {
BasicTree,
ContextMenuItem,
TreeActionItem,
TreeActionType,
TreeItem,
} from '@/components/core/Tree';
import { eachTree, findChildrenByParentId, findNodeByKey } from '@/utils/helper/treeHelper';
import { RoleEnum } from '@/enums/roleEnum';
import { OrgTypeEnum } from '@/enums/biz/base';
import { useFormModal } from '@/hooks/useModal/index';
import { createDict, deleteBatchDictById, fetchDictList, updateDict } from '@/api/dict/index';
// import { useModal } from '@/components/Modal';
// import OrgRole from './orgRole/index.vue';
import { useMessage } from '@/hooks/useMessage';
import { mockTree } from './mockData';
import { Nullable } from '@/utils/types';
import { typeFormSchema } from './formSchemas';
import { TableListItem } from './columns';
import { omit } from 'lodash-es';
import { isArray } from '@/utils/is';
export default defineComponent({
name: 'BaseDictValueManagement',
components: {
BasicTree,
// OrgRole,
Checkbox,
Tag,
},
props: {
query: {
type: Boolean,
default: false,
},
},
emits: ['select', 'add', 'edit', 'change', 'reset'],
setup(props, { emit }) {
const { t } = useI18n();
const { createMessage, createConfirm } = useMessage();
const treeRef = ref<Nullable<TreeActionType>>(null);
const treeData = ref<TreeItem[]>([]);
const recursion = ref<boolean>(false);
//
// const [registerModal, { openModal }] = useModal();
const [showModal] = useFormModal();
/**
* @description 打开弹窗
*/
const openTypeModal = async (record: Partial<TableListItem> = {}, isReadOnly = false) => {
const [formRef] = await showModal<any>({
modalProps: {
title: `${isReadOnly ? '查看' : record.id ? '编辑' : '新增'}类型`,
width: 500,
onFinish: async (values) => {
values.id = record.id;
await (record.id ? updateDict : createDict)(values);
message.success(`${record.id ? '编辑' : '新增'}成功`);
fetch();
},
},
formProps: {
labelWidth: 100,
schemas: typeFormSchema,
autoSubmitOnEnter: true,
},
});
formRef?.setFieldsValue(record);
};
function getTree() {
const tree = unref(treeRef);
if (!tree) {
throw new Error('树结构加载失败,请刷新页面');
}
return tree;
}
onMounted(() => {
fetch();
});
//
async function fetch() {
// const types = (await fetchDictList()) as unknown as TreeItem[];
const types = mockTree;
console.log('types: ', types);
treeData.value = types.map((e) => {
return {
...e,
name: e.name,
editable: true,
};
}) as unknown as TreeItem[];
eachTree(treeData.value, (item) => {
item.key = item.id;
item.title = item.name;
item.slots = { titleBefore: 'titleBefore' };
return item;
});
console.log('treeData.value: ', treeData.value);
console.log('[treeData.value[0].id]: ', [treeData.value[0].id]);
if (isArray(treeData.value) && treeData.value.length > 0) {
getTree().setSelectedKeys([treeData.value[0].id]);
handleSelect([treeData.value[0].id]);
}
// setTimeout(() => {
// getTree().filterByLevel(2);
// }, 0);
}
//
function handleSelect(keys: string[]) {
console.log('keys: ', keys);
if (keys[0]) {
const node = findNodeByKey(keys[0], treeData.value);
const parent = findNodeByKey(node?.parentId, treeData.value);
let childrenIds: string[] = [];
if (recursion.value) {
childrenIds = findChildrenByParentId(keys[0], treeData.value);
} else {
childrenIds = [node.id];
}
emit('select', parent, node, childrenIds);
} else {
emit('select', {}, {});
}
}
let actionList: TreeActionItem[] = [];
let getRightMenuList = (_: any): ContextMenuItem[] => {
return [];
};
if (!props.query) {
//
actionList = [
{
render: (node) => {
if (node.editable) {
return h(
'a',
{
class: 'ml-2',
onClick: (e: Event) => {
e?.stopPropagation();
e?.preventDefault();
const current = findNodeByKey(node?.id, treeData.value);
// const parent = findNodeByKey(node?.parentId, treeData.value);
// emit('edit', parent, current);
openTypeModal(current);
},
},
'编辑',
);
}
},
},
{
render: (node) => {
if (node.editable) {
return h(
'a',
{
class: 'ml-2',
onClick: (e: Event) => {
e?.stopPropagation();
e?.preventDefault();
batchDelete([node.id]);
},
},
'删除',
);
}
},
},
];
//
// getRightMenuList = (node: any): ContextMenuItem[] => {
// return [
// {
// label: '',
// auth: RoleEnum.DICT_EDIT,
// handler: () => {
// const current = findNodeByKey(unref(node)?.id, treeData.value);
// const parent = findNodeByKey(unref(node)?.parentId, treeData.value);
// emit('edit', parent, current);
// },
// },
// {
// label: '',
// auth: RoleEnum.DICT_DELETE,
// handler: () => {
// batchDelete([unref(node).id]);
// },
// },
// ];
// };
}
//
async function batchDelete(ids: string[]) {
createConfirm({
iconType: 'warning',
content: '选中节点及其子结点将被永久删除, 是否确定删除?',
onOk: async () => {
try {
await deleteBatchDictById(ids);
createMessage.success(t('common.tips.deleteSuccess'));
fetch();
} catch (e) {}
},
});
}
//
function handleAdd() {
emit('add', findNodeByKey('0', treeData.value));
}
//
function handleBatchDelete() {
const { checked } = getTree().getCheckedKeys() as {
checked: string[];
halfChecked: string[];
};
if (!checked || checked.length <= 0) {
createMessage.warning(t('common.tips.pleaseSelectTheData'));
return;
}
batchDelete(checked);
}
//
function changeDisplay() {
emit('change', '2');
}
//
function handleReset() {
getTree().setSelectedKeys([]);
emit('reset');
}
//
function handleQuery() {
handleSelect(getTree().getSelectedKeys() as string[]);
}
//
function handleSuccess() {
fetch();
}
return {
t,
treeRef,
treeData,
fetch,
handleAdd,
handleBatchDelete,
getRightMenuList,
actionList,
handleSelect,
changeDisplay,
handleReset,
handleQuery,
RoleEnum,
// registerModal,
handleSuccess,
recursion,
OrgTypeEnum,
openTypeModal,
};
},
});
</script>

154
src/views/system/dictionary/columns.tsx

@ -1,132 +1,80 @@
/*
* @Author: AaronWu 2463371514@qq.com
* @Date: 2025-04-01 13:43:34
* @LastEditors: AaronWu 2463371514@qq.com
* @LastEditTime: 2025-04-01 13:52:02
* @FilePath: /IssueSupportManage/src/views/system/dictionary/columns.tsx
* @Description: ,`customMade`, koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import type { TableColumn } from '@/components/core/dynamic-table';
import { formatToDateTime } from '@/utils/dateUtil';
import { h } from 'vue';
import { Switch } from 'ant-design-vue';
import { disableDictValue, enableDictValue } from '@/api/dict';
export type TableListItem = API.DictionaryInfoType;
export type DictionaryTableListItem = API.DictionaryItemInfoType;
export type TableListItem = API.DictValueType;
export type TableColumnItem = TableColumn<TableListItem>;
export type DictionaryTableColumnItem = TableColumn<DictionaryTableListItem>;
// 数据项类型
// export type ListItemType = typeof tableData[number];
// 使用TableColumn<ListItemType> 将会限制dataIndex的类型,但换来的是dataIndex有类型提示
export const baseColumns: TableColumnItem[] = [
{
title: '标识',
title: '字段值',
align: 'center',
dataIndex: 'dictKey',
width: 200,
dataIndex: 'fieldValue',
width: 150,
ellipsis: true,
resizable: true,
// defaultEditable: true,
// editable: ({ index }) => {
// // 第一行不允许被编辑
// return index > 0;
// },
formItemProps: {
defaultValue: '',
required: false,
},
},
{
title: '名称',
title: '启用状态',
align: 'center',
dataIndex: 'name',
width: 200,
dataIndex: 'enable',
width: 150,
ellipsis: true,
resizable: true,
formItemProps: {
defaultValue: '',
required: false,
},
},
component: 'Select',
defaultValue: '1',
componentProps: {
options: [
{
title: '状态',
align: 'center',
dataIndex: 'status',
hideInSearch: true,
width: 100,
customRender: ({ record }) => {
return (
<a-tag color={record.status ? 'success' : 'error'}>{record.status ? '启用' : '禁用'}</a-tag>
);
},
label: '启用',
value: '1',
},
{
title: '创建时间',
align: 'center',
dataIndex: 'createTime',
width: 200,
formItemProps: {
defaultValue: '',
required: false,
component: 'RangePicker',
label: '禁用',
value: '0',
},
customRender: ({ record }) => `${formatToDateTime(record.createTime)}`,
},
];
export const dictItemColumns: DictionaryTableColumnItem[]=[
{
title: '标识',
align: 'center',
dataIndex: 'dictKey',
width: 200,
resizable: true,
defaultEditable: true,
// editable: ({ index }) => {
// // 第一行不允许被编辑
// return index > 0;
// },
formItemProps: {
defaultValue: '',
required: false,
],
},
},
{
title: '名称',
align: 'center',
dataIndex: 'name',
width: 200,
resizable: true,
formItemProps: {
defaultValue: '',
required: false,
},
},
{
title: '状态',
align: 'center',
dataIndex: 'status',
hideInSearch: true,
width: 100,
customRender: ({ record }) => {
return (
<a-tag color={record.status ? 'success' : 'error'}>{record.status ? '启用' : '禁用'}</a-tag>
);
},
},
{
title: '排序',
align: 'center',
dataIndex: 'sortValue',
hideInSearch: true,
width: 200,
resizable: true,
formItemProps: {
defaultValue: '',
required: false,
},
},
{
title: '创建时间',
align: 'center',
dataIndex: 'createTime',
hideInSearch: true,
width: 200,
formItemProps: {
defaultValue: '',
required: false,
component: 'RangePicker',
const onChange = (checked: boolean) => {
record.pendingStatus = true;
const newState = checked ? '1' : '0';
record.enable = newState;
(checked ? enableDictValue : disableDictValue)({ id: record.id! })
.then(() => {
record.enable = newState;
})
.catch(() => {})
.finally(() => {
record.pendingStatus = false;
});
};
// 渲染函数写法
return h(Switch, {
checked: record.enable === '1',
loading: record.pendingStatus,
onChange,
});
},
customRender: ({ record }) => `${formatToDateTime(record.createTime)}`,
},
]
];

62
src/views/system/dictionary/formSchemas.ts

@ -1,44 +1,62 @@
import type { FormSchema } from '@/components/core/schema-form/';
export const schemas: FormSchema[] = [
// 列表编辑页字段
export const editFormSchema: FormSchema[] = [
{
field: 'dictKey',
field: 'id',
label: 'ID',
component: 'Input',
label: '标识',
colProps: {
span: 12,
},
rules: [{ required: true }],
vShow: false,
},
{
field: 'name',
label: '字段值',
field: 'fieldValue',
component: 'Input',
label: '名称',
colProps: {
span: 12,
span: 24,
},
rules: [{ required: true }],
},
{
field: 'status',
component: 'Switch',
label: '状态',
label: '启用状态',
field: 'enable',
component: 'RadioGroup',
defaultValue: '1',
colProps: {
span: 12,
},
componentProps: {
options: [
{
label: '启用',
value: '1',
},
{
field: 'remark',
component: 'InputTextArea',
label: '备注',
colProps: {
span: 12,
label: '禁用',
value: '0',
},
],
},
rules: [{ required: true, type: 'string' }],
},
];
// 新增类别表单
export const typeFormSchema: FormSchema[] = [
{
field: 'id',
label: 'ID',
component: 'Input',
vShow: false,
},
{
field: 'dictItems',
label: '字典条目',
slot: 'dictItems',
label: '名称',
field: 'name',
component: 'Input',
colProps: {
span: 24,
},
rules: [{ required: true }],
},
];

344
src/views/system/dictionary/index.vue

@ -1,293 +1,203 @@
<!--
功能功能描述
作者Aaron.Wu
时间2023年05月25日 17:00:26
版本v1.0
修改记录
修改内容
修改人员
修改时间
* @Author: AaronWu 2463371514@qq.com
* @Date: 2025-04-01 13:17:38
* @LastEditors: AaronWu 2463371514@qq.com
* @LastEditTime: 2025-04-01 14:43:11
* @FilePath: /IssueSupportManage/src/views/system/dictionary/index.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<SplitPanel>
<template #left-content>
<BaseTypeTree ref="treeRef" class="" @reset="handleReset" @select="handleTypeSelect" />
</template>
<template #right-content>
<DynamicTable
search-type="drawer"
size="small"
showIndex
headerTitle="系统字典列表"
headerTitle="字段值列表"
titleTooltip=""
:data-request="loadData"
:columns="columns"
row-key="id"
@resize-column="handleResizeColumn"
:row-selection="rowSelection"
:scroll="{ x: '100%' }"
>
<template v-if="isCheckRows" #title>
<template v-if="isCheckRows" #[`title`]>
<Alert class="w-full" type="info" show-icon>
<template #message>
已选 {{ isCheckRows }}
已选 {{ rowSelection.selectedRowKeys.length }}
<a-button type="link" @click="rowSelection.selectedRowKeys = []">取消选择</a-button>
</template>
</Alert>
</template>
<template #toolbar>
<template #[`toolbar`]>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-button
type="danger"
:disabled="!isCheckRows"
@click="delRowConfirm(rowSelection.selectedRowKeys)"
>
<DeleteOutlined /> 删除
</a-button>
</template>
</DynamicTable>
<DraggableModal :title="modalTitle" width="800px" v-model:visible="state.visible" @ok="onOk">
<SchemaForm>
<template #dictItems="{ formModel, field }">
<DynamicTable
size="small"
bordered
showIndex
headerTitle=""
titleTooltip=""
:data-request="loadDicItemData"
:columns="dictItemColumns"
editable-type="cell"
row-key="id"
@resize-column="handleResizeColumn"
:on-save="handleSave"
:on-cancel="handleCancelSave"
:row-selection="dictRowSelection"
:scroll="{ x: '100%' }"
>
<template #toolbar>
<a-button type="primary" size="small" @click="handleDictAdd">新增</a-button>
<a-button type="danger" size="small" :disabled="!isCheckDictRows" @click="">
<a-button type="danger" @click="delRowConfirm(rowSelection.selectedRowKeys)">
<DeleteOutlined /> 删除
</a-button>
</template>
</DynamicTable>
</template>
</SchemaForm>
</DraggableModal>
</SplitPanel>
</template>
<script lang="tsx" setup>
import {
baseColumns,
type TableListItem,
dictItemColumns,
type DictionaryTableListItem,
} from './columns';
import {
useTable,
type OnChangeCallbackParams,
OnSave,
OnCancel,
} from '@/components/core/dynamic-table';
import {
fetchDictionaryPageList,
createDictionary,
updateDictionary,
deleteDictionaryById,
deleteBatchDictionaryById,
} from '@/api/dictionary';
import { computed, reactive, ref, watchEffect } from 'vue';
import { Modal, message, Alert } from 'ant-design-vue';
import { useFormModal } from '@/hooks/useModal/';
import { schemas } from './formSchemas';
import { computed, h, ref } from 'vue';
import { SplitPanel } from '@/components/basic/split-panel';
import BaseTypeTree from './Tree.vue';
import { OnChangeCallbackParams, useTable } from '@/components/core/dynamic-table';
import { baseColumns } from './columns';
import { message, Modal, Alert } from 'ant-design-vue';
import { ExclamationCircleOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import { DraggableModal } from '@/components/core/draggable-modal';
import { useForm } from '@/components/core/schema-form';
defineOptions({
name: 'dictionary-maintenance',
});
const state = reactive<{
visible: boolean;
isReadOnly: boolean;
record: any;
}>({
visible: false,
isReadOnly: false,
record: {},
});
const modalTitle = computed(() => {
return `${state.isReadOnly ? '查看' : state.record?.id ? '编辑' : '新增'}字典`;
});
const [SchemaForm, formRef] = useForm({
parentVisiable: state.visible,
labelWidth: 100,
schemas,
showActionButtonGroup: false,
actionColOptions: {
span: 24,
},
submitButtonOptions: {
text: '保存',
},
});
watchEffect(() => {
formRef.setSchemaFormProps({
disabled: state.isReadOnly,
showActionButtonGroup: !state.isReadOnly,
});
});
const onOk = () => {};
const [showModal] = useFormModal();
import { useFormModal } from '@/hooks/useModal';
import { editFormSchema } from './formSchemas';
import {
createDictValue,
deleteBatchDictValueById,
deleteDictValueById,
fetchDictValuePageList,
updateDictValue,
} from '@/api/dict';
const columns: any = [
...baseColumns,
{
title: '操作',
width: 200,
width: 50,
dataIndex: 'ACTION',
hideInSearch: true,
align: 'center',
fixed: 'right',
actions: ({ record }) => [
{
icon: 'searchoutlined',
color: '#3b82f6',
label: '查看',
// auth: {
// perm: 'sys.menu.update',
// effect: 'disable',
// },
onClick: () => handleShow(record),
},
{
icon: 'edit',
color: '#3b82f6',
size: '15',
label: '修改',
// auth: {
// perm: 'sys.menu.update',
// effect: 'disable',
// },
label: '编辑',
onClick: () => handleEdit(record),
},
{
icon: 'delete',
color: '#ec6f6f',
label: '删除',
popConfirm: {
title: '确定要删除吗?',
onConfirm: () => handleDelete(record.id),
},
onClick: () => handleDelete(record.id),
},
],
},
];
const treeRef = ref<InstanceType<typeof BaseTypeTree>>();
const [showModal] = useFormModal();
const openModal = () => {
state.visible = true;
};
const handleAdd = () => {
state.record = {};
openModal();
};
const handleEdit = (record: API.DictionaryInfoType) => {
state.record = record;
openModal();
};
const currentType = ref<API.DictType>({
id: '',
name: '',
});
const handleShow = (record: API.DictionaryInfoType) => {
state.record = record;
state.isReadOnly = true;
openModal();
};
const [DynamicTable, dynamicTableInstance] = useTable();
const handleDelete = (id: string) => {
delRowConfirm(id);
};
function handleTypeSelect(_parent = {}, record: API.DictType, childrenIds = []) {
console.log('record: ', record);
currentType.value = record;
dynamicTableInstance?.reload();
}
const handleSave: OnSave = async (rowKey, record, originRow) => {
console.log('handleSave', rowKey, record, originRow);
function handleReset() {
currentType.value = {
id: '',
name: '',
};
dynamicTableInstance?.reload();
}
const handleCancelSave: OnCancel = (rowKey, record, originRow) => {
console.log('handleCancelSave', rowKey, record, originRow);
};
/**
* @description 表格删除行
*/
const delRowConfirm = async (id: string | string[]) => {
Modal.confirm({
title: '确定要删除所选的字吗?',
title: '确定要删除所选的字段值吗?',
icon: <ExclamationCircleOutlined />,
centered: true,
onOk: async () => {
if (Array.isArray(id)) {
//
await deleteBatchDictionaryById(id).finally(dynamicTableInstance?.reload);
await deleteBatchDictValueById(id).finally(dynamicTableInstance?.reload);
} else {
await deleteDictionaryById({ id }).finally(dynamicTableInstance?.reload);
await deleteDictValueById({ id }).finally(dynamicTableInstance?.reload);
}
},
});
};
const [DynamicTable, dynamicTableInstance] = useTable();
const handleAdd = () => {
// openDrawer(true, { type: ActionEnum.ADD, record: {} });
const handleDictAdd = () => {
const emptyDictItem: DictionaryTableListItem = {
id: '',
name: '',
dictKey: '',
sortValue: '',
status: true,
// modal
openModal({});
};
dynamicTableInstance.tableData.unshift(emptyDictItem);
const handleEdit = (record: API.DictValueType) => {
if (record.enable === '1') {
message.warning('启用状态不可编辑');
return;
}
//modal
openModal(record);
};
const rowSelection = ref<any>({
selectedRowKeys: [] as string[],
onChange: (selectedRowKeys: string[], selectedRows: API.DictionaryInfoType[]) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
rowSelection.value.selectedRowKeys = selectedRowKeys;
const handleDelete = (id: string) => {
delRowConfirm(id);
};
/**
* @description 打开弹窗
*/
const openModal = async (record: Partial<API.DictValueType> = {}, isReadOnly = false) => {
if (!currentType.value.id) {
message.warning('请先选择左侧字段类别');
return;
}
const [formRef] = await showModal<any>({
modalProps: {
title: `${isReadOnly ? '查看' : record.id ? '编辑' : '新增'}字段值`,
width: 700,
onFinish: async (values) => {
console.log('新增/编辑字段值', values);
values.id = record.id;
values.fieldTypeId = currentType.value.id;
await (record.id ? updateDictValue : createDictValue)(values);
message.success(`${record.id ? '编辑' : '新增'}成功`);
dynamicTableInstance?.reload();
},
footer: isReadOnly ? null : undefined,
},
formProps: {
labelWidth: 100,
schemas: editFormSchema,
autoSubmitOnEnter: true,
disabled: isReadOnly,
},
});
const dictRowSelection = ref<any>({
formRef?.setFieldsValue(record);
// if (record?.id) {
// const { roles } = await getTenantInfo({ employeeId: record.id });
// formRef?.setFieldsValue({ roles });
// }
};
const rowSelection = ref<any>({
selectedRowKeys: [] as string[],
onChange: (selectedRowKeys: string[], selectedRows: API.DictionaryInfoType[]) => {
onChange: (selectedRowKeys: string[], selectedRows: API.DictValueType[]) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
dictRowSelection.value.selectedRowKeys = selectedRowKeys;
rowSelection.value.selectedRowKeys = selectedRowKeys;
},
});
//
const isCheckRows = computed(() => rowSelection.value.selectedRowKeys.length);
const isCheckDictRows = computed(() => dictRowSelection.value.selectedRowKeys.length);
const loadDicItemData = async (
params,
onChangeParams?: OnChangeCallbackParams,
): Promise<API.TableListResult> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
list: [
{
id: '1',
name: '1',
dictKey: '1',
},
],
...params,
});
}, 500);
});
};
const loadData = async (
params,
@ -296,24 +206,9 @@
console.log('params', params);
console.log('onChangeParams', onChangeParams);
return new Promise((resolve) => {
setTimeout(() => {
resolve({
list: [
{
id: '1',
name: '1',
},
],
const res = await fetchDictValuePageList({
...params,
});
}, 500);
});
const res = await fetchDictionaryPageList({
model: {
...params,
},
fieldTypeId: currentType.value.id,
current: params.page,
size: params.limit,
});
@ -321,9 +216,10 @@
rowSelection.value.selectedRowKeys = [];
return {
list: res.data,
list: res.data.list,
pagination: {
total: Number(res.totalCount),
total: Number(res.data.total),
size: params.limit,
...params,
},
};
@ -334,8 +230,4 @@
};
</script>
<style lang="less" scoped>
.action-divider {
margin: 0 5px;
}
</style>
<style lang="less" scoped></style>

14
src/views/system/dictionary/mockData.ts

@ -0,0 +1,14 @@
/*
* @Author: AaronWu 2463371514@qq.com
* @Date: 2025-04-01 13:24:31
* @LastEditors: AaronWu 2463371514@qq.com
* @LastEditTime: 2025-04-01 13:25:12
* @FilePath: /IssueSupportManage/src/views/system/dictionary/mockData.ts
* @Description: ,`customMade`, koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
export const mockTree: any[] = [
{
id: 1,
name: '字典类别',
},
];

34
src/views/system/user/columns.tsx

@ -11,18 +11,18 @@ export type TableColumnItem = TableColumn<TableListItem>;
// export type ListItemType = typeof tableData[number];
// 使用TableColumn<ListItemType> 将会限制dataIndex的类型,但换来的是dataIndex有类型提示
export const baseColumns: TableColumnItem[] = [
// {
// title: '用户名',
// align: 'center',
// dataIndex: 'username',
// // sorter: true,
// width: 150,
// resizable: true,
// formItemProps: {
// defaultValue: '',
// required: false,
// },
// },
{
title: '账号',
align: 'center',
dataIndex: 'account',
// sorter: true,
width: 150,
resizable: true,
formItemProps: {
defaultValue: '',
required: false,
},
},
{
title: '姓名',
align: 'center',
@ -44,16 +44,6 @@ export const baseColumns: TableColumnItem[] = [
required: false,
},
},
// {
// title: '身份证',
// align: 'center',
// dataIndex: 'idCard',
// width: 150,
// formItemProps: {
// defaultValue: '',
// required: false,
// },
// },
{
title: '性别',

49
src/views/system/user/formSchemas.ts

@ -1,25 +1,34 @@
/*
* @Author: AaronWu 2463371514@qq.com
* @Date: 2025-04-01 09:08:06
* @LastEditors: AaronWu 2463371514@qq.com
* @LastEditTime: 2025-04-01 16:48:50
* @FilePath: /IssueSupportManage/src/views/system/user/formSchemas.ts
* @Description: ,`customMade`, koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import type { FormSchema } from '@/components/core/schema-form/';
import { sexTypeList } from './data';
export const userSchemas: FormSchema<API.CreateUserParams>[] = [
// {
// field: 'username',
// component: 'Input',
// label: '用户名',
// colProps: {
// span: 12,
// },
// rules: [{ required: true }],
// },
{
field: 'realName',
field: 'account',
component: 'Input',
label: '姓名',
label: '账号',
colProps: {
span: 12,
},
rules: [{ required: true }],
},
{
field: 'username',
component: 'Input',
label: '用户名',
colProps: {
span: 12,
},
rules: [{ required: true }],
},
{
field: 'mobile',
component: 'Input',
@ -35,14 +44,6 @@ export const userSchemas: FormSchema<API.CreateUserParams>[] = [
},
],
},
// {
// field: 'idCard',
// component: 'Input',
// label: '身份证',
// colProps: {
// span: 12,
// },
// },
{
field: 'sex',
@ -59,14 +60,7 @@ export const userSchemas: FormSchema<API.CreateUserParams>[] = [
},
},
},
{
field: 'avatar',
component: 'Input',
colProps: {
span: 12,
},
label: '头像',
},
{
field: 'state',
component: 'RadioGroup',
@ -89,7 +83,6 @@ export const userSchemas: FormSchema<API.CreateUserParams>[] = [
},
// required:true,
rules: [{ required: true, type: 'number' }],
},
{

12
src/views/system/user/index.vue

@ -81,10 +81,6 @@
icon: 'searchoutlined',
color: '#3b82f6',
label: '查看',
// auth: {
// perm: 'sys.menu.update',
// effect: 'disable',
// },
onClick: () => handleShow(record),
},
{
@ -92,10 +88,6 @@
color: '#3b82f6',
size: '15',
label: '修改',
// auth: {
// perm: 'sys.menu.update',
// effect: 'disable',
// },
onClick: () => handleEdit(record),
},
{
@ -129,7 +121,7 @@
},
formProps: {
labelWidth: 100,
schemas: userSchemas,
schemas: userSchemas as any,
autoSubmitOnEnter: true,
disabled: isReadOnly,
},
@ -224,9 +216,7 @@
}, 500);
});
const res = await fetchUserPageList({
model: {
...params,
},
current: params.page,
size: params.limit,
});

6
vite.config.ts

@ -9,6 +9,7 @@ import tsconfigPaths from 'vite-tsconfig-paths';
import { terser } from 'rollup-plugin-terser';
import WindiCSS from 'vite-plugin-windicss';
import path from 'path';
import Markdown from 'vite-plugin-md';
const CWD = process.cwd();
@ -21,7 +22,10 @@ export default defineConfig({
extensions: ['.js', '.ts', '.tsx', '.jsx'],
},
plugins: [
vue(),
vue({
include: [/\.vue$/, /\.md$/], // <--
}),
Markdown(),
WindiCSS(),
vueJsx({
// options are passed on to @vue/babel-plugin-jsx

1491
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save