feat: init
This commit is contained in:
commit
7bbc69aeda
41
.cursorrules
Normal file
41
.cursorrules
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# cursorrules for UMIJS + React + TypeScript + Ant Design
|
||||||
|
|
||||||
|
角色模拟
|
||||||
|
- 您是UMIJS、React、TypeScript、Ant Design及相关Web开发技术的专家
|
||||||
|
- 您已经熟练的读懂了文档中指出的各项技术的文档
|
||||||
|
|
||||||
|
技术选型
|
||||||
|
- UMIJS@v4 [官方文档](https://umijs.org/docs/guides/getting-started)
|
||||||
|
- React@v18 [官方文档](https://react.dev/reference/react)
|
||||||
|
- TypeScript@v5 [官方文档](https://www.typescriptlang.org/docs)
|
||||||
|
- Ant Design@v5 [官方文档](https://ant.design/docs/react/introduce-cn)
|
||||||
|
|
||||||
|
React基本原则
|
||||||
|
- 组件命名: PascalCase,例如 MyComponent.tsx。
|
||||||
|
- 代码格式: 使用 Prettier 和 ESLint,2 空格缩进,JSX 双引号,TypeScript 单引号,JSX 属性驼峰式。
|
||||||
|
- 组件结构: 每个组件单独文件,优先使用函数组件和 Hooks,拆分组件,使用 TypeScript 定义 props 类型。
|
||||||
|
- 状态管理: 使用 valtio,避免直接修改 state。
|
||||||
|
- 事件处理: 事件处理函数驼峰式命名,使用箭头函数。
|
||||||
|
- 代码注释: 复杂逻辑添加注释,使用 JSDoc 规范。
|
||||||
|
- 避免不必要的渲染: 使用 React.memo、useMemo 和 useCallback。
|
||||||
|
- 懒加载: 使用 React.lazy 和 Suspense。
|
||||||
|
- 虚拟化长列表: 使用 react-window 或 react-virtualized。
|
||||||
|
|
||||||
|
TypeScript基本原则
|
||||||
|
- 尽可能使用类型注解: 显式地声明变量、函数参数和返回值的类型,可以提高代码的可读性和可维护性,并帮助 TypeScript 编译器捕获潜在的错误。
|
||||||
|
- 避免使用 any 类型: any 类型会绕过 TypeScript 的类型检查,应该尽量避免使用。如果必须使用 any,可以考虑使用更具体的类型,例如 unknown 或 Record<string, unknown>。
|
||||||
|
- 使用类型别名和接口: 使用类型别名和接口来定义复杂的类型,可以提高代码的可读性和可维护性。
|
||||||
|
- 利用类型推断: TypeScript 编译器可以根据上下文推断出变量的类型,因此不需要在所有地方都显式地声明类型。
|
||||||
|
- 避免过度使用类型断言: 类型断言会覆盖 TypeScript 的类型推断,应该尽量避免使用。如果必须使用类型断言,请确保你完全理解其含义和潜在风险。
|
||||||
|
- 使用模块化: 将代码组织成模块,可以提高代码的可读性、可维护性和可复用性。
|
||||||
|
- 使用命名空间: 对于大型项目,可以使用命名空间来组织代码,避免命名冲突。
|
||||||
|
- 使用严格模式: 启用 TypeScript 的严格模式选项,可以帮助你编写更安全、更健壮的代码。
|
||||||
|
- 使用代码格式化工具: 使用 Prettier 等工具统一代码格式,保持代码风格一致。
|
||||||
|
- 使用代码检查工具: 使用 ESLint 等工具进行代码规范检查,避免常见错误。
|
||||||
|
- 编写单元测试: 编写单元测试可以确保代码的正确性和稳定性。
|
||||||
|
|
||||||
|
Ant Design基本原则
|
||||||
|
|
||||||
|
第三方包
|
||||||
|
|
||||||
|
项目文件及目录
|
||||||
3
.env
Normal file
3
.env
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
GUARD_NAME=ADMIN
|
||||||
|
TOKEN_NAME=ADMIN_TOKEN
|
||||||
|
DEPLOY_BASE_PATH=/
|
||||||
3
.eslintrc.js
Normal file
3
.eslintrc.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: require.resolve('@umijs/max/eslint'),
|
||||||
|
};
|
||||||
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/node_modules
|
||||||
|
/.env.local
|
||||||
|
/.umirc.local.ts
|
||||||
|
/config/config.local.ts
|
||||||
|
/src/.umi
|
||||||
|
/src/.umi-production
|
||||||
|
/src/.umi-test
|
||||||
|
/.umi
|
||||||
|
/.umi-production
|
||||||
|
/.umi-test
|
||||||
|
/dist
|
||||||
|
/.mfsu
|
||||||
|
.swc
|
||||||
1
.husky/commit-msg
Normal file
1
.husky/commit-msg
Normal file
@ -0,0 +1 @@
|
|||||||
|
npx --no-install max verify-commit $1
|
||||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@ -0,0 +1 @@
|
|||||||
|
npx --no-install lint-staged --quiet
|
||||||
17
.lintstagedrc
Normal file
17
.lintstagedrc
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"*.{md,json}": [
|
||||||
|
"prettier --cache --write"
|
||||||
|
],
|
||||||
|
"*.{js,jsx}": [
|
||||||
|
"max lint --fix --eslint-only",
|
||||||
|
"prettier --cache --write"
|
||||||
|
],
|
||||||
|
"*.{css,less}": [
|
||||||
|
"max lint --fix --stylelint-only",
|
||||||
|
"prettier --cache --write"
|
||||||
|
],
|
||||||
|
"*.ts?(x)": [
|
||||||
|
"max lint --fix --eslint-only",
|
||||||
|
"prettier --cache --parser=typescript --write"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
.umi
|
||||||
|
.umi-production
|
||||||
8
.prettierrc
Normal file
8
.prettierrc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 80,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"proseWrap": "never",
|
||||||
|
"overrides": [{ "files": ".prettierrc", "options": { "parser": "json" } }],
|
||||||
|
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-packagejson"]
|
||||||
|
}
|
||||||
3
.stylelintrc.js
Normal file
3
.stylelintrc.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: require.resolve('@umijs/max/stylelint'),
|
||||||
|
};
|
||||||
47
.umirc.ts
Normal file
47
.umirc.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { defineConfig } from '@umijs/max';
|
||||||
|
const { DEPLOY_BASE_PATH = '/' } = process.env;
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
layout: {
|
||||||
|
title: 'AI 智能助手',
|
||||||
|
logo: '/logo.png',
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
'@primary-color': '#1DA57A',
|
||||||
|
'root-entry-name': 'variable',
|
||||||
|
},
|
||||||
|
proxy: {
|
||||||
|
'/api/': {
|
||||||
|
target: 'http://0.0.0.0:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: { '^': '' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
'process.env.GUARD_NAME': process.env.GUARD_NAME,
|
||||||
|
'process.env.TOKEN_NAME': process.env.TOKEN_NAME,
|
||||||
|
'process.env.DEPLOY_BASE_PATH': process.env.DEPLOY_BASE_PATH,
|
||||||
|
},
|
||||||
|
// 通用的
|
||||||
|
hash: true,
|
||||||
|
base: DEPLOY_BASE_PATH,
|
||||||
|
publicPath: DEPLOY_BASE_PATH,
|
||||||
|
ignoreMomentLocale: true,
|
||||||
|
fastRefresh: true,
|
||||||
|
mako: {},
|
||||||
|
esbuildMinifyIIFE: true,
|
||||||
|
conventionRoutes: {
|
||||||
|
exclude: [/\/components\//, /\/modals\//],
|
||||||
|
},
|
||||||
|
deadCode: {},
|
||||||
|
srcTranspiler: 'swc',
|
||||||
|
antd: {},
|
||||||
|
access: {},
|
||||||
|
model: {},
|
||||||
|
initialState: {},
|
||||||
|
request: {
|
||||||
|
dataField: '',
|
||||||
|
},
|
||||||
|
valtio: {},
|
||||||
|
npmClient: 'pnpm',
|
||||||
|
});
|
||||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# 使用官方的 Caddy 镜像作为基础镜像
|
||||||
|
FROM caddy:2.9.1-alpine
|
||||||
|
|
||||||
|
# 将构建后的 dist 目录复制到 Caddy 的默认网站目录
|
||||||
|
COPY dist /usr/share/caddy
|
||||||
|
|
||||||
|
# 复制自定义的 Caddyfile 到 Caddy 的配置目录
|
||||||
|
COPY docker/Caddyfile /etc/caddy/Caddyfile
|
||||||
|
|
||||||
|
# 暴露 Caddy 的默认端口
|
||||||
|
EXPOSE 80
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# README
|
||||||
|
|
||||||
|
`@umijs/max` 模板项目,更多功能参考 [Umi Max 简介](https://umijs.org/docs/max/introduce)
|
||||||
16
build-and-run.sh
Executable file
16
build-and-run.sh
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 设置默认的镜像名称和版本
|
||||||
|
IMAGE_NAME="umi-test"
|
||||||
|
IMAGE_VERSION="main"
|
||||||
|
|
||||||
|
# 如果存在容器,则删除
|
||||||
|
if docker ps -a | grep -q $IMAGE_NAME; then
|
||||||
|
docker rm -f $IMAGE_NAME
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 构建多平台镜像
|
||||||
|
docker buildx build --platform linux/arm64 --tag $IMAGE_NAME:$IMAGE_VERSION --load .
|
||||||
|
|
||||||
|
# 运行容器
|
||||||
|
docker run -d --name $IMAGE_NAME --restart=always -p 3000:80 $IMAGE_NAME:$IMAGE_VERSION
|
||||||
62
docker/Caddyfile
Normal file
62
docker/Caddyfile
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
:80 {
|
||||||
|
root * /usr/share/caddy
|
||||||
|
file_server {
|
||||||
|
precompressed br gzip
|
||||||
|
}
|
||||||
|
|
||||||
|
encode {
|
||||||
|
gzip
|
||||||
|
}
|
||||||
|
|
||||||
|
# 安全头配置保持不变
|
||||||
|
header /* {
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "DENY"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 缓存策略优化
|
||||||
|
header {
|
||||||
|
# 默认动态内容不缓存
|
||||||
|
Cache-Control "no-cache, must-revalidate"
|
||||||
|
|
||||||
|
# 静态资源缓存(改进正则表达式)
|
||||||
|
@static {
|
||||||
|
path_regexp \.(?:css|js|png|jpe?g|gif|ico|svg|woff2?|ttf|eot|web[pm]|avif)$
|
||||||
|
}
|
||||||
|
header @static Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
}
|
||||||
|
|
||||||
|
# SPA路由处理优化(通用方案)
|
||||||
|
@spa {
|
||||||
|
not file
|
||||||
|
}
|
||||||
|
rewrite @spa /index.html
|
||||||
|
|
||||||
|
# 日志配置(保持精简)
|
||||||
|
log {
|
||||||
|
output stdout
|
||||||
|
format json {
|
||||||
|
time_format iso8601
|
||||||
|
}
|
||||||
|
format filter {
|
||||||
|
fields {
|
||||||
|
request>remote_ip delete
|
||||||
|
request>remote_port delete
|
||||||
|
request>proto delete
|
||||||
|
request>method delete
|
||||||
|
request>headers delete
|
||||||
|
resp_headers delete
|
||||||
|
}
|
||||||
|
wrap json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 错误处理
|
||||||
|
handle_errors {
|
||||||
|
@404 {
|
||||||
|
expression {http.error.status_code} == 404
|
||||||
|
}
|
||||||
|
respond @404 "Not Found" 404
|
||||||
|
}
|
||||||
|
}
|
||||||
20
mock/userAPI.ts
Normal file
20
mock/userAPI.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const users = [
|
||||||
|
{ id: 0, name: 'Umi', nickName: 'U', gender: 'MALE' },
|
||||||
|
{ id: 1, name: 'Fish', nickName: 'B', gender: 'FEMALE' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'GET /api/v1/queryUserList': (req: any, res: any) => {
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: { list: users },
|
||||||
|
errorCode: 0,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
'PUT /api/v1/user/': (req: any, res: any) => {
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
errorCode: 0,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
35
package.json
Normal file
35
package.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"author": "zuoge <zuogeus@gmail.com>",
|
||||||
|
"scripts": {
|
||||||
|
"build": "max build",
|
||||||
|
"dev": "max dev",
|
||||||
|
"format": "prettier --cache --write .",
|
||||||
|
"postinstall": "max setup",
|
||||||
|
"prepare": "husky",
|
||||||
|
"setup": "max setup",
|
||||||
|
"start": "npm run dev"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.0.1",
|
||||||
|
"@ant-design/pro-components": "^2.4.4",
|
||||||
|
"@umijs/max": "^4.4.4",
|
||||||
|
"ahooks": "^3.8.4",
|
||||||
|
"antd": "^5.4.0",
|
||||||
|
"radash": "^12.1.0",
|
||||||
|
"valtio": "^2.1.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@swc/core": "^1.3.67",
|
||||||
|
"@types/react": "^18.0.33",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"husky": "^9",
|
||||||
|
"lint-staged": "^13.2.0",
|
||||||
|
"prettier": "^2.8.7",
|
||||||
|
"prettier-plugin-organize-imports": "^3.2.2",
|
||||||
|
"prettier-plugin-packagejson": "^2.4.3",
|
||||||
|
"swc-plugin-auto-css-modules": "^1.5.0",
|
||||||
|
"typescript": "^5.0.3",
|
||||||
|
"vitest": "^3.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
14619
pnpm-lock.yaml
generated
Normal file
14619
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
src/access.ts
Normal file
10
src/access.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export default (initialState: API.UserInfo) => {
|
||||||
|
// 在这里按照初始化数据定义项目中的权限,统一管理
|
||||||
|
// 参考文档 https://umijs.org/docs/max/access
|
||||||
|
const canSeeAdmin = !!(
|
||||||
|
initialState && initialState.name !== 'dontHaveAccess'
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
canSeeAdmin,
|
||||||
|
};
|
||||||
|
};
|
||||||
14
src/app.tsx
Normal file
14
src/app.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// 运行时配置
|
||||||
|
|
||||||
|
import { LayoutConfig, requestConfig, state, stateActions } from './common';
|
||||||
|
|
||||||
|
export async function getInitialState(): Promise<{ name: string }> {
|
||||||
|
await stateActions.me();
|
||||||
|
return {
|
||||||
|
name: state.session.user?.username ?? '未登录',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const layout = LayoutConfig;
|
||||||
|
|
||||||
|
export const request = requestConfig;
|
||||||
0
src/assets/.gitkeep
Normal file
0
src/assets/.gitkeep
Normal file
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
63
src/common/components/DataEntry/MyColorPicker.tsx
Normal file
63
src/common/components/DataEntry/MyColorPicker.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { ColorPicker, ColorPickerProps } from 'antd';
|
||||||
|
|
||||||
|
type Props = ColorPickerProps & {
|
||||||
|
value?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MyColorPicker({ onChange, ...rest }: Props) {
|
||||||
|
return (
|
||||||
|
<ColorPicker
|
||||||
|
allowClear
|
||||||
|
arrow
|
||||||
|
showText
|
||||||
|
format="hex"
|
||||||
|
defaultValue={null}
|
||||||
|
presets={[
|
||||||
|
{
|
||||||
|
label: 'Recommended',
|
||||||
|
colors: [
|
||||||
|
'#000000',
|
||||||
|
'#000000E0',
|
||||||
|
'#000000A6',
|
||||||
|
'#00000073',
|
||||||
|
'#00000040',
|
||||||
|
'#00000026',
|
||||||
|
'#0000001A',
|
||||||
|
'#00000012',
|
||||||
|
'#0000000A',
|
||||||
|
'#00000005',
|
||||||
|
'#F5222D',
|
||||||
|
'#FA8C16',
|
||||||
|
'#FADB14',
|
||||||
|
'#8BBB11',
|
||||||
|
'#52C41A',
|
||||||
|
'#13A8A8',
|
||||||
|
'#1677FF',
|
||||||
|
'#2F54EB',
|
||||||
|
'#722ED1',
|
||||||
|
'#EB2F96',
|
||||||
|
'#F5222D4D',
|
||||||
|
'#FA8C164D',
|
||||||
|
'#FADB144D',
|
||||||
|
'#8BBB114D',
|
||||||
|
'#52C41A4D',
|
||||||
|
'#13A8A84D',
|
||||||
|
'#1677FF4D',
|
||||||
|
'#2F54EB4D',
|
||||||
|
'#722ED14D',
|
||||||
|
'#EB2F964D',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Recent',
|
||||||
|
colors: [],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
{...rest}
|
||||||
|
onChange={(color) => {
|
||||||
|
onChange?.(color.toHexString());
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
src/common/components/MyIcons.tsx
Normal file
41
src/common/components/MyIcons.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
AuditOutlined,
|
||||||
|
BankOutlined,
|
||||||
|
BarChartOutlined,
|
||||||
|
BarcodeOutlined,
|
||||||
|
ClusterOutlined,
|
||||||
|
ConsoleSqlOutlined,
|
||||||
|
ControlOutlined,
|
||||||
|
CreditCardOutlined,
|
||||||
|
DashboardOutlined,
|
||||||
|
DatabaseOutlined,
|
||||||
|
FileWordOutlined,
|
||||||
|
FormOutlined,
|
||||||
|
HomeOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
ShopOutlined,
|
||||||
|
ShoppingCartOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
|
export type MyIconsType = keyof typeof MyIcons;
|
||||||
|
|
||||||
|
export const MyIcons = {
|
||||||
|
AuditOutlined: <AuditOutlined />,
|
||||||
|
BankOutlined: <BankOutlined />,
|
||||||
|
BarcodeOutlined: <BarcodeOutlined />,
|
||||||
|
BarChartOutlined: <BarChartOutlined />,
|
||||||
|
ClusterOutlined: <ClusterOutlined />,
|
||||||
|
ConsoleSqlOutlined: <ConsoleSqlOutlined />,
|
||||||
|
ControlOutlined: <ControlOutlined />,
|
||||||
|
CreditCardOutlined: <CreditCardOutlined />,
|
||||||
|
DashboardOutlined: <DashboardOutlined />,
|
||||||
|
DatabaseOutlined: <DatabaseOutlined />,
|
||||||
|
FileWordOutlined: <FileWordOutlined />,
|
||||||
|
FormOutlined: <FormOutlined />,
|
||||||
|
HomeOutlined: <HomeOutlined />,
|
||||||
|
SettingOutlined: <SettingOutlined />,
|
||||||
|
ShopOutlined: <ShopOutlined />,
|
||||||
|
ShoppingCartOutlined: <ShoppingCartOutlined />,
|
||||||
|
UserOutlined: <UserOutlined />,
|
||||||
|
};
|
||||||
107
src/common/components/biz/MyLoginPage/index.tsx
Normal file
107
src/common/components/biz/MyLoginPage/index.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { Apis } from '@/gen/Apis';
|
||||||
|
import {
|
||||||
|
FieldTimeOutlined,
|
||||||
|
LockOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
LoginFormPage,
|
||||||
|
ProConfigProvider,
|
||||||
|
ProFormText,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { useMemoizedFn, useRequest } from 'ahooks';
|
||||||
|
import { stateActions } from '../../../libs/valtio/actions';
|
||||||
|
|
||||||
|
export default function MyLoginPage() {
|
||||||
|
const pageReq = useRequest(Apis.Auth.Captcha);
|
||||||
|
const loginReq = useRequest(stateActions.login, { manual: true });
|
||||||
|
|
||||||
|
const handleLogin = useMemoizedFn((values: ApiReqTypes.Auth.Login) => {
|
||||||
|
loginReq.run({
|
||||||
|
...values,
|
||||||
|
captcha_key: pageReq.data?.data?.key ?? '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProConfigProvider>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#f8f8f8',
|
||||||
|
height: '100vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LoginFormPage<ApiReqTypes.Auth.Login>
|
||||||
|
loading={loginReq.loading}
|
||||||
|
title="欢迎使用后台管理系统"
|
||||||
|
backgroundVideoUrl="https://gw.alipayobjects.com/v/huamei_gcee1x/afts/video/jXRBRK_VAwoAAAAAAAAAAAAAK4eUAQBr"
|
||||||
|
subTitle="Admin management system"
|
||||||
|
onFinish={handleLogin}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<ProFormText
|
||||||
|
name="username"
|
||||||
|
fieldProps={{
|
||||||
|
size: 'large',
|
||||||
|
prefix: <UserOutlined className={'prefixIcon'} />,
|
||||||
|
}}
|
||||||
|
placeholder={'登录账号'}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入登录账号!',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<ProFormText.Password
|
||||||
|
name="password"
|
||||||
|
fieldProps={{
|
||||||
|
size: 'large',
|
||||||
|
prefix: <LockOutlined className={'prefixIcon'} />,
|
||||||
|
}}
|
||||||
|
placeholder={'登录密码'}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入密码!',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex' }}>
|
||||||
|
<ProFormText
|
||||||
|
name="captcha"
|
||||||
|
fieldProps={{
|
||||||
|
size: 'large',
|
||||||
|
prefix: <FieldTimeOutlined className={'prefixIcon'} />,
|
||||||
|
}}
|
||||||
|
placeholder={'验证码'}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入验证码!',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '40px',
|
||||||
|
border: '1px solid #eee',
|
||||||
|
padding: '0 2px',
|
||||||
|
margin: '0 10px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: '3px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={pageReq.run}
|
||||||
|
>
|
||||||
|
<img height={32} width={100} src={pageReq.data?.data?.img} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</LoginFormPage>
|
||||||
|
</div>
|
||||||
|
</ProConfigProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
0
src/common/components/biz/MyTable/index.tsx
Normal file
0
src/common/components/biz/MyTable/index.tsx
Normal file
15
src/common/components/buttons/AddButton.tsx
Normal file
15
src/common/components/buttons/AddButton.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import type { ButtonProps } from 'antd';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
|
||||||
|
interface AddButtonProps extends ButtonProps {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AddButton({ title, ...props }: AddButtonProps) {
|
||||||
|
return (
|
||||||
|
<Button type="primary" icon={<PlusOutlined />} {...props}>
|
||||||
|
{`添加${title}`}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
src/common/components/buttons/DeleteButton.tsx
Normal file
32
src/common/components/buttons/DeleteButton.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { DeleteOutlined, WarningOutlined } from '@ant-design/icons';
|
||||||
|
import type { ButtonProps } from 'antd';
|
||||||
|
import { Button, Popconfirm } from 'antd';
|
||||||
|
|
||||||
|
interface DeleteButtonProps extends ButtonProps {
|
||||||
|
title?: string;
|
||||||
|
onConfirm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DeleteButton({
|
||||||
|
title,
|
||||||
|
onConfirm,
|
||||||
|
...props
|
||||||
|
}: DeleteButtonProps) {
|
||||||
|
return (
|
||||||
|
<Popconfirm
|
||||||
|
title={title || '确定要删除吗?'}
|
||||||
|
icon={<WarningOutlined style={{ color: 'red' }} />}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
type="link"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{title || '删除'}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
src/common/components/buttons/EditButton.tsx
Normal file
15
src/common/components/buttons/EditButton.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { EditOutlined } from '@ant-design/icons';
|
||||||
|
import type { ButtonProps } from 'antd';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
|
||||||
|
interface EditButtonProps extends ButtonProps {
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EditButton({ title, ...props }: EditButtonProps) {
|
||||||
|
return (
|
||||||
|
<Button size="small" type="link" icon={<EditOutlined />} {...props}>
|
||||||
|
{title || '编辑'}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/common/components/layout/LoadingSpin.tsx
Normal file
5
src/common/components/layout/LoadingSpin.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { Spin } from 'antd';
|
||||||
|
|
||||||
|
export default function LoadingSpin() {
|
||||||
|
return <Spin />;
|
||||||
|
}
|
||||||
2
src/common/constants/index.ts
Normal file
2
src/common/constants/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const LOGIN_PATH = process.env.DEPLOY_BASE_PATH + 'login';
|
||||||
|
export const HOME_PATH = process.env.DEPLOY_BASE_PATH || '/';
|
||||||
1
src/common/constants/paths.ts
Normal file
1
src/common/constants/paths.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
22
src/common/index.ts
Normal file
22
src/common/index.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// components
|
||||||
|
export { default as MyLoginPage } from './components/biz/MyLoginPage';
|
||||||
|
export { default as AddButton } from './components/buttons/AddButton';
|
||||||
|
export { default as DeleteButton } from './components/buttons/DeleteButton';
|
||||||
|
export { default as EditButton } from './components/buttons/EditButton';
|
||||||
|
export * from './components/MyIcons';
|
||||||
|
export { default as MyColorPicker } from './components/DataEntry/MyColorPicker';
|
||||||
|
|
||||||
|
// constants
|
||||||
|
export * from './constants';
|
||||||
|
|
||||||
|
// utils
|
||||||
|
export * from './libs/umi/request/downloadFile';
|
||||||
|
export * from './utils/common';
|
||||||
|
|
||||||
|
// libs
|
||||||
|
export * from './libs/valtio/actions';
|
||||||
|
export * from './libs/valtio/state';
|
||||||
|
|
||||||
|
export * from './libs/umi/layout';
|
||||||
|
export * from './libs/umi/request';
|
||||||
|
|
||||||
29
src/common/libs/umi/layout/AvatarDropdown.tsx
Normal file
29
src/common/libs/umi/layout/AvatarDropdown.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { history } from '@umijs/max';
|
||||||
|
import { Dropdown, MenuProps } from 'antd';
|
||||||
|
import { stateActions } from '../../valtio/actions';
|
||||||
|
|
||||||
|
export default function AvatarDropdown({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const items: MenuProps['items'] = [
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
label: '修改密码',
|
||||||
|
onClick: () => {
|
||||||
|
history.push('/user/password');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
danger: true,
|
||||||
|
label: '退出登录',
|
||||||
|
onClick: () => {
|
||||||
|
stateActions.logout();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return <Dropdown menu={{ items }}>{children}</Dropdown>;
|
||||||
|
}
|
||||||
37
src/common/libs/umi/layout/index.tsx
Normal file
37
src/common/libs/umi/layout/index.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { UserOutlined } from '@ant-design/icons';
|
||||||
|
import { history, RuntimeConfig } from '@umijs/max';
|
||||||
|
import { Avatar } from 'antd';
|
||||||
|
import { useMyState } from '../../valtio/state';
|
||||||
|
import AvatarDropdown from './AvatarDropdown';
|
||||||
|
import { loopMenu } from './loopMenu';
|
||||||
|
|
||||||
|
export const LayoutConfig: RuntimeConfig['layout'] = () => {
|
||||||
|
const { snap } = useMyState();
|
||||||
|
|
||||||
|
return {
|
||||||
|
layout: 'mix',
|
||||||
|
colorPrimary: '#1890ff',
|
||||||
|
siderWidth: 220,
|
||||||
|
pure: history.location.pathname === process.env.DEPLOY_BASE_PATH + 'login',
|
||||||
|
actionsRender: () => [],
|
||||||
|
avatarProps: {
|
||||||
|
src: (
|
||||||
|
<Avatar
|
||||||
|
style={{ backgroundColor: '#87d068' }}
|
||||||
|
icon={<UserOutlined />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
title: snap.session.user?.username,
|
||||||
|
render: (_, avatarChildren) => {
|
||||||
|
return <AvatarDropdown>{avatarChildren}</AvatarDropdown>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
waterMarkProps: {
|
||||||
|
content: snap.session.user?.username,
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
params: snap.session.permissions,
|
||||||
|
request: async () => loopMenu(snap.session.permissions),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
42
src/common/libs/umi/layout/loopMenu.ts
Normal file
42
src/common/libs/umi/layout/loopMenu.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { MyIcons, MyIconsType } from "@/common/components/MyIcons";
|
||||||
|
import { sort } from "radash";
|
||||||
|
|
||||||
|
type PermissionsType = any[]//ApiRespTypes.Auth.Login['permissions']
|
||||||
|
|
||||||
|
// TODO 可以换成umi的route类型
|
||||||
|
type RouteType = {
|
||||||
|
path: string | undefined;
|
||||||
|
name: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
hideInMenu: boolean;
|
||||||
|
children: RouteType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loopMenu = (permissions: PermissionsType) => {
|
||||||
|
// 排序一下
|
||||||
|
sort(permissions, (f: PermissionsType[0]) => f._lft)
|
||||||
|
|
||||||
|
let tree: RouteType[] = [];
|
||||||
|
let map: Record<number, RouteType> = {};
|
||||||
|
|
||||||
|
permissions?.forEach((permission) => {
|
||||||
|
map[permission.id] = {
|
||||||
|
path: permission.type === 'Button' ? '' : permission.path,
|
||||||
|
name: permission.name,
|
||||||
|
icon: permission.icon && MyIcons[permission.icon as MyIconsType],
|
||||||
|
hideInMenu: permission.type === 'Button',
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
permissions?.forEach((permission) => {
|
||||||
|
let node = map[permission.id];
|
||||||
|
if (permission.parent_id !== null) {
|
||||||
|
map[permission.parent_id].children.push(node);
|
||||||
|
} else {
|
||||||
|
tree.push(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return tree?.[0]?.children;
|
||||||
|
};
|
||||||
24
src/common/libs/umi/request/downloadFile.ts
Normal file
24
src/common/libs/umi/request/downloadFile.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export const downloadFile = (disposition: string, data: any) => {
|
||||||
|
const blob = new Blob([data]);
|
||||||
|
const start = "filename*=utf-8''";
|
||||||
|
let fileName = '';
|
||||||
|
let dis = disposition.replace('UTF-8', 'utf-8');
|
||||||
|
if (dis.includes(start)) {
|
||||||
|
fileName = dis.substr(dis.indexOf(start) + start.length);
|
||||||
|
fileName = decodeURI(fileName);
|
||||||
|
}
|
||||||
|
if ('download' in document.createElement('a')) {
|
||||||
|
// 非IE下载
|
||||||
|
const elink: any = document.createElement('a');
|
||||||
|
elink.download = fileName;
|
||||||
|
elink.style.display = 'none';
|
||||||
|
elink.href = URL.createObjectURL(blob);
|
||||||
|
document.body.appendChild(elink);
|
||||||
|
elink.click();
|
||||||
|
URL.revokeObjectURL(elink.href); // 释放URL 对象
|
||||||
|
document.body.removeChild(elink);
|
||||||
|
} else {
|
||||||
|
// IE10+下载
|
||||||
|
// navigator.msSaveBlob(blob, fileName);
|
||||||
|
}
|
||||||
|
};
|
||||||
116
src/common/libs/umi/request/index.ts
Normal file
116
src/common/libs/umi/request/index.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { LOGIN_PATH } from '@/common/constants';
|
||||||
|
import type { RequestConfig } from '@umijs/max';
|
||||||
|
import { history } from '@umijs/max';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { state } from '../../valtio/state';
|
||||||
|
import { downloadFile } from './downloadFile';
|
||||||
|
|
||||||
|
export const requestConfig: RequestConfig = {
|
||||||
|
baseURL: '/api/',
|
||||||
|
timeout: 1000 * 60,
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||||
|
errorConfig: {
|
||||||
|
errorThrower: (res) => {
|
||||||
|
// console.log('errorThrower', res);
|
||||||
|
// 只要 success === false,就会进这里
|
||||||
|
const { success, data, errorCode, errorMessage, showType } = res;
|
||||||
|
if (!success) {
|
||||||
|
const error: any = new Error(errorMessage);
|
||||||
|
error.name = 'BizError';
|
||||||
|
error.info = { errorCode, errorMessage, showType, data };
|
||||||
|
throw error; // 抛出自制的错误
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 错误接收及处理
|
||||||
|
errorHandler: (error: any, opts: any) => {
|
||||||
|
// console.log('errorHandler', error);
|
||||||
|
// 这里会处理所有的错误
|
||||||
|
|
||||||
|
// 如果 opts.skipErrorHandler 为 true,则抛出错误
|
||||||
|
if (opts?.skipErrorHandler) throw error;
|
||||||
|
|
||||||
|
// 我们的 errorThrower 抛出的错误。
|
||||||
|
if (error.name === 'BizError') {
|
||||||
|
const errorInfo: ResponseStructure | undefined = error.info;
|
||||||
|
if (errorInfo) {
|
||||||
|
const { errorMessage, errorCode } = errorInfo;
|
||||||
|
|
||||||
|
// 如果错误码为10000,并且当前路径不是登录页,则跳转到登录页
|
||||||
|
if (errorCode === 10000) {
|
||||||
|
if (history.location.pathname !== LOGIN_PATH) {
|
||||||
|
history.push(LOGIN_PATH);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 其他情况默认显示错误信息
|
||||||
|
message.error(errorMessage);
|
||||||
|
}
|
||||||
|
// switch (errorInfo.showType) {
|
||||||
|
// case ErrorShowType.SILENT:
|
||||||
|
// // do nothing
|
||||||
|
// break;
|
||||||
|
// case ErrorShowType.WARN_MESSAGE:
|
||||||
|
// message.warning(errorMessage);
|
||||||
|
// break;
|
||||||
|
// case ErrorShowType.ERROR_MESSAGE:
|
||||||
|
// message.error(errorMessage);
|
||||||
|
// break;
|
||||||
|
// case ErrorShowType.NOTIFICATION:
|
||||||
|
// notification.open({
|
||||||
|
// description: errorMessage,
|
||||||
|
// message: errorCode,
|
||||||
|
// });
|
||||||
|
// break;
|
||||||
|
// case ErrorShowType.REDIRECT:
|
||||||
|
// // TODO: redirect
|
||||||
|
// break;
|
||||||
|
// default:
|
||||||
|
// message.error(errorMessage);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
} else if (error.response) {
|
||||||
|
// Axios 的错误
|
||||||
|
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
|
||||||
|
message.error(`Response status:${error.response.status}`);
|
||||||
|
} else if (error.request) {
|
||||||
|
// 请求已经成功发起,但没有收到响应
|
||||||
|
// \`error.request\` 在浏览器中是 XMLHttpRequest 的实例,
|
||||||
|
// 而在node.js中是 http.ClientRequest 的实例
|
||||||
|
message.error('None response! Please retry.');
|
||||||
|
} else {
|
||||||
|
// 发送请求时出了点问题
|
||||||
|
message.error('Request error, please retry.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
requestInterceptors: [
|
||||||
|
(url: string, options: any) => {
|
||||||
|
return {
|
||||||
|
url: url,
|
||||||
|
options: {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
authorization: 'Bearer ' + state.storage.access_token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
responseInterceptors: [
|
||||||
|
(response: any) => {
|
||||||
|
const { headers } = response;
|
||||||
|
|
||||||
|
// 拦截并处理下载文件
|
||||||
|
if (headers['content-disposition']) {
|
||||||
|
downloadFile(headers['content-disposition'], response.data);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
31
src/common/libs/valtio/actions.ts
Normal file
31
src/common/libs/valtio/actions.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { HOME_PATH, LOGIN_PATH } from '@/common/constants';
|
||||||
|
import { Apis } from '@/gen/Apis';
|
||||||
|
import { history } from '@umijs/max';
|
||||||
|
import { state } from './state';
|
||||||
|
|
||||||
|
export const stateActions = {
|
||||||
|
me: async () => {
|
||||||
|
Apis.Auth.Me().then(res => {
|
||||||
|
state.session.user = res.data?.user;
|
||||||
|
state.session.permissions = res.data?.permissions;
|
||||||
|
if (history.location.pathname === LOGIN_PATH) {
|
||||||
|
history.push(HOME_PATH);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
login: async (params: ApiReqTypes.Auth.Login) => {
|
||||||
|
// await sleep(1000);
|
||||||
|
Apis.Auth.Login(params).then(res => {
|
||||||
|
state.session.user = res.data?.user;
|
||||||
|
state.session.permissions = res.data?.permissions;
|
||||||
|
state.storage.access_token = res.data?.access_token;
|
||||||
|
history.push(HOME_PATH);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
logout() {
|
||||||
|
state.session.user = undefined;
|
||||||
|
state.session.permissions = undefined;
|
||||||
|
state.storage.access_token = undefined;
|
||||||
|
history.push(LOGIN_PATH);
|
||||||
|
},
|
||||||
|
};
|
||||||
32
src/common/libs/valtio/state.ts
Normal file
32
src/common/libs/valtio/state.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { proxy, useSnapshot } from 'valtio';
|
||||||
|
import { proxyWithPersistant } from './utils';
|
||||||
|
|
||||||
|
export const state: {
|
||||||
|
storage: {
|
||||||
|
access_token: string | undefined;
|
||||||
|
};
|
||||||
|
session: {
|
||||||
|
user?: any;
|
||||||
|
permissions?: any;
|
||||||
|
apiKeys: string[];
|
||||||
|
};
|
||||||
|
} = proxy({
|
||||||
|
storage: proxyWithPersistant(
|
||||||
|
{
|
||||||
|
access_token: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: process.env.TOKEN_NAME as string,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
session: proxy({
|
||||||
|
user: undefined,
|
||||||
|
permissions: undefined,
|
||||||
|
apiKeys: [],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useMyState() {
|
||||||
|
const snap = useSnapshot(state);
|
||||||
|
return { snap };
|
||||||
|
}
|
||||||
15
src/common/libs/valtio/utils.ts
Normal file
15
src/common/libs/valtio/utils.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { proxy, snapshot, subscribe } from 'valtio';
|
||||||
|
|
||||||
|
export function proxyWithPersistant<V>(
|
||||||
|
val: V,
|
||||||
|
opts: {
|
||||||
|
key: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const local = localStorage.getItem(opts.key);
|
||||||
|
const state = proxy(local ? JSON.parse(local) : val);
|
||||||
|
subscribe(state, () => {
|
||||||
|
localStorage.setItem(opts.key, JSON.stringify(snapshot(state)));
|
||||||
|
});
|
||||||
|
return state;
|
||||||
|
}
|
||||||
26
src/common/types.d.ts
vendored
Normal file
26
src/common/types.d.ts
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// 错误处理方案: 错误类型
|
||||||
|
enum ErrorShowType {
|
||||||
|
SILENT = 0,
|
||||||
|
WARN_MESSAGE = 1,
|
||||||
|
ERROR_MESSAGE = 2,
|
||||||
|
NOTIFICATION = 3,
|
||||||
|
REDIRECT = 9,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 与后端约定的响应数据格式
|
||||||
|
type ResponseStructure<T = undefined> = {
|
||||||
|
success: boolean;
|
||||||
|
data?: T;
|
||||||
|
errorCode?: number;
|
||||||
|
errorMessage?: string;
|
||||||
|
errorDetail?: any;
|
||||||
|
showType?: ErrorShowType;
|
||||||
|
meta?: ResponseMetaStructure;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResponseMetaStructure = {
|
||||||
|
current_page: number;
|
||||||
|
last_page: number;
|
||||||
|
per_page: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
13
src/common/utils/common.ts
Normal file
13
src/common/utils/common.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wrapTableResp(data: ResponseStructure) {
|
||||||
|
return {
|
||||||
|
data: data.data,
|
||||||
|
success: data.success,
|
||||||
|
total: data.meta?.total || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
1
src/common/utils/navigation.ts
Normal file
1
src/common/utils/navigation.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
34
src/components/PermissionTreeSelect.tsx
Normal file
34
src/components/PermissionTreeSelect.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { useSysPermissionsDataSource } from '@/hooks/dataSources';
|
||||||
|
import { ProFormColumnsType } from '@ant-design/pro-components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取权限树形选择器的配置
|
||||||
|
* @param guardName 权限守卫名称
|
||||||
|
* @returns 权限树形选择器配置
|
||||||
|
*/
|
||||||
|
export const PermissionTreeSelect = (
|
||||||
|
guardName: string,
|
||||||
|
): ProFormColumnsType<any, 'treeSelect'> => {
|
||||||
|
const { data } = useSysPermissionsDataSource({ guard_name: guardName });
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: 'parent_id',
|
||||||
|
title: '上级菜单',
|
||||||
|
valueType: 'treeSelect',
|
||||||
|
request: async () => data || [],
|
||||||
|
fieldProps: {
|
||||||
|
rowKey: 'id',
|
||||||
|
allowClear: true,
|
||||||
|
autoClearSearchValue: false,
|
||||||
|
bordered: true,
|
||||||
|
fieldNames: {
|
||||||
|
label: 'name',
|
||||||
|
value: 'id',
|
||||||
|
},
|
||||||
|
filterTreeNode: false,
|
||||||
|
showSearch: false,
|
||||||
|
treeNodeFilterProp: 'title',
|
||||||
|
treeDefaultExpandAll: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
127
src/gen/ApiReqTypes.d.ts
vendored
Normal file
127
src/gen/ApiReqTypes.d.ts
vendored
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
declare namespace ApiReqTypes {
|
||||||
|
namespace Select {}
|
||||||
|
namespace SysRoles {
|
||||||
|
type List = {
|
||||||
|
guard_name?: string; // 守护者,[enum:GuardNameEnum]
|
||||||
|
};
|
||||||
|
type Store = {
|
||||||
|
name: string; // 名称
|
||||||
|
guard_name?: string; // 守护者,[enum:GuardNameEnum]
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
type Update = {
|
||||||
|
id: number; // ID
|
||||||
|
name: string; // 名称
|
||||||
|
guard_name?: string; // 守护者,[enum:GuardNameEnum]
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
type Delete = {
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
type GetPermissions = {
|
||||||
|
id: number; // ID
|
||||||
|
guard_name?: string; // 守护者,[enum:GuardNameEnum]
|
||||||
|
};
|
||||||
|
type SetPermissions = {
|
||||||
|
id: number; // ID
|
||||||
|
guard_name?: string; // 守护者,[enum:GuardNameEnum]
|
||||||
|
permissions_ids: any[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
namespace Bosses {
|
||||||
|
type List = {
|
||||||
|
name1?: string; // 模糊搜索:名称1
|
||||||
|
};
|
||||||
|
type Store = {
|
||||||
|
name: string; // 简称
|
||||||
|
full_name?: string; // 全称
|
||||||
|
address?: string; // 地址
|
||||||
|
contact?: string; // 联系人
|
||||||
|
phone?: string; // 联系电话
|
||||||
|
};
|
||||||
|
type Update = {
|
||||||
|
id: number; // id
|
||||||
|
name: string; // 简称
|
||||||
|
full_name?: string; // 全称
|
||||||
|
address?: string; // 地址
|
||||||
|
contact?: string; // 联系人
|
||||||
|
phone?: string; // 联系电话
|
||||||
|
};
|
||||||
|
type Delete = {
|
||||||
|
id: number; // id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
namespace Auth {
|
||||||
|
type Login = {
|
||||||
|
username: string; // 用户名
|
||||||
|
password: string; // 密码
|
||||||
|
captcha: string; // 验证码
|
||||||
|
captcha_key: string; // 验证码key
|
||||||
|
};
|
||||||
|
type ChangePassword = {
|
||||||
|
old_password: string; // 老密码
|
||||||
|
new_password: string; // 新密码
|
||||||
|
re_new_password: string; // 重复新密码
|
||||||
|
};
|
||||||
|
type PreUpload = {
|
||||||
|
filename: string; // 文件名称
|
||||||
|
};
|
||||||
|
}
|
||||||
|
namespace SysPermissions {
|
||||||
|
type List = {
|
||||||
|
parent_id?: number; // 上级ID
|
||||||
|
guard_name: string; // 守护者,[enum:GuardNameEnum]
|
||||||
|
};
|
||||||
|
type Store = {
|
||||||
|
name: string;
|
||||||
|
guard_name: string; // 守护者,[enum:GuardNameEnum]
|
||||||
|
key?: string; // 前端权限识别符
|
||||||
|
icon?: string; // 图标
|
||||||
|
type?: string; // 类型,[enum:SysPermissionsTypeEnum]
|
||||||
|
backend_apis?: any[]; // 后台api
|
||||||
|
path?: string; // 路由
|
||||||
|
parent_id?: number;
|
||||||
|
};
|
||||||
|
type Update = {
|
||||||
|
id: number; // ID
|
||||||
|
name: string;
|
||||||
|
key?: string;
|
||||||
|
guard_name: string;
|
||||||
|
icon?: string; // 图标
|
||||||
|
type: string; // 类型:SysPermissionsTypeEnum
|
||||||
|
backend_apis?: any[]; // 后台api
|
||||||
|
path?: string; // 路由
|
||||||
|
parent_id?: number;
|
||||||
|
};
|
||||||
|
type Delete = {
|
||||||
|
id: number; // ID
|
||||||
|
};
|
||||||
|
type Move = {
|
||||||
|
id: number; // ID
|
||||||
|
type: string; // 类型:up 升级,down 降级
|
||||||
|
};
|
||||||
|
}
|
||||||
|
namespace Admins {
|
||||||
|
type List = {
|
||||||
|
username?: string; // 模糊搜索:名称
|
||||||
|
};
|
||||||
|
type Store = {
|
||||||
|
username: string; // 用户名
|
||||||
|
password: string; // 密码hidden
|
||||||
|
last_login_at?: Date; // 最后登录时间
|
||||||
|
last_login_ip?: string; // 最后登录IP
|
||||||
|
is_locked: boolean; // 是否锁定
|
||||||
|
};
|
||||||
|
type Update = {
|
||||||
|
id: number; // id
|
||||||
|
username: string; // 用户名
|
||||||
|
password: string; // 密码hidden
|
||||||
|
last_login_at?: Date; // 最后登录时间
|
||||||
|
last_login_ip?: string; // 最后登录IP
|
||||||
|
is_locked: boolean; // 是否锁定
|
||||||
|
};
|
||||||
|
type Delete = {
|
||||||
|
id: number; // id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/gen/ApiRespTypes.d.ts
vendored
Normal file
59
src/gen/ApiRespTypes.d.ts
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
declare namespace ApiRespTypes {
|
||||||
|
namespace Select {
|
||||||
|
}
|
||||||
|
namespace SysRoles {
|
||||||
|
type List = {
|
||||||
|
id: number; // ID
|
||||||
|
name: string; // 名称
|
||||||
|
guard_name: string; // 守卫名称
|
||||||
|
created_at: string; // 创建时间
|
||||||
|
updated_at: string; // 更新时间
|
||||||
|
color: string; // 颜色
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
namespace Bosses {
|
||||||
|
}
|
||||||
|
namespace Auth {
|
||||||
|
}
|
||||||
|
namespace SysPermissions {
|
||||||
|
type List = {
|
||||||
|
id: number; // ID
|
||||||
|
name: string; // 名称
|
||||||
|
guard_name: string; // 守卫名称
|
||||||
|
created_at: string; // 创建时间
|
||||||
|
updated_at: string; // 更新时间
|
||||||
|
key: string; // 键
|
||||||
|
icon: string; // 图标
|
||||||
|
type: string; // 类型
|
||||||
|
backend_apis: string[]; // 后端API
|
||||||
|
path: string; // 路径
|
||||||
|
_lft: number; // 左值
|
||||||
|
_rgt: number; // 右值
|
||||||
|
parent_id: number; // 父级ID
|
||||||
|
children: {
|
||||||
|
id: number; // ID
|
||||||
|
name: string; // 名称
|
||||||
|
guard_name: string; // 守卫名称
|
||||||
|
created_at: string; // 创建时间
|
||||||
|
updated_at: string; // 更新时间
|
||||||
|
key: string; // 键
|
||||||
|
icon: string; // 图标
|
||||||
|
type: string; // 类型
|
||||||
|
backend_apis: string[]; // 后端API
|
||||||
|
path: string; // 路径
|
||||||
|
_lft: number; // 左值
|
||||||
|
_rgt: number; // 右值
|
||||||
|
parent_id: number; // 父级ID
|
||||||
|
children: undefined[]; // 子节点
|
||||||
|
}[]; // 子节点
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
namespace Admins {
|
||||||
|
type List = {
|
||||||
|
id: number; // ID
|
||||||
|
username: string; // 用户名
|
||||||
|
created_at: string; // 创建时间
|
||||||
|
updated_at: string; // 更新时间
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
}
|
||||||
94
src/gen/Apis.ts
Normal file
94
src/gen/Apis.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { request } from "@umijs/max";
|
||||||
|
|
||||||
|
export const Apis = {
|
||||||
|
Select: {
|
||||||
|
SysRoles() {
|
||||||
|
return request('/admin/select/sys_roles', {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SysRoles: {
|
||||||
|
List<TReq = ApiReqTypes.SysRoles.List, TResp = ApiRespTypes.SysRoles.List>(data?: TReq) {
|
||||||
|
return request<ResponseStructure<TResp>>('/admin/sys_roles/list', { data });
|
||||||
|
},
|
||||||
|
Store<TReq = ApiReqTypes.SysRoles.Store>(data: TReq) {
|
||||||
|
return request('/admin/sys_roles/store', { data });
|
||||||
|
},
|
||||||
|
Update<TReq = ApiReqTypes.SysRoles.Update>(data: TReq) {
|
||||||
|
return request('/admin/sys_roles/update', { data });
|
||||||
|
},
|
||||||
|
Delete<TReq = ApiReqTypes.SysRoles.Delete>(data: TReq) {
|
||||||
|
return request('/admin/sys_roles/delete', { data });
|
||||||
|
},
|
||||||
|
GetPermissions<TReq = ApiReqTypes.SysRoles.GetPermissions>(data: TReq) {
|
||||||
|
return request('/admin/sys_roles/get_permissions', { data });
|
||||||
|
},
|
||||||
|
SetPermissions<TReq = ApiReqTypes.SysRoles.SetPermissions>(data: TReq) {
|
||||||
|
return request('/admin/sys_roles/set_permissions', { data });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Bosses: {
|
||||||
|
List<TReq = ApiReqTypes.Bosses.List>(data: TReq) {
|
||||||
|
return request('/admin/bosses/list', { data });
|
||||||
|
},
|
||||||
|
Store<TReq = ApiReqTypes.Bosses.Store>(data: TReq) {
|
||||||
|
return request('/admin/bosses/store', { data });
|
||||||
|
},
|
||||||
|
Update<TReq = ApiReqTypes.Bosses.Update>(data: TReq) {
|
||||||
|
return request('/admin/bosses/update', { data });
|
||||||
|
},
|
||||||
|
Delete<TReq = ApiReqTypes.Bosses.Delete>(data: TReq) {
|
||||||
|
return request('/admin/bosses/delete', { data });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Auth: {
|
||||||
|
Captcha() {
|
||||||
|
return request('/admin/auth/captcha', {});
|
||||||
|
},
|
||||||
|
Login<TReq = ApiReqTypes.Auth.Login>(data: TReq) {
|
||||||
|
return request('/admin/auth/login', { data });
|
||||||
|
},
|
||||||
|
Logout() {
|
||||||
|
return request('/admin/auth/logout', {});
|
||||||
|
},
|
||||||
|
Me() {
|
||||||
|
return request('/admin/auth/me', {});
|
||||||
|
},
|
||||||
|
ChangePassword<TReq = ApiReqTypes.Auth.ChangePassword>(data: TReq) {
|
||||||
|
return request('/admin/auth/change_password', { data });
|
||||||
|
},
|
||||||
|
PreUpload<TReq = ApiReqTypes.Auth.PreUpload>(data: TReq) {
|
||||||
|
return request('/admin/auth/pre_upload', { data });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SysPermissions: {
|
||||||
|
List<TReq = ApiReqTypes.SysPermissions.List, TResp = ApiRespTypes.SysPermissions.List>(data: TReq) {
|
||||||
|
return request<ResponseStructure<TResp>>('/admin/sys_permissions/list', { data });
|
||||||
|
},
|
||||||
|
Store<TReq = ApiReqTypes.SysPermissions.Store>(data: TReq) {
|
||||||
|
return request('/admin/sys_permissions/store', { data });
|
||||||
|
},
|
||||||
|
Update<TReq = ApiReqTypes.SysPermissions.Update>(data: TReq) {
|
||||||
|
return request('/admin/sys_permissions/update', { data });
|
||||||
|
},
|
||||||
|
Delete<TReq = ApiReqTypes.SysPermissions.Delete>(data: TReq) {
|
||||||
|
return request('/admin/sys_permissions/delete', { data });
|
||||||
|
},
|
||||||
|
Move<TReq = ApiReqTypes.SysPermissions.Move>(data: TReq) {
|
||||||
|
return request('/admin/sys_permissions/move', { data });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Admins: {
|
||||||
|
List<TReq = ApiReqTypes.Admins.List>(data: TReq) {
|
||||||
|
return request('/admin/admins/list', { data });
|
||||||
|
},
|
||||||
|
Store<TReq = ApiReqTypes.Admins.Store>(data: TReq) {
|
||||||
|
return request('/admin/admins/store', { data });
|
||||||
|
},
|
||||||
|
Update<TReq = ApiReqTypes.Admins.Update>(data: TReq) {
|
||||||
|
return request('/admin/admins/update', { data });
|
||||||
|
},
|
||||||
|
Delete<TReq = ApiReqTypes.Admins.Delete>(data: TReq) {
|
||||||
|
return request('/admin/admins/delete', { data });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
25
src/gen/Enums.ts
Normal file
25
src/gen/Enums.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// 模块类型
|
||||||
|
export const GuardNameEnum = {
|
||||||
|
ADMIN: { text: '平台', color: '#007BFF', value: 'ADMIN' },
|
||||||
|
CUSTOMER: { text: '机构', color: '#FFC107', value: 'CUSTOMER' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// 管理员状态
|
||||||
|
export const AdminsStatusEnum = {
|
||||||
|
ENABLED: { text: '启用', color: '#008000', value: 'ENABLED' },
|
||||||
|
DISABLED: { text: '禁用', color: '#808080', value: 'DISABLED' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// 系统权限类型
|
||||||
|
export const SysPermissionsTypeEnum = {
|
||||||
|
DIRECTORY: { text: '目录', color: '#0000FF', value: 'DIRECTORY' },
|
||||||
|
PAGE: { text: '页面', color: '#FFA500', value: 'PAGE' },
|
||||||
|
BUTTON: { text: '按钮', color: '#008000', value: 'BUTTON' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
export const EnableEnum = {
|
||||||
|
1: { text: '是', color: '#008000', value: 1 },
|
||||||
|
0: { text: '否', color: '#808080', value: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
41
src/hooks/dataSources.ts
Normal file
41
src/hooks/dataSources.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Apis } from '@/gen/Apis';
|
||||||
|
import { useRequest } from 'ahooks';
|
||||||
|
|
||||||
|
export enum DataSourceType {
|
||||||
|
SysPermissions = 'SysPermissions',
|
||||||
|
SysRoles = 'SysRoles'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取权限列表数据源
|
||||||
|
*/
|
||||||
|
export const useSysPermissionsDataSource = (params: ApiReqTypes.SysPermissions.List) => {
|
||||||
|
return useRequest(
|
||||||
|
async () => {
|
||||||
|
const response = await Apis.SysPermissions.List(params);
|
||||||
|
return response.data || [];
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// manual: true,
|
||||||
|
cacheKey: `${DataSourceType.SysPermissions}-${params.guard_name}`,
|
||||||
|
staleTime: -1, // 10秒内不重新请求
|
||||||
|
// cacheTime: 5 * 60 * 1000, // 缓存保持5分钟
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSysRolesDataSource = () => {
|
||||||
|
return useRequest(
|
||||||
|
async () => {
|
||||||
|
const response = await Apis.SysRoles.List();
|
||||||
|
return response.data || [];
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// manual: true,
|
||||||
|
cacheKey: `${DataSourceType.SysRoles}`,
|
||||||
|
staleTime: -1, // 10秒内不重新请求
|
||||||
|
// cacheTime: 5 * 60 * 1000, // 缓存保持5分钟
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
14
src/hooks/useParams.spec.ts
Normal file
14
src/hooks/useParams.spec.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('CLI', () => {
|
||||||
|
it('应该能正确执行命令', () => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
a: '1',
|
||||||
|
b: '2',
|
||||||
|
c: '3',
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
console.log('params', params);
|
||||||
|
expect(true).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
109
src/hooks/useParams.ts
Normal file
109
src/hooks/useParams.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { useSearchParams } from '@umijs/max';
|
||||||
|
import { SortOrder } from 'antd/es/table/interface';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export default function useParams<T>({ defaultParams }: { defaultParams: T }) {
|
||||||
|
const [ready, setReady] = useState(false);
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams(); // 浏览器参数
|
||||||
|
const [apiParams, setApiParams] = useState<T & Record<string, any>>(); // 请求参数
|
||||||
|
const [antParams, setAntParams] = useState<
|
||||||
|
T & { current?: number; pageSize?: number } & Record<string, any>
|
||||||
|
>(); // 表格参数
|
||||||
|
|
||||||
|
const antParamsToSearchParams = ({
|
||||||
|
params,
|
||||||
|
sort,
|
||||||
|
filter,
|
||||||
|
}: {
|
||||||
|
params: T & { pageSize?: number; current?: number };
|
||||||
|
sort: Record<string, SortOrder>;
|
||||||
|
filter: Record<string, (string | number)[] | null>;
|
||||||
|
}) => {
|
||||||
|
const { current = 1, pageSize = 20, ...rest } = params;
|
||||||
|
|
||||||
|
// 构造 p 参数,只包含非默认值
|
||||||
|
const pPayload: Record<string, any> = {};
|
||||||
|
if (current !== 1) {
|
||||||
|
pPayload.page = current;
|
||||||
|
}
|
||||||
|
if (pageSize !== 20) {
|
||||||
|
pPayload.perPage = pageSize;
|
||||||
|
}
|
||||||
|
if (Object.keys(rest).length > 0) {
|
||||||
|
Object.assign(pPayload, rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: Record<string, string> = {};
|
||||||
|
if (Object.keys(pPayload).length > 0) {
|
||||||
|
data.p = JSON.stringify(pPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sort && Object.keys(sort).length > 0) {
|
||||||
|
data.s = JSON.stringify(sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter && Object.keys(filter).length > 0) {
|
||||||
|
data.f = JSON.stringify(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchParams(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const antParamsToApiParams = ({
|
||||||
|
params,
|
||||||
|
sort,
|
||||||
|
filter,
|
||||||
|
}: {
|
||||||
|
params: T & { pageSize?: number; current?: number };
|
||||||
|
sort: Record<string, SortOrder>;
|
||||||
|
filter: Record<string, (string | number)[] | null>;
|
||||||
|
}) => {
|
||||||
|
const { current = 1, pageSize = 20, ...rest } = params;
|
||||||
|
const data: Record<string, any> = {};
|
||||||
|
|
||||||
|
// 如果 current 不是默认值,则添加 page
|
||||||
|
if (current !== 1) {
|
||||||
|
data.page = current;
|
||||||
|
}
|
||||||
|
// 如果 pageSize 不是默认值,则添加 perPage
|
||||||
|
if (pageSize !== 20) {
|
||||||
|
data.perPage = pageSize;
|
||||||
|
}
|
||||||
|
// 如果 rest 不为空,则合并 rest 到 data 中
|
||||||
|
if (Object.keys(rest).length > 0) {
|
||||||
|
Object.assign(data, rest);
|
||||||
|
}
|
||||||
|
// 仅当 sort 不为空时合并 sort
|
||||||
|
if (sort && Object.keys(sort).length > 0) {
|
||||||
|
Object.assign(data, sort);
|
||||||
|
}
|
||||||
|
// 仅当 filter 不为空时合并 filter
|
||||||
|
if (filter && Object.keys(filter).length > 0) {
|
||||||
|
Object.assign(data, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getApiParams = (params: {
|
||||||
|
params: T & { pageSize?: number; current?: number };
|
||||||
|
sort: Record<string, SortOrder>;
|
||||||
|
filter: Record<string, (string | number)[] | null>;
|
||||||
|
}) => {
|
||||||
|
antParamsToSearchParams(params);
|
||||||
|
const data = antParamsToApiParams(params);
|
||||||
|
setApiParams(data as T & Record<string, any>);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('searchParams', searchParams, defaultParams);
|
||||||
|
setReady(true);
|
||||||
|
}, [searchParams, defaultParams]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ready,
|
||||||
|
apiParams,
|
||||||
|
getApiParams,
|
||||||
|
};
|
||||||
|
}
|
||||||
28
src/pages/index.tsx
Normal file
28
src/pages/index.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Apis } from '@/gen/Apis';
|
||||||
|
import { useRequest } from '@umijs/max';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const { data, loading, run } = useRequest(
|
||||||
|
async () => {
|
||||||
|
return Apis.Auth.Me()
|
||||||
|
.then((res) => {
|
||||||
|
console.log('res', res);
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('err', err);
|
||||||
|
return err;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
manual: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={run} loading={loading}>
|
||||||
|
{data?.data?.user?.username ?? 'Click Me'}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/pages/login.tsx
Normal file
5
src/pages/login.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { MyLoginPage } from '@/common';
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
return <MyLoginPage />;
|
||||||
|
}
|
||||||
152
src/pages/system/admins/index.bak.tsx
Normal file
152
src/pages/system/admins/index.bak.tsx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import { DeleteButton, wrapTableResp } from '@/common';
|
||||||
|
import { Apis } from '@/gen/Apis';
|
||||||
|
import { EnableEnum } from '@/gen/Enums';
|
||||||
|
import useParams from '@/hooks/useParams';
|
||||||
|
import {
|
||||||
|
ActionType,
|
||||||
|
PageContainer,
|
||||||
|
ProFormInstance,
|
||||||
|
ProTable,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import { Space } from 'antd';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import Create from './modals/Create';
|
||||||
|
import Update from './modals/Update';
|
||||||
|
|
||||||
|
type TableType = ApiRespTypes.Admins.List;
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const title = '管理员';
|
||||||
|
const ref = useRef<ActionType>();
|
||||||
|
const formRef = useRef<ProFormInstance>();
|
||||||
|
|
||||||
|
const { ready, getApiParams } = useParams<ApiReqTypes.Admins.List>({
|
||||||
|
defaultParams: {
|
||||||
|
username: '123',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDelete = useMemoizedFn((entity: TableType[0]) => {
|
||||||
|
Apis.SysRoles.Delete({ id: entity.id }).then(() => {
|
||||||
|
ref.current?.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
formRef.current?.setFieldsValue({
|
||||||
|
username: '123',
|
||||||
|
});
|
||||||
|
}, [ready]);
|
||||||
|
|
||||||
|
return !ready ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<PageContainer>
|
||||||
|
<ProTable<TableType[0]>
|
||||||
|
actionRef={ref}
|
||||||
|
formRef={formRef}
|
||||||
|
scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
|
||||||
|
bordered
|
||||||
|
size="small"
|
||||||
|
rowKey="id"
|
||||||
|
options={false}
|
||||||
|
toolBarRender={(action) => [
|
||||||
|
<Create
|
||||||
|
key="Create"
|
||||||
|
onFinish={() => action?.reload()}
|
||||||
|
title={title}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
request={async (params, sort, filter) =>
|
||||||
|
wrapTableResp(
|
||||||
|
await Apis.Admins.List(
|
||||||
|
getApiParams({
|
||||||
|
params,
|
||||||
|
sort,
|
||||||
|
filter,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// dataSource={data}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户名',
|
||||||
|
dataIndex: 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '是否锁定',
|
||||||
|
dataIndex: 'is_locked',
|
||||||
|
valueEnum: EnableEnum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '更新时间',
|
||||||
|
dataIndex: 'updated_at',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
fixed: 'right',
|
||||||
|
width: '150px',
|
||||||
|
hideInSearch: true,
|
||||||
|
render: (_1, entity, _2, action) => {
|
||||||
|
return (
|
||||||
|
<Space key={entity.id}>
|
||||||
|
<Update
|
||||||
|
item={entity}
|
||||||
|
title={title}
|
||||||
|
onFinish={() => action?.reload()}
|
||||||
|
/>
|
||||||
|
<DeleteButton onConfirm={() => handleDelete(entity)} />
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
src/pages/system/admins/index.tsx
Normal file
60
src/pages/system/admins/index.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
ActionType,
|
||||||
|
ProFormInstance,
|
||||||
|
ProTable,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
const MyComponent = () => {
|
||||||
|
const formRef = useRef<ProFormInstance>(); // 表单引用
|
||||||
|
const actionRef = useRef<ActionType>(); // 表格操作引用
|
||||||
|
|
||||||
|
// 动态设置搜索表单值的函数
|
||||||
|
const setSearchValues = () => {
|
||||||
|
if (formRef.current) {
|
||||||
|
formRef.current?.setFieldsValue({
|
||||||
|
name: '新名称',
|
||||||
|
status: 'active',
|
||||||
|
});
|
||||||
|
// 可选:提交表单以触发搜索
|
||||||
|
formRef.current?.submit();
|
||||||
|
// 或手动刷新表格
|
||||||
|
actionRef.current?.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={setSearchValues}>设置搜索值</Button>
|
||||||
|
<ProTable
|
||||||
|
formRef={formRef}
|
||||||
|
actionRef={actionRef}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
valueType: 'select',
|
||||||
|
valueEnum: {
|
||||||
|
active: { text: '活跃' },
|
||||||
|
inactive: { text: '禁用' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
request={async (params) => {
|
||||||
|
// 根据表单参数发起请求
|
||||||
|
console.log('请求参数:', params);
|
||||||
|
return { data: [], success: true };
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MyComponent;
|
||||||
169
src/pages/system/admins/indexbak2.tsx
Normal file
169
src/pages/system/admins/indexbak2.tsx
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import { DeleteButton } from '@/common';
|
||||||
|
import { Apis } from '@/gen/Apis';
|
||||||
|
import { EnableEnum } from '@/gen/Enums';
|
||||||
|
import {
|
||||||
|
BetaSchemaForm,
|
||||||
|
PageContainer,
|
||||||
|
ProColumns,
|
||||||
|
ProTable,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { useSearchParams } from '@umijs/max';
|
||||||
|
import { useMemoizedFn, useRequest } from 'ahooks';
|
||||||
|
import { Space } from 'antd';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import Update from './modals/Update';
|
||||||
|
|
||||||
|
type SearchType = ApiReqTypes.Admins.List;
|
||||||
|
type TableType = ApiRespTypes.Admins.List;
|
||||||
|
|
||||||
|
interface TableParams<T> {
|
||||||
|
s?: T;
|
||||||
|
p?: {
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
};
|
||||||
|
o?: {
|
||||||
|
field: string;
|
||||||
|
order: 'asc' | 'desc';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminList() {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const [params, setParams] = useState<TableParams<SearchType>>({
|
||||||
|
s: JSON.parse(searchParams.get('s') || '{}'),
|
||||||
|
p: JSON.parse(searchParams.get('p') || '{}'),
|
||||||
|
o: JSON.parse(searchParams.get('o') || '{}'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const title = '管理员';
|
||||||
|
|
||||||
|
const { data, run } = useRequest(
|
||||||
|
() =>
|
||||||
|
Apis.Admins.List({
|
||||||
|
...params.s,
|
||||||
|
...params.p,
|
||||||
|
...params.o,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
refreshDeps: [params],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDelete = useMemoizedFn((entity: TableType[0]) => {
|
||||||
|
Apis.SysRoles.Delete({ id: entity.id }).then(() => {
|
||||||
|
run();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = useMemo<ProColumns<TableType[0]>[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
dataIndex: 'id',
|
||||||
|
title: 'ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户名',
|
||||||
|
dataIndex: 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '是否锁定',
|
||||||
|
dataIndex: 'is_locked',
|
||||||
|
valueEnum: EnableEnum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '更新时间',
|
||||||
|
dataIndex: 'updated_at',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
fixed: 'right',
|
||||||
|
width: '150px',
|
||||||
|
hideInSearch: true,
|
||||||
|
render: (_1, entity, _2, action) => (
|
||||||
|
<Space key={entity.id}>
|
||||||
|
<Update
|
||||||
|
item={entity}
|
||||||
|
title={title}
|
||||||
|
onFinish={() => action?.reload()}
|
||||||
|
/>
|
||||||
|
<DeleteButton onConfirm={() => handleDelete(entity)} />
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearch = (values: SearchType) => {
|
||||||
|
const newParams = {
|
||||||
|
...params,
|
||||||
|
s: values,
|
||||||
|
};
|
||||||
|
setParams(newParams);
|
||||||
|
setSearchParams({
|
||||||
|
...searchParams,
|
||||||
|
s: JSON.stringify(values),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePaginationChange = (page: number, pageSize: number) => {
|
||||||
|
setParams({
|
||||||
|
...params,
|
||||||
|
p: {
|
||||||
|
page,
|
||||||
|
perPage: pageSize,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<BetaSchemaForm<SearchType>
|
||||||
|
style={{ backgroundColor: 'white' }}
|
||||||
|
layoutType="QueryFilter"
|
||||||
|
initialValues={params.s}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
key: 'username',
|
||||||
|
title: '用户名',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onReset={() => {
|
||||||
|
setParams({
|
||||||
|
...params,
|
||||||
|
s: {},
|
||||||
|
});
|
||||||
|
setSearchParams({
|
||||||
|
...searchParams,
|
||||||
|
s: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onFinish={handleSearch}
|
||||||
|
/>
|
||||||
|
<ProTable<TableType[0]>
|
||||||
|
scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
|
||||||
|
bordered
|
||||||
|
size="small"
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={data?.data}
|
||||||
|
search={false}
|
||||||
|
pagination={{
|
||||||
|
current: params.p?.page || 1,
|
||||||
|
total: data?.meta?.total || 100,
|
||||||
|
pageSize: params.p?.perPage || 10,
|
||||||
|
onChange: handlePaginationChange,
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
src/pages/system/admins/modals/Create.tsx
Normal file
60
src/pages/system/admins/modals/Create.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { AddButton } from '@/common';
|
||||||
|
import { Apis } from '@/gen/Apis';
|
||||||
|
import { SysPermissionsTypeEnum } from '@/gen/Enums';
|
||||||
|
import { BetaSchemaForm } from '@ant-design/pro-components';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
type FormType = ApiReqTypes.Admins.Store;
|
||||||
|
|
||||||
|
export default function Create(props: { title: string; onFinish: () => void }) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleFinish = useMemoizedFn(async (values: FormType) => {
|
||||||
|
setLoading(true);
|
||||||
|
return Apis.Admins.Store(values)
|
||||||
|
.then(() => {
|
||||||
|
message.success('添加成功');
|
||||||
|
props.onFinish();
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BetaSchemaForm<FormType>
|
||||||
|
title={`添加${props.title}`}
|
||||||
|
layoutType="ModalForm"
|
||||||
|
trigger={<AddButton title={props.title} />}
|
||||||
|
modalProps={{
|
||||||
|
maskClosable: false,
|
||||||
|
destroyOnClose: true,
|
||||||
|
}}
|
||||||
|
initialValues={{
|
||||||
|
type: SysPermissionsTypeEnum.PAGE.value,
|
||||||
|
}}
|
||||||
|
onFinish={handleFinish}
|
||||||
|
disabled={loading}
|
||||||
|
loading={loading}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
key: 'username',
|
||||||
|
title: '用户名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'password',
|
||||||
|
title: '密码',
|
||||||
|
valueType: 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'is_locked',
|
||||||
|
title: '是否锁定',
|
||||||
|
valueType: 'switch',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
src/pages/system/admins/modals/Update.tsx
Normal file
71
src/pages/system/admins/modals/Update.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { EditButton } from '@/common';
|
||||||
|
import { Apis } from '@/gen/Apis';
|
||||||
|
import { BetaSchemaForm } from '@ant-design/pro-components';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import { Form, message } from 'antd';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
type TableType = ApiRespTypes.Admins.List;
|
||||||
|
type FormType = ApiReqTypes.Admins.Update;
|
||||||
|
|
||||||
|
export default function Update(props: {
|
||||||
|
item: TableType[0];
|
||||||
|
title: string;
|
||||||
|
onFinish: () => void;
|
||||||
|
}) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleFinish = useMemoizedFn(async (values: FormType) => {
|
||||||
|
setLoading(true);
|
||||||
|
return Apis.Admins.Update({
|
||||||
|
...values,
|
||||||
|
id: props.item.id,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
message.success('编辑成功');
|
||||||
|
props.onFinish();
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BetaSchemaForm<FormType>
|
||||||
|
form={form}
|
||||||
|
title={`编辑${props.title}`}
|
||||||
|
layoutType="ModalForm"
|
||||||
|
trigger={<EditButton />}
|
||||||
|
modalProps={{
|
||||||
|
maskClosable: false,
|
||||||
|
destroyOnClose: true,
|
||||||
|
}}
|
||||||
|
onOpenChange={(open: any) => {
|
||||||
|
if (open && props.item) {
|
||||||
|
form.setFieldsValue(props.item);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onFinish={handleFinish}
|
||||||
|
disabled={loading}
|
||||||
|
loading={loading}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
key: 'username',
|
||||||
|
title: '用户名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'password',
|
||||||
|
title: '密码',
|
||||||
|
valueType: 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'is_locked',
|
||||||
|
title: '是否锁定',
|
||||||
|
valueType: 'switch',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
143
src/pages/system/sys_permissions/index.tsx
Normal file
143
src/pages/system/sys_permissions/index.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { DeleteButton } from '@/common';
|
||||||
|
import { Apis } from '@/gen/Apis';
|
||||||
|
import { SysPermissionsTypeEnum } from '@/gen/Enums';
|
||||||
|
import {
|
||||||
|
DataSourceType,
|
||||||
|
useSysPermissionsDataSource,
|
||||||
|
} from '@/hooks/dataSources';
|
||||||
|
import { DownOutlined, UpOutlined } from '@ant-design/icons';
|
||||||
|
import { PageContainer, ProTable } from '@ant-design/pro-components';
|
||||||
|
import { clearCache, useMemoizedFn } from 'ahooks';
|
||||||
|
import { Button, message, Space, Tag } from 'antd';
|
||||||
|
import Create from './modals/Create';
|
||||||
|
import Update from './modals/Update';
|
||||||
|
|
||||||
|
type TableType = ApiRespTypes.SysPermissions.List;
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const title = '权限';
|
||||||
|
|
||||||
|
const { data, run } = useSysPermissionsDataSource({
|
||||||
|
guard_name: process.env.GUARD_NAME || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCreateFinish = useMemoizedFn(() => {
|
||||||
|
clearCache(DataSourceType.SysPermissions);
|
||||||
|
run();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleUpdateFinish = useMemoizedFn(() => {
|
||||||
|
clearCache(DataSourceType.SysPermissions);
|
||||||
|
run();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleMove = useMemoizedFn(
|
||||||
|
(entity: TableType[0], type: 'up' | 'down') => {
|
||||||
|
Apis.SysPermissions.Move({
|
||||||
|
id: entity.id,
|
||||||
|
type,
|
||||||
|
}).then(() => {
|
||||||
|
clearCache(DataSourceType.SysPermissions);
|
||||||
|
run();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDelete = useMemoizedFn((entity: TableType[0]) => {
|
||||||
|
Apis.SysPermissions.Delete({
|
||||||
|
id: entity.id,
|
||||||
|
}).then(() => {
|
||||||
|
message.success('删除成功');
|
||||||
|
clearCache(DataSourceType.SysPermissions);
|
||||||
|
run();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// run();
|
||||||
|
// }, []);
|
||||||
|
|
||||||
|
return !data ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<PageContainer>
|
||||||
|
<ProTable<TableType[0]>
|
||||||
|
scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
|
||||||
|
bordered
|
||||||
|
size="small"
|
||||||
|
rowKey="id"
|
||||||
|
search={false}
|
||||||
|
pagination={false}
|
||||||
|
options={false}
|
||||||
|
expandable={{
|
||||||
|
defaultExpandAllRows: true,
|
||||||
|
}}
|
||||||
|
toolBarRender={() => [
|
||||||
|
<Create
|
||||||
|
key="Create"
|
||||||
|
guard_name={process.env.GUARD_NAME || ''}
|
||||||
|
onFinish={handleCreateFinish}
|
||||||
|
title={title}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
dataSource={data}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
render: (_, entity) => {
|
||||||
|
return `${entity.id}_${entity.name}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ title: 'icon', dataIndex: 'icon' },
|
||||||
|
{
|
||||||
|
title: '类型',
|
||||||
|
dataIndex: 'type',
|
||||||
|
valueEnum: SysPermissionsTypeEnum,
|
||||||
|
},
|
||||||
|
{ title: '链接', dataIndex: 'path' },
|
||||||
|
{ title: '前端表示', dataIndex: 'key' },
|
||||||
|
{
|
||||||
|
title: '后端API',
|
||||||
|
dataIndex: 'backend_apis',
|
||||||
|
render: (_, entity) => {
|
||||||
|
return (
|
||||||
|
<Space direction="vertical">
|
||||||
|
{entity.backend_apis?.map((item: string) => (
|
||||||
|
<Tag key={item}>{item}</Tag>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
render: (_, entity) => {
|
||||||
|
return (
|
||||||
|
<Space key={entity.id}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<UpOutlined />}
|
||||||
|
disabled={!entity.parent_id}
|
||||||
|
onClick={() => handleMove(entity, 'up')}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<DownOutlined />}
|
||||||
|
disabled={!entity.parent_id}
|
||||||
|
onClick={() => handleMove(entity, 'down')}
|
||||||
|
/>
|
||||||
|
<Update
|
||||||
|
item={entity}
|
||||||
|
title={title}
|
||||||
|
onFinish={handleUpdateFinish}
|
||||||
|
/>
|
||||||
|
<DeleteButton onConfirm={() => handleDelete(entity)} />
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
89
src/pages/system/sys_permissions/modals/Create.tsx
Normal file
89
src/pages/system/sys_permissions/modals/Create.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { AddButton, MyIcons } from '@/common';
|
||||||
|
import { PermissionTreeSelect } from '@/components/PermissionTreeSelect';
|
||||||
|
import { Apis } from '@/gen/Apis';
|
||||||
|
import { SysPermissionsTypeEnum } from '@/gen/Enums';
|
||||||
|
import { BetaSchemaForm } from '@ant-design/pro-components';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import { message, Space } from 'antd';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
type FormType = ApiReqTypes.SysPermissions.Store;
|
||||||
|
|
||||||
|
export default function Create(props: {
|
||||||
|
title: string;
|
||||||
|
guard_name: string;
|
||||||
|
onFinish: () => void;
|
||||||
|
}) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleFinish = useMemoizedFn(async (values: FormType) => {
|
||||||
|
setLoading(true);
|
||||||
|
return Apis.SysPermissions.Store({
|
||||||
|
...values,
|
||||||
|
guard_name: props.guard_name,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
message.success('添加成功');
|
||||||
|
props.onFinish();
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BetaSchemaForm<FormType>
|
||||||
|
title={`添加${props.title}`}
|
||||||
|
layoutType="ModalForm"
|
||||||
|
trigger={<AddButton title={props.title} />}
|
||||||
|
modalProps={{
|
||||||
|
maskClosable: false,
|
||||||
|
destroyOnClose: true,
|
||||||
|
}}
|
||||||
|
initialValues={{
|
||||||
|
type: SysPermissionsTypeEnum.PAGE.value,
|
||||||
|
}}
|
||||||
|
onFinish={handleFinish}
|
||||||
|
disabled={loading}
|
||||||
|
loading={loading}
|
||||||
|
columns={[
|
||||||
|
PermissionTreeSelect(props.guard_name),
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
title: '名称',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'key',
|
||||||
|
title: '前端权限识别符',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'icon',
|
||||||
|
title: '图标',
|
||||||
|
valueType: 'select',
|
||||||
|
request: async () => {
|
||||||
|
return Object.entries(MyIcons).map(([key, value]) => ({
|
||||||
|
label: (
|
||||||
|
<Space style={{ gap: '5px' }}>
|
||||||
|
{value}
|
||||||
|
<span>{key}</span>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
value: key,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'type',
|
||||||
|
title: '类型',
|
||||||
|
valueType: 'radioButton',
|
||||||
|
fieldProps: {
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
},
|
||||||
|
valueEnum: SysPermissionsTypeEnum,
|
||||||
|
},
|
||||||
|
{ key: 'path', title: '路由' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
95
src/pages/system/sys_permissions/modals/Update.tsx
Normal file
95
src/pages/system/sys_permissions/modals/Update.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { EditButton, MyIcons } from '@/common';
|
||||||
|
import { PermissionTreeSelect } from '@/components/PermissionTreeSelect';
|
||||||
|
import { Apis } from '@/gen/Apis';
|
||||||
|
import { SysPermissionsTypeEnum } from '@/gen/Enums';
|
||||||
|
import { BetaSchemaForm } from '@ant-design/pro-components';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import { Form, message, Space } from 'antd';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
type TableType = ApiRespTypes.SysPermissions.List;
|
||||||
|
type FormType = ApiReqTypes.SysPermissions.Update;
|
||||||
|
|
||||||
|
export default function Update(props: {
|
||||||
|
item: TableType[0];
|
||||||
|
title: string;
|
||||||
|
onFinish: () => void;
|
||||||
|
}) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleFinish = useMemoizedFn(async (values: FormType) => {
|
||||||
|
setLoading(true);
|
||||||
|
return Apis.SysPermissions.Update({
|
||||||
|
...values,
|
||||||
|
guard_name: props.item.guard_name,
|
||||||
|
id: props.item.id,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
message.success('编辑成功');
|
||||||
|
props.onFinish();
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BetaSchemaForm<FormType>
|
||||||
|
form={form}
|
||||||
|
title={`编辑${props.title}`}
|
||||||
|
layoutType="ModalForm"
|
||||||
|
trigger={<EditButton />}
|
||||||
|
modalProps={{
|
||||||
|
maskClosable: false,
|
||||||
|
destroyOnClose: true,
|
||||||
|
}}
|
||||||
|
onOpenChange={(open: any) => {
|
||||||
|
if (open && props.item) {
|
||||||
|
form.setFieldsValue(props.item);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onFinish={handleFinish}
|
||||||
|
disabled={loading}
|
||||||
|
loading={loading}
|
||||||
|
columns={[
|
||||||
|
PermissionTreeSelect(props.item.guard_name),
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
title: '名称',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'key',
|
||||||
|
title: '前端权限识别符',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'icon',
|
||||||
|
title: '图标',
|
||||||
|
valueType: 'select',
|
||||||
|
request: async () => {
|
||||||
|
return Object.entries(MyIcons).map(([key, value]) => ({
|
||||||
|
label: (
|
||||||
|
<Space style={{ gap: '5px' }}>
|
||||||
|
{value}
|
||||||
|
<span>{key}</span>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
value: key,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'type',
|
||||||
|
title: '类型',
|
||||||
|
valueType: 'radioButton',
|
||||||
|
fieldProps: {
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
},
|
||||||
|
valueEnum: SysPermissionsTypeEnum,
|
||||||
|
},
|
||||||
|
{ key: 'path', title: '路由' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
95
src/pages/system/sys_roles/index.tsx
Normal file
95
src/pages/system/sys_roles/index.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { DeleteButton } from '@/common';
|
||||||
|
import { Apis } from '@/gen/Apis';
|
||||||
|
import { GuardNameEnum } from '@/gen/Enums';
|
||||||
|
import { DataSourceType, useSysRolesDataSource } from '@/hooks/dataSources';
|
||||||
|
import { PageContainer, ProTable } from '@ant-design/pro-components';
|
||||||
|
import { clearCache, useMemoizedFn } from 'ahooks';
|
||||||
|
import { Space, Tag } from 'antd';
|
||||||
|
import Create from './modals/Create';
|
||||||
|
import Update from './modals/Update';
|
||||||
|
|
||||||
|
type TableType = ApiRespTypes.SysRoles.List;
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const title = '角色';
|
||||||
|
|
||||||
|
const { data, run } = useSysRolesDataSource();
|
||||||
|
|
||||||
|
const handleCreateFinish = useMemoizedFn(() => {
|
||||||
|
clearCache(DataSourceType.SysRoles);
|
||||||
|
run();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleUpdateFinish = useMemoizedFn(() => {
|
||||||
|
clearCache(DataSourceType.SysRoles);
|
||||||
|
run();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDelete = useMemoizedFn((entity: TableType[0]) => {
|
||||||
|
Apis.SysRoles.Delete({ id: entity.id }).then(() => {
|
||||||
|
clearCache(DataSourceType.SysRoles);
|
||||||
|
run();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return !data ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<PageContainer>
|
||||||
|
<ProTable<TableType[0]>
|
||||||
|
scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
|
||||||
|
bordered
|
||||||
|
size="small"
|
||||||
|
rowKey="id"
|
||||||
|
options={false}
|
||||||
|
expandable={{
|
||||||
|
defaultExpandAllRows: true,
|
||||||
|
}}
|
||||||
|
toolBarRender={() => [
|
||||||
|
<Create key="Create" onFinish={handleCreateFinish} title={title} />,
|
||||||
|
]}
|
||||||
|
dataSource={data}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '权限组',
|
||||||
|
dataIndex: 'guard_name',
|
||||||
|
valueEnum: GuardNameEnum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '颜色',
|
||||||
|
dataIndex: 'color',
|
||||||
|
renderText: (color) => <Tag color={color as string}>{color}</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
render: (_, entity) => {
|
||||||
|
return (
|
||||||
|
<Space key={entity.id}>
|
||||||
|
<Update
|
||||||
|
item={entity}
|
||||||
|
title={title}
|
||||||
|
onFinish={handleUpdateFinish}
|
||||||
|
/>
|
||||||
|
<DeleteButton onConfirm={() => handleDelete(entity)} />
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
src/pages/system/sys_roles/modals/Create.tsx
Normal file
64
src/pages/system/sys_roles/modals/Create.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { AddButton, MyColorPicker } from '@/common';
|
||||||
|
import { Apis } from '@/gen/Apis';
|
||||||
|
import { GuardNameEnum, SysPermissionsTypeEnum } from '@/gen/Enums';
|
||||||
|
import { BetaSchemaForm } from '@ant-design/pro-components';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
type FormType = ApiReqTypes.SysRoles.Store;
|
||||||
|
|
||||||
|
export default function Create(props: { title: string; onFinish: () => void }) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleFinish = useMemoizedFn(async (values: FormType) => {
|
||||||
|
setLoading(true);
|
||||||
|
return Apis.SysRoles.Store(values)
|
||||||
|
.then(() => {
|
||||||
|
message.success('添加成功');
|
||||||
|
props.onFinish();
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BetaSchemaForm<FormType>
|
||||||
|
title={`添加${props.title}`}
|
||||||
|
layoutType="ModalForm"
|
||||||
|
trigger={<AddButton title={props.title} />}
|
||||||
|
modalProps={{
|
||||||
|
maskClosable: false,
|
||||||
|
destroyOnClose: true,
|
||||||
|
}}
|
||||||
|
initialValues={{
|
||||||
|
type: SysPermissionsTypeEnum.PAGE.value,
|
||||||
|
}}
|
||||||
|
onFinish={handleFinish}
|
||||||
|
disabled={loading}
|
||||||
|
loading={loading}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
title: '名称',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'guard_name',
|
||||||
|
title: '权限组',
|
||||||
|
valueType: 'radioButton',
|
||||||
|
fieldProps: {
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
},
|
||||||
|
valueEnum: GuardNameEnum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'color',
|
||||||
|
title: '颜色',
|
||||||
|
renderFormItem: () => <MyColorPicker />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
76
src/pages/system/sys_roles/modals/Update.tsx
Normal file
76
src/pages/system/sys_roles/modals/Update.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { EditButton, MyColorPicker } from '@/common';
|
||||||
|
import { Apis } from '@/gen/Apis';
|
||||||
|
import { GuardNameEnum } from '@/gen/Enums';
|
||||||
|
import { BetaSchemaForm } from '@ant-design/pro-components';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import { Form, message } from 'antd';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
type TableType = ApiRespTypes.SysRoles.List;
|
||||||
|
type FormType = ApiReqTypes.SysRoles.Update;
|
||||||
|
|
||||||
|
export default function Update(props: {
|
||||||
|
item: TableType[0];
|
||||||
|
title: string;
|
||||||
|
onFinish: () => void;
|
||||||
|
}) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleFinish = useMemoizedFn(async (values: FormType) => {
|
||||||
|
setLoading(true);
|
||||||
|
return Apis.SysRoles.Update({
|
||||||
|
...values,
|
||||||
|
id: props.item.id,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
message.success('编辑成功');
|
||||||
|
props.onFinish();
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BetaSchemaForm<FormType>
|
||||||
|
form={form}
|
||||||
|
title={`编辑${props.title}`}
|
||||||
|
layoutType="ModalForm"
|
||||||
|
trigger={<EditButton />}
|
||||||
|
modalProps={{
|
||||||
|
maskClosable: false,
|
||||||
|
destroyOnClose: true,
|
||||||
|
}}
|
||||||
|
onOpenChange={(open: any) => {
|
||||||
|
if (open && props.item) {
|
||||||
|
form.setFieldsValue(props.item);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onFinish={handleFinish}
|
||||||
|
disabled={loading}
|
||||||
|
loading={loading}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
title: '名称',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'guard_name',
|
||||||
|
title: '权限组',
|
||||||
|
valueType: 'radioButton',
|
||||||
|
fieldProps: {
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
},
|
||||||
|
valueEnum: GuardNameEnum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'color',
|
||||||
|
title: '颜色',
|
||||||
|
renderFormItem: () => <MyColorPicker />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
96
src/services/demo/UserController.ts
Normal file
96
src/services/demo/UserController.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// 该文件由 OneAPI 自动生成,请勿手动修改!
|
||||||
|
import { request } from '@umijs/max';
|
||||||
|
|
||||||
|
/** 此处后端没有提供注释 GET /api/v1/queryUserList */
|
||||||
|
export async function queryUserList(
|
||||||
|
params: {
|
||||||
|
// query
|
||||||
|
/** keyword */
|
||||||
|
keyword?: string;
|
||||||
|
/** current */
|
||||||
|
current?: number;
|
||||||
|
/** pageSize */
|
||||||
|
pageSize?: number;
|
||||||
|
},
|
||||||
|
options?: { [key: string]: any },
|
||||||
|
) {
|
||||||
|
return request<API.Result_PageInfo_UserInfo__>('/api/v1/queryUserList', {
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 此处后端没有提供注释 POST /api/v1/user */
|
||||||
|
export async function addUser(
|
||||||
|
body?: API.UserInfoVO,
|
||||||
|
options?: { [key: string]: any },
|
||||||
|
) {
|
||||||
|
return request<API.Result_UserInfo_>('/api/v1/user', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 此处后端没有提供注释 GET /api/v1/user/${param0} */
|
||||||
|
export async function getUserDetail(
|
||||||
|
params: {
|
||||||
|
// path
|
||||||
|
/** userId */
|
||||||
|
userId?: string;
|
||||||
|
},
|
||||||
|
options?: { [key: string]: any },
|
||||||
|
) {
|
||||||
|
const { userId: param0 } = params;
|
||||||
|
return request<API.Result_UserInfo_>(`/api/v1/user/${param0}`, {
|
||||||
|
method: 'GET',
|
||||||
|
params: { ...params },
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 此处后端没有提供注释 PUT /api/v1/user/${param0} */
|
||||||
|
export async function modifyUser(
|
||||||
|
params: {
|
||||||
|
// path
|
||||||
|
/** userId */
|
||||||
|
userId?: string;
|
||||||
|
},
|
||||||
|
body?: API.UserInfoVO,
|
||||||
|
options?: { [key: string]: any },
|
||||||
|
) {
|
||||||
|
const { userId: param0 } = params;
|
||||||
|
return request<API.Result_UserInfo_>(`/api/v1/user/${param0}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
params: { ...params },
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 此处后端没有提供注释 DELETE /api/v1/user/${param0} */
|
||||||
|
export async function deleteUser(
|
||||||
|
params: {
|
||||||
|
// path
|
||||||
|
/** userId */
|
||||||
|
userId?: string;
|
||||||
|
},
|
||||||
|
options?: { [key: string]: any },
|
||||||
|
) {
|
||||||
|
const { userId: param0 } = params;
|
||||||
|
return request<API.Result_string_>(`/api/v1/user/${param0}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
params: { ...params },
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
7
src/services/demo/index.ts
Normal file
7
src/services/demo/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// 该文件由 OneAPI 自动生成,请勿手动修改!
|
||||||
|
|
||||||
|
import * as UserController from './UserController';
|
||||||
|
export default {
|
||||||
|
UserController,
|
||||||
|
};
|
||||||
68
src/services/demo/typings.d.ts
vendored
Normal file
68
src/services/demo/typings.d.ts
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// 该文件由 OneAPI 自动生成,请勿手动修改!
|
||||||
|
|
||||||
|
declare namespace API {
|
||||||
|
interface PageInfo {
|
||||||
|
/**
|
||||||
|
1 */
|
||||||
|
current?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
total?: number;
|
||||||
|
list?: Array<Record<string, any>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageInfo_UserInfo_ {
|
||||||
|
/**
|
||||||
|
1 */
|
||||||
|
current?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
total?: number;
|
||||||
|
list?: Array<UserInfo>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Result {
|
||||||
|
success?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
data?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Result_PageInfo_UserInfo__ {
|
||||||
|
success?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
data?: PageInfo_UserInfo_;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Result_UserInfo_ {
|
||||||
|
success?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
data?: UserInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Result_string_ {
|
||||||
|
success?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
data?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserGenderEnum = 'MALE' | 'FEMALE';
|
||||||
|
|
||||||
|
interface UserInfo {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
/** nick */
|
||||||
|
nickName?: string;
|
||||||
|
/** email */
|
||||||
|
email?: string;
|
||||||
|
gender?: UserGenderEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserInfoVO {
|
||||||
|
name?: string;
|
||||||
|
/** nick */
|
||||||
|
nickName?: string;
|
||||||
|
/** email */
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type definitions_0 = null;
|
||||||
|
}
|
||||||
1
src/types/session.ts
Normal file
1
src/types/session.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
4
src/utils/format.ts
Normal file
4
src/utils/format.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// 示例方法,没有实际意义
|
||||||
|
export function trim(str: string) {
|
||||||
|
return str.trim();
|
||||||
|
}
|
||||||
3
tsconfig.json
Normal file
3
tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "./src/.umi/tsconfig.json"
|
||||||
|
}
|
||||||
1
typings.d.ts
vendored
Normal file
1
typings.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
import '@umijs/max/typings';
|
||||||
103
workflow.md
Normal file
103
workflow.md
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
# 微信 H5 登录流程
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
|用户|
|
||||||
|
start
|
||||||
|
:访问H5页面;
|
||||||
|
:点击微信登录;
|
||||||
|
:处理授权请求;
|
||||||
|
if (是否同意?) then (是)
|
||||||
|
:等待登录结果;
|
||||||
|
else (否)
|
||||||
|
:授权失败;
|
||||||
|
end
|
||||||
|
endif
|
||||||
|
|
||||||
|
|H5前端|
|
||||||
|
:加载页面;
|
||||||
|
:配置wx.config;
|
||||||
|
if (配置成功?) then (是)
|
||||||
|
:调用wx.login;
|
||||||
|
:发送code到后端;
|
||||||
|
:等待后端响应;
|
||||||
|
if (登录结果?) then (成功)
|
||||||
|
:展示用户信息;
|
||||||
|
else (禁用)
|
||||||
|
:显示禁用提示;
|
||||||
|
endif
|
||||||
|
else (否)
|
||||||
|
:显示配置失败;
|
||||||
|
end
|
||||||
|
endif
|
||||||
|
|
||||||
|
|微信客户端|
|
||||||
|
:处理配置请求;
|
||||||
|
:返回配置结果;
|
||||||
|
:获取用户授权;
|
||||||
|
:生成临时code;
|
||||||
|
|
||||||
|
|后端服务器|
|
||||||
|
:接收code;
|
||||||
|
:换取access_token;
|
||||||
|
:获取用户信息;
|
||||||
|
:查询用户状态;
|
||||||
|
if (用户状态?) then (正常)
|
||||||
|
:生成登录态;
|
||||||
|
else (禁用)
|
||||||
|
:返回禁用信息;
|
||||||
|
endif
|
||||||
|
|
||||||
|
|数据库|
|
||||||
|
:查询用户记录;
|
||||||
|
:返回用户状态;
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
## 流程说明
|
||||||
|
|
||||||
|
1. 用户泳道:
|
||||||
|
|
||||||
|
- 访问 H5 页面
|
||||||
|
- 点击微信登录按钮
|
||||||
|
- 处理授权请求
|
||||||
|
- 等待登录结果
|
||||||
|
|
||||||
|
2. H5 前端泳道:
|
||||||
|
|
||||||
|
- 加载页面
|
||||||
|
- 配置 wx.config
|
||||||
|
- 调用 wx.login 获取 code
|
||||||
|
- 发送 code 到后端
|
||||||
|
- 根据响应显示结果
|
||||||
|
|
||||||
|
3. 微信客户端泳道:
|
||||||
|
|
||||||
|
- 处理配置请求
|
||||||
|
- 获取用户授权
|
||||||
|
- 生成临时 code
|
||||||
|
|
||||||
|
4. 后端服务器泳道:
|
||||||
|
|
||||||
|
- 接收 code
|
||||||
|
- 换取 access_token
|
||||||
|
- 获取用户信息
|
||||||
|
- 查询用户状态
|
||||||
|
- 生成登录态或返回禁用信息
|
||||||
|
|
||||||
|
5. 数据库泳道:
|
||||||
|
- 查询用户记录
|
||||||
|
- 返回用户状态
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. code 的有效期很短,通常只有 5 分钟
|
||||||
|
2. access_token 需要安全存储在后端,避免泄露
|
||||||
|
3. 建议后端生成自己的登录态 token 返回给前端
|
||||||
|
4. 需要处理用户拒绝授权的情况
|
||||||
|
5. 需要处理各种网络异常情况
|
||||||
|
6. 建议实现静默登录机制,提升用户体验
|
||||||
|
7. 用户禁用状态应该定期同步和更新
|
||||||
|
8. 建议在前端缓存用户禁用状态,避免频繁请求
|
||||||
|
9. 禁用消息提示应该清晰明确,告知用户如何解除禁用
|
||||||
Loading…
x
Reference in New Issue
Block a user