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