commit
6177c7cd10
@ -0,0 +1,13 @@ |
||||
root = true |
||||
|
||||
[*] |
||||
charset = utf-8 |
||||
end_of_line = lf |
||||
trim_trailing_whitespace = true |
||||
insert_final_newline = true |
||||
indent_style = space |
||||
indent_size = 2 |
||||
tab_width = 2 |
||||
|
||||
[*md] |
||||
trim_trailing_whitespace = false |
@ -0,0 +1,2 @@ |
||||
node_modules |
||||
dist |
@ -0,0 +1,13 @@ |
||||
module.exports = { |
||||
root: true, |
||||
env: { |
||||
browser: true, |
||||
es2020: true, |
||||
}, |
||||
extends: [ |
||||
'eslint:recommended', |
||||
'plugin:@typescript-eslint/recommended', |
||||
], |
||||
ignorePatterns: ['dist', '.eslintrc.cjs'], |
||||
parser: '@typescript-eslint/parser', |
||||
} |
@ -0,0 +1,9 @@ |
||||
dist |
||||
.DS_Store |
||||
node_modules |
||||
TODOs.md |
||||
*.log |
||||
*.local |
||||
.idea |
||||
!.vscode/extensions.json |
||||
.vscode |
@ -0,0 +1,11 @@ |
||||
# 设置源 为淘宝源 |
||||
# registry =https://registry.npm.taobao.org |
||||
|
||||
# 默认情况下,PNPM创建了一个半模型,依赖关系可以访问未删除的依赖项, |
||||
# 但在node_modules之外的模块不会。 使用此布局,生态系统中的大多数包都没有问题。 |
||||
# 但是,如果某些工具仅在Node_Modules的根目录中工作时,您可以将其设置为true以为您提升。 |
||||
# shamefully-hoist=true |
||||
|
||||
# 结合根目录的 package.json 中的 engines 字段, |
||||
# 我们可以指定运行的 node 版本和 pnpm 版本。 |
||||
engine-strict=true |
@ -0,0 +1,23 @@ |
||||
module.exports = { |
||||
root: true, |
||||
env: { browser: true, es2020: true }, |
||||
extends: [ |
||||
'eslint:recommended', |
||||
'plugin:@typescript-eslint/recommended', |
||||
'plugin:react-hooks/recommended', |
||||
], |
||||
ignorePatterns: ['dist', '.eslintrc.cjs'], |
||||
parser: '@typescript-eslint/parser', |
||||
plugins: ['react-refresh'], |
||||
rules: { |
||||
// 'react-refresh/only-export-components': [ |
||||
// 'warn', |
||||
// { allowConstantExport: true }, |
||||
// ], |
||||
"react-hooks/exhaustive-deps": [ |
||||
"warn", { |
||||
"additionalHooks": "useRecoilCallback" |
||||
} |
||||
], |
||||
}, |
||||
} |
@ -0,0 +1,27 @@ |
||||
# React + TypeScript + Vite |
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. |
||||
|
||||
Currently, two official plugins are available: |
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh |
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh |
||||
|
||||
## Expanding the ESLint configuration |
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: |
||||
|
||||
- Configure the top-level `parserOptions` property like this: |
||||
|
||||
```js |
||||
parserOptions: { |
||||
ecmaVersion: 'latest', |
||||
sourceType: 'module', |
||||
project: ['./tsconfig.json', './tsconfig.node.json'], |
||||
tsconfigRootDir: __dirname, |
||||
}, |
||||
``` |
||||
|
||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` |
||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` |
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list |
@ -0,0 +1,13 @@ |
||||
<!doctype html> |
||||
<html lang="zh-Hans"> |
||||
<head> |
||||
<meta charset="UTF-8" /> |
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||
<title>后台脚手架</title> |
||||
</head> |
||||
<body> |
||||
<div id="root"></div> |
||||
<script type="module" src="/src/main.tsx"></script> |
||||
</body> |
||||
</html> |
@ -0,0 +1,41 @@ |
||||
{ |
||||
"name": "@doad/example", |
||||
"private": true, |
||||
"version": "0.0.1", |
||||
"type": "module", |
||||
"license": "MIT", |
||||
"description": "a Vite4 + Typescript + React + Antd + Less + Eslint + Prettier template", |
||||
"scripts": { |
||||
"dev": "vite", |
||||
"build": "tsc && vite build", |
||||
"preview": "vite preview" |
||||
}, |
||||
"engines": { |
||||
"node": ">=14", |
||||
"pnpm": ">=7" |
||||
}, |
||||
"dependencies": { |
||||
"@ant-design/icons": "^5.1.4", |
||||
"antd": "^5.7.2", |
||||
"dayjs": "^1.11.9", |
||||
"default-passive-events": "^2.0.0", |
||||
"react": "^18.2.0", |
||||
"react-dom": "^18.2.0", |
||||
"react-router-dom": "^6.14.2", |
||||
"recoil": "^0.7.7", |
||||
"@doad/doad": "workspace:*" |
||||
}, |
||||
"devDependencies": { |
||||
"@types/node": "^20.4.4", |
||||
"@types/react": "^18.2.15", |
||||
"@types/react-dom": "^18.2.7", |
||||
"@typescript-eslint/eslint-plugin": "^6.0.0", |
||||
"@typescript-eslint/parser": "^6.0.0", |
||||
"@vitejs/plugin-react-swc": "^3.3.2", |
||||
"eslint": "^8.45.0", |
||||
"eslint-plugin-react-hooks": "^4.6.0", |
||||
"eslint-plugin-react-refresh": "^0.4.3", |
||||
"typescript": "^5.0.2", |
||||
"vite": "^4.4.5" |
||||
} |
||||
} |
@ -0,0 +1,15 @@ |
||||
import {FC} from "react"; |
||||
import {DoadRoot} from "@doad/doad"; |
||||
import {Outlet} from "react-router-dom"; |
||||
|
||||
const AdminLayout: FC = () => { |
||||
return ( |
||||
<DoadRoot search> |
||||
<Outlet/> |
||||
</DoadRoot> |
||||
) |
||||
} |
||||
|
||||
AdminLayout.displayName = 'AdminLayout' |
||||
|
||||
export default AdminLayout |
@ -0,0 +1,50 @@ |
||||
import {ExceptionStatusType} from "antd/es/result"; |
||||
import {FC} from "react"; |
||||
import {useNavigate, useRouteError} from "react-router-dom"; |
||||
import {Button, Card, Layout, Result} from "antd"; |
||||
import {DoadGeneralLayout, DoadHead, doadHeaderStyle, doadLayoutStyles} from "@doad/doad"; |
||||
|
||||
const errors: Record<ExceptionStatusType, string> = { |
||||
403: '您没有权限访问当前页面', |
||||
404: '您访问的页面不存在', |
||||
500: '程序出错了', |
||||
} |
||||
|
||||
export interface RouteError { |
||||
status?: ExceptionStatusType |
||||
statusText?: string |
||||
message?: string |
||||
} |
||||
|
||||
const ErrorPage: FC = () => { |
||||
const error = useRouteError() as RouteError; |
||||
const navigate = useNavigate() |
||||
|
||||
return ( |
||||
<Layout style={{backgroundColor: 'transparent'}}> |
||||
<Layout.Header style={doadHeaderStyle}> |
||||
<DoadHead search /> |
||||
</Layout.Header> |
||||
<Layout.Content> |
||||
<DoadGeneralLayout> |
||||
<Card style={{...doadLayoutStyles, textAlign: 'center'}}> |
||||
<Result |
||||
status={error.status ?? 500} |
||||
subTitle={errors[error.status!] ?? (error.statusText || error.message)} |
||||
extra={ |
||||
<> |
||||
<Button onClick={() => navigate(-1)}>上一页</Button> |
||||
<Button type="primary" onClick={() => navigate('/')}>返回首页</Button> |
||||
</> |
||||
} |
||||
/> |
||||
</Card> |
||||
</DoadGeneralLayout> |
||||
</Layout.Content> |
||||
</Layout> |
||||
) |
||||
} |
||||
|
||||
ErrorPage.displayName = 'ErrorPage' |
||||
|
||||
export default ErrorPage |
@ -0,0 +1,24 @@ |
||||
html, |
||||
body { |
||||
width: 100%; |
||||
min-height: 100vh; |
||||
} |
||||
|
||||
html { |
||||
box-sizing: border-box; |
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); |
||||
} |
||||
|
||||
body { |
||||
margin: 0; |
||||
min-width: 1200px; |
||||
/*background: #f8f9fb;*/ |
||||
/*background: #f5f8ff;*/ |
||||
background: #f1f1f1; |
||||
} |
||||
|
||||
*, |
||||
*::before, |
||||
*::after { |
||||
box-sizing: inherit; |
||||
} |
@ -0,0 +1,120 @@ |
||||
import { FC, ReactNode, StrictMode, useEffect } from 'react' |
||||
import ReactDOM from 'react-dom/client' |
||||
import dayjs from 'dayjs'; |
||||
import { RecoilRoot } from 'recoil'; |
||||
import { RouterProvider } from 'react-router-dom'; |
||||
import router from './routes/test.tsx'; |
||||
import { |
||||
AppstoreFilled, |
||||
CodeFilled, |
||||
CompassFilled, |
||||
FireFilled, |
||||
HomeFilled, |
||||
NotificationFilled, |
||||
PieChartFilled, |
||||
SettingFilled, |
||||
TrophyFilled |
||||
} from "@ant-design/icons"; |
||||
import { App } from 'antd' |
||||
|
||||
import {DoadMenuItem, useDoadMenuItemsSetter} from "@doad/doad"; |
||||
|
||||
// 关于 Added non-passive event listener to a scroll-blocking ‘wheel’ event. Consider marking event handler as 'passive'
|
||||
// to make the page more responsive. 的新解决方案
|
||||
import 'default-passive-events' |
||||
import 'dayjs/locale/zh-cn'; |
||||
import './index.css' |
||||
|
||||
dayjs.locale('zh-cn'); |
||||
|
||||
let itemKey = 0 |
||||
|
||||
function getItem( |
||||
title: ReactNode, |
||||
icon?: ReactNode, |
||||
children?: DoadMenuItem[], |
||||
path?: string |
||||
): DoadMenuItem { |
||||
let handle: DoadMenuItem['handle'] |
||||
if (path == null) { |
||||
handle = (_, navigate) => navigate("/abc") |
||||
} |
||||
return { |
||||
key: `${++itemKey}`, |
||||
title, |
||||
icon, |
||||
path, |
||||
handle, |
||||
children, |
||||
} |
||||
} |
||||
|
||||
const items: DoadMenuItem[] = [ |
||||
getItem("首页", <HomeFilled/>, [ |
||||
getItem('工作台', null, [], "/about"), |
||||
]), |
||||
getItem('管理', <CompassFilled/>, [ |
||||
getItem('版本管理'), |
||||
getItem('成员管理'), |
||||
getItem('用户反馈'), |
||||
getItem('付费管理'), |
||||
]), |
||||
getItem("统计", <PieChartFilled/>), |
||||
getItem('功能', <AppstoreFilled/>, [ |
||||
getItem('附近的小程序'), |
||||
getItem('微信搜一搜'), |
||||
getItem('微信支付'), |
||||
getItem('购物订单'), |
||||
getItem('交易保障'), |
||||
getItem('体验评价'), |
||||
getItem('购物订单'), |
||||
getItem('物流服务'), |
||||
getItem('硬件设备'), |
||||
getItem('客服'), |
||||
getItem('订阅消息'), |
||||
getItem('页面内容接入'), |
||||
getItem('小程序插件'), |
||||
getItem('交易组件'), |
||||
getItem('实验工具'), |
||||
]), |
||||
|
||||
getItem('开发', <CodeFilled/>, [ |
||||
getItem('开发管理'), |
||||
getItem('开发工具'), |
||||
getItem('云服务'), |
||||
]), |
||||
|
||||
getItem('成长', <TrophyFilled/>, [ |
||||
getItem('小程序评测'), |
||||
getItem('违规记录'), |
||||
]), |
||||
|
||||
getItem('推广', <NotificationFilled/>, [ |
||||
getItem('流量主'), |
||||
getItem('广告主'), |
||||
]), |
||||
|
||||
getItem('第三方服务', <FireFilled/>, [ |
||||
getItem('服务'), |
||||
]), |
||||
|
||||
getItem('设置', <SettingFilled/>), |
||||
]; |
||||
|
||||
const Application: FC = () => { |
||||
const setMenus = useDoadMenuItemsSetter() |
||||
useEffect(() => setMenus(items)) |
||||
return ( |
||||
<App> |
||||
<RouterProvider router={router} /> |
||||
</App> |
||||
) |
||||
} |
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render( |
||||
<StrictMode> |
||||
<RecoilRoot> |
||||
<Application /> |
||||
</RecoilRoot> |
||||
</StrictMode>, |
||||
) |
@ -0,0 +1,50 @@ |
||||
import {Card} from "antd"; |
||||
import {createBrowserRouter} from "react-router-dom"; |
||||
import {DoadLazy, DoadTabbedLayout, DoadTabbedLayoutProps} from "@doad/doad"; |
||||
import AdminLayout from "@/components/layouts/AdminLayout"; |
||||
import ErrorPage from "@/components/layouts/ErrorPage"; |
||||
|
||||
const getTabItem = (label: string, key: string) => { |
||||
return { |
||||
key, |
||||
label, |
||||
render: () => <DoadLazy>{() => { |
||||
return new Promise(resolve => { |
||||
setTimeout(() => { |
||||
resolve({ |
||||
default: () => <Card>{label}</Card> |
||||
}) |
||||
}, 2000) |
||||
}) |
||||
}}</DoadLazy>, |
||||
} |
||||
} |
||||
|
||||
const tabsItems: DoadTabbedLayoutProps['items'] = [ |
||||
getTabItem('功能异常', '1'), |
||||
getTabItem('产品建议', '2'), |
||||
getTabItem('监控告警', '3'), |
||||
getTabItem('垃圾箱', '4'), |
||||
]; |
||||
|
||||
const router = createBrowserRouter([ |
||||
{ |
||||
path: '/', |
||||
element: <AdminLayout/>, |
||||
errorElement: <ErrorPage/>, |
||||
children: [ |
||||
{ |
||||
path: "about", |
||||
element: ( |
||||
<DoadTabbedLayout |
||||
title="用户反馈" |
||||
items={tabsItems} |
||||
activeKey="1" |
||||
/> |
||||
) |
||||
}, |
||||
] |
||||
} |
||||
]) |
||||
|
||||
export default router |
@ -0,0 +1,50 @@ |
||||
declare namespace API { |
||||
/** 请求参数结构 */ |
||||
type Request< |
||||
Query = Record<string, unknown> | string, |
||||
Body = unknown, |
||||
Files = Record<string, Blob | File | string> |
||||
> = { |
||||
/** 请求的目标地址 */ |
||||
url: string |
||||
/** HTTP 请求方法,我们可以按照下面列举的方式理解其用法和意义: |
||||
* |
||||
* - GET(SELECT):从服务器取出资源(一项或多项)。 |
||||
* - POST(CREATE):在服务器新建一个资源。 |
||||
* - PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。 |
||||
* - PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。 |
||||
* - DELETE(DELETE):从服务器删除资源。 */ |
||||
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' |
||||
/** 搜索参数,对应 URL 的 QueryString 部分,用于对返回结构的过滤, |
||||
* 比如分页参数、排序字段、排序方式及其它。*/ |
||||
query?: Query |
||||
/** 请求体数据 */ |
||||
body?: Body |
||||
/** 上传的文件 */ |
||||
files?: Files |
||||
} |
||||
|
||||
/** 响应数据结构 */ |
||||
type Response<T = unknown> = { |
||||
/** 业务上的请求是否成功 */ |
||||
success: boolean |
||||
/** 业务约定的错误码 */ |
||||
code: string |
||||
/** 业务上的错误信息 */ |
||||
message?: string |
||||
/** 业务数据 */ |
||||
data?: T |
||||
} |
||||
|
||||
/** 分页数据结构 */ |
||||
type Pagination<T = unknown> = { |
||||
/** 数据总数 */ |
||||
total: number |
||||
/** 当前页码 */ |
||||
page: number |
||||
/** 页面数据容量 */ |
||||
per_page: number |
||||
/** 数据列表 */ |
||||
list: T[] |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
/// <reference types="vite/client" />
|
@ -0,0 +1,29 @@ |
||||
{ |
||||
"compilerOptions": { |
||||
"target": "ES2020", |
||||
"useDefineForClassFields": true, |
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"], |
||||
"module": "ESNext", |
||||
"skipLibCheck": true, |
||||
|
||||
/* Bundler mode */ |
||||
"moduleResolution": "bundler", |
||||
"allowImportingTsExtensions": true, |
||||
"resolveJsonModule": true, |
||||
"isolatedModules": true, |
||||
"noEmit": true, |
||||
"jsx": "react-jsx", |
||||
"baseUrl": ".", |
||||
"paths": { |
||||
"@/*": ["src/*"] |
||||
}, |
||||
|
||||
/* Linting */ |
||||
"strict": true, |
||||
"noUnusedLocals": true, |
||||
"noUnusedParameters": true, |
||||
"noFallthroughCasesInSwitch": true |
||||
}, |
||||
"include": ["src"], |
||||
"references": [{ "path": "./tsconfig.node.json" }] |
||||
} |
@ -0,0 +1,10 @@ |
||||
{ |
||||
"compilerOptions": { |
||||
"composite": true, |
||||
"skipLibCheck": true, |
||||
"module": "ESNext", |
||||
"moduleResolution": "bundler", |
||||
"allowSyntheticDefaultImports": true |
||||
}, |
||||
"include": ["vite.config.ts"] |
||||
} |
@ -0,0 +1,13 @@ |
||||
import { resolve } from 'node:path' |
||||
import { defineConfig } from 'vite' |
||||
import react from '@vitejs/plugin-react-swc' |
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({ |
||||
plugins: [react()], |
||||
resolve: { |
||||
alias: [ |
||||
{find: '@', replacement: resolve(__dirname, "src")} |
||||
] |
||||
} |
||||
}) |
@ -0,0 +1,34 @@ |
||||
{ |
||||
"private": true, |
||||
"version": "1.0.0", |
||||
"packageManager": "pnpm@8.6.2", |
||||
"type": "module", |
||||
"scripts": { |
||||
"check": "tsc --incremental --noEmit", |
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" |
||||
}, |
||||
"engines": { |
||||
"node": ">=16.11.0" |
||||
}, |
||||
"simple-git-hooks": { |
||||
"pre-commit": "pnpm lint-staged && pnpm check", |
||||
"commit-msg": "node scripts/verifyCommit.js" |
||||
}, |
||||
"lint-staged": { |
||||
"*.{json,ts?(x)}": [ |
||||
"eslint --fix" |
||||
] |
||||
}, |
||||
"devDependencies": { |
||||
"@types/node": "^20.4.4", |
||||
"@typescript-eslint/eslint-plugin": "^6.0.0", |
||||
"@typescript-eslint/parser": "^6.0.0", |
||||
"chalk": "^5.3.0", |
||||
"eslint": "^8.45.0", |
||||
"lint-staged": "^13.2.3", |
||||
"simple-git-hooks": "^2.9.0", |
||||
"typescript": "^5.0.2", |
||||
"vite": "^4.4.5", |
||||
"vitest": "^0.33.0" |
||||
} |
||||
} |
@ -0,0 +1,22 @@ |
||||
module.exports = { |
||||
env: { browser: true, es2020: true }, |
||||
extends: [ |
||||
'eslint:recommended', |
||||
'plugin:@typescript-eslint/recommended', |
||||
'plugin:react-hooks/recommended', |
||||
], |
||||
ignorePatterns: ['dist', '.eslintrc.cjs'], |
||||
parser: '@typescript-eslint/parser', |
||||
plugins: ['react-refresh'], |
||||
rules: { |
||||
'react-refresh/only-export-components': [ |
||||
'warn', |
||||
{ allowConstantExport: true }, |
||||
], |
||||
"react-hooks/exhaustive-deps": [ |
||||
"warn", { |
||||
"additionalHooks": "useRecoilCallback" |
||||
} |
||||
], |
||||
}, |
||||
} |
@ -0,0 +1,41 @@ |
||||
{ |
||||
"name": "@doad/doad", |
||||
"version": "0.0.1", |
||||
"type": "module", |
||||
"license": "MIT", |
||||
"description": "Dashboard of ant design", |
||||
"main": "src/index.ts", |
||||
"scripts": { |
||||
"dev": "vite", |
||||
"build": "tsc && vite build", |
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", |
||||
"preview": "vite preview" |
||||
}, |
||||
"engines": { |
||||
"node": ">=14", |
||||
"pnpm": ">=7" |
||||
}, |
||||
"dependencies": { |
||||
"@ant-design/icons": "^5.1.4", |
||||
"antd": "^5.7.2", |
||||
"dayjs": "^1.11.9", |
||||
"default-passive-events": "^2.0.0", |
||||
"react": "^18.2.0", |
||||
"react-dom": "^18.2.0", |
||||
"react-router-dom": "^6.14.2", |
||||
"recoil": "^0.7.7" |
||||
}, |
||||
"devDependencies": { |
||||
"@types/node": "^20.4.4", |
||||
"@types/react": "^18.2.15", |
||||
"@types/react-dom": "^18.2.7", |
||||
"@typescript-eslint/eslint-plugin": "^6.0.0", |
||||
"@typescript-eslint/parser": "^6.0.0", |
||||
"@vitejs/plugin-react-swc": "^3.3.2", |
||||
"eslint": "^8.45.0", |
||||
"eslint-plugin-react-hooks": "^4.6.0", |
||||
"eslint-plugin-react-refresh": "^0.4.3", |
||||
"typescript": "^5.0.2", |
||||
"vite": "^4.4.5" |
||||
} |
||||
} |
@ -0,0 +1,123 @@ |
||||
import {ComponentClass, FunctionComponent} from "react"; |
||||
import {useDoadPermissionValue} from "../state"; |
||||
import {getDisplayName, toArray} from "../utils"; |
||||
import {Link} from "react-router-dom"; |
||||
import { |
||||
Alert, |
||||
AutoComplete, |
||||
Avatar, |
||||
Badge, |
||||
Button, |
||||
Calendar, |
||||
Card, |
||||
Carousel, |
||||
Cascader, |
||||
Checkbox, |
||||
Collapse, |
||||
ColorPicker, |
||||
DatePicker, |
||||
Descriptions, |
||||
Drawer, |
||||
Dropdown, |
||||
FloatButton, |
||||
Form, |
||||
Image, |
||||
Input, |
||||
InputNumber, |
||||
List, |
||||
Mentions, |
||||
Modal, |
||||
Popconfirm, |
||||
Popover, |
||||
Progress, |
||||
QRCode, |
||||
Radio, |
||||
Rate, |
||||
Segmented, |
||||
Select, |
||||
Slider, |
||||
Statistic, |
||||
Switch, |
||||
Table, |
||||
Tabs, |
||||
Tag, |
||||
Timeline, |
||||
TimePicker, |
||||
Tooltip, |
||||
Tour, |
||||
Transfer, |
||||
Tree, |
||||
TreeSelect, |
||||
Upload |
||||
} from "antd"; |
||||
|
||||
export type AuthorityComponent<P> = FunctionComponent<P & { |
||||
permissions?: Array<string | number> | string | number |
||||
}> |
||||
|
||||
export function withAuthority<P>(BaseComponent: FunctionComponent<P> | ComponentClass<P> | string): AuthorityComponent<P> { |
||||
return function (props) { |
||||
const permissions = useDoadPermissionValue() |
||||
const allows = toArray(props.permissions) |
||||
if (allows == null || permissions.some(x => allows.includes(x.key))) { |
||||
const attributes = {...props} |
||||
delete attributes.permissions |
||||
return <BaseComponent {...attributes} /> |
||||
} |
||||
return null |
||||
} |
||||
} |
||||
|
||||
function wrap<P>(BaseComponent: FunctionComponent<P> | ComponentClass<P> | string): AuthorityComponent<P> { |
||||
const Comp = withAuthority<P>(BaseComponent) |
||||
Comp.displayName = `Doad${getDisplayName(BaseComponent)}` |
||||
return Comp |
||||
} |
||||
|
||||
export const DoadLink = wrap(Link) |
||||
export const DoadButton = wrap(Button) |
||||
export const DoadDropdown = wrap(Dropdown) |
||||
export const DoadAutoComplete = wrap(AutoComplete) |
||||
export const DoadCascader = wrap(Cascader) |
||||
export const DoadCheckbox = wrap(Checkbox) |
||||
export const DoadColorPicker = wrap(ColorPicker) |
||||
export const DoadDatePicker = wrap(DatePicker) |
||||
export const DoadForm = wrap(Form) |
||||
export const DoadInput = wrap(Input) |
||||
export const DoadInputNumber = wrap(InputNumber) |
||||
export const DoadMentions = wrap(Mentions) |
||||
export const DoadRadio = wrap(Radio) |
||||
export const DoadRate = wrap(Rate) |
||||
export const DoadSelect = wrap(Select) |
||||
export const DoadSlider = wrap(Slider) |
||||
export const DoadSwitch = wrap(Switch) |
||||
export const DoadTimePicker = wrap(TimePicker) |
||||
export const DoadTransfer = wrap(Transfer) |
||||
export const DoadTreeSelect = wrap(TreeSelect) |
||||
export const DoadUpload = wrap(Upload) |
||||
export const DoadAvatar = wrap(Avatar) |
||||
export const DoadBadge = wrap(Badge) |
||||
export const DoadCalendar = wrap(Calendar) |
||||
export const DoadCard = wrap(Card) |
||||
export const DoadCarousel = wrap(Carousel) |
||||
export const DoadCollapse = wrap(Collapse) |
||||
export const DoadDescriptions = wrap(Descriptions) |
||||
export const DoadImage = wrap(Image) |
||||
export const DoadList = wrap(List) |
||||
export const DoadPopover = wrap(Popover) |
||||
export const DoadQRCode = wrap(QRCode) |
||||
export const DoadSegmented = wrap(Segmented) |
||||
export const DoadStatistic = wrap(Statistic) |
||||
export const DoadTable = wrap(Table) |
||||
export const DoadTabs = wrap(Tabs) |
||||
export const DoadTag = wrap(Tag) |
||||
export const DoadTimeline = wrap(Timeline) |
||||
export const DoadTooltip = wrap(Tooltip) |
||||
export const DoadTour = wrap(Tour) |
||||
export const DoadTree = wrap(Tree) |
||||
export const DoadAlert = wrap(Alert) |
||||
export const DoadDrawer = wrap(Drawer) |
||||
export const DoadModal = wrap(Modal) |
||||
export const DoadPopconfirm = wrap(Popconfirm) |
||||
export const DoadProgress = wrap(Progress) |
||||
export const DoadFloatButton = wrap(FloatButton) |
@ -0,0 +1,39 @@ |
||||
import {FC, ReactNode} from "react"; |
||||
import {DoadSearch, DoadSearchProps} from "./search"; |
||||
import {Avatar, Col, Row} from "antd"; |
||||
import {BellOutlined} from "@ant-design/icons"; |
||||
import {DoadLogo} from './logo' |
||||
|
||||
export interface DoadHeadProps { |
||||
search?: DoadSearchProps | boolean |
||||
logo?: ReactNode |
||||
} |
||||
|
||||
export const DoadHead: FC<DoadHeadProps> = (props) => { |
||||
const searchProps = props.search === true ? {} : props.search |
||||
return ( |
||||
<Row wrap={false} align='middle'> |
||||
<Col flex="187px" style={{ |
||||
alignItems: 'center', |
||||
width: '100%', |
||||
justifyContent: 'start', |
||||
display: 'flex', |
||||
gap: '12px', |
||||
fontSize: '24px', |
||||
}}> |
||||
<DoadLogo element={props.logo}/> |
||||
</Col> |
||||
<Col flex="1px" style={{borderInlineEnd: '1px solid rgba(5, 5, 5, 0.06)', height: '22px'}}/> |
||||
<Col flex="none"> |
||||
{searchProps && <DoadSearch {...searchProps} />} |
||||
</Col> |
||||
<Col flex="auto"/> |
||||
<Col flex="none" style={{display: "flex", alignItems: 'center', gap: '40px'}}> |
||||
<BellOutlined style={{fontSize: '22px', opacity: '0.65'}}/> |
||||
<Avatar src="https://wx.qlogo.cn/mmhead/Q3auHgzwzM7nCvm721MTlkPGQuLWEYYYkV6ibpDI9zcSlkBIbib2Emyw/0"/> |
||||
</Col> |
||||
</Row> |
||||
) |
||||
} |
||||
|
||||
DoadHead.displayName = 'DoadHead' |
@ -0,0 +1,7 @@ |
||||
.acsRLTLine .ant-tabs-nav::before { |
||||
border-bottom-color: #e4e8eb; |
||||
} |
||||
|
||||
.acsRLTLine .ant-tabs-nav .ant-tabs-ink-bar { |
||||
height: 3px; |
||||
} |
@ -0,0 +1,87 @@ |
||||
import {CSSProperties, FC, ReactNode, useState} from "react"; |
||||
import {Tabs} from "antd"; |
||||
import {DoadTitle} from './title' |
||||
import {mergeStyles} from "../utils"; |
||||
import './layout.css' |
||||
|
||||
export const doadLayoutStyles: CSSProperties = { |
||||
maxWidth: '1280px', |
||||
width: '85%', |
||||
margin: '40px auto', |
||||
} |
||||
|
||||
export interface DoadGeneralLayoutProps { |
||||
title?: ReactNode |
||||
tailing?: ReactNode |
||||
style?: CSSProperties |
||||
children?: ReactNode |
||||
} |
||||
|
||||
export const DoadGeneralLayout: FC<DoadGeneralLayoutProps> = (props) => { |
||||
return ( |
||||
<div style={mergeStyles(doadLayoutStyles, props.style)}> |
||||
<DoadTitle title={props.title} tailing={props.tailing} /> |
||||
{props.children} |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
DoadGeneralLayout.displayName = 'DoadGeneralLayout' |
||||
|
||||
export interface DoadTabbedLayoutTab { |
||||
key: string |
||||
label: string |
||||
disabled?: boolean |
||||
render?: (ready: boolean) => ReactNode |
||||
} |
||||
|
||||
export interface DoadTabbedLayoutProps { |
||||
title?: ReactNode |
||||
titleTailing?: ReactNode |
||||
subtitle?: ReactNode |
||||
style?: CSSProperties |
||||
activeKey?: string, |
||||
lazy?: boolean |
||||
animated?: boolean |
||||
items: DoadTabbedLayoutTab[] |
||||
} |
||||
|
||||
export const DoadTabbedLayout: FC<DoadTabbedLayoutProps> = (props) => { |
||||
const activeKey = props.activeKey ?? props.items[0]?.key ?? '' |
||||
const [rendered, setRendered] = useState<string[]>([activeKey]) |
||||
const tabs = props.items.map((tab) => { |
||||
return { |
||||
key: tab.key, |
||||
label: <span style={{fontSize: '16px'}}>{tab.label}</span>, |
||||
disabled: tab.disabled, |
||||
children: tab.render?.(props.lazy ? rendered.includes(tab.key) : true), |
||||
} |
||||
}) |
||||
const handleTabChange = (activeKey: string) => { |
||||
if (props.lazy && !rendered.includes(activeKey)) { |
||||
setRendered((keys) => [...keys, activeKey]) |
||||
} |
||||
} |
||||
|
||||
// fixme 为什么会被渲染2次
|
||||
|
||||
return ( |
||||
<DoadGeneralLayout |
||||
title={props.title} |
||||
tailing={props.titleTailing} |
||||
style={props.style} |
||||
> |
||||
{props.subtitle} |
||||
<Tabs |
||||
defaultActiveKey={props.activeKey} |
||||
className="acsRLTLine" |
||||
items={tabs} |
||||
size={'small'} |
||||
animated={props.animated} |
||||
onChange={handleTabChange} |
||||
/> |
||||
</DoadGeneralLayout> |
||||
) |
||||
} |
||||
|
||||
DoadTabbedLayout.displayName = 'DoadTabbedLayout' |
@ -0,0 +1,29 @@ |
||||
import {FC, ReactNode, Suspense, createElement, ComponentType, lazy} from "react"; |
||||
import {Space, Spin} from "antd"; |
||||
|
||||
export type LazyFactory<T> = () => Promise<{ default: T }> |
||||
|
||||
export interface DoadLazyProps { |
||||
children?: LazyFactory<ComponentType<unknown>> |
||||
loading?: ReactNode |
||||
} |
||||
|
||||
const defaultLoading = ( |
||||
<Space align="center" style={{lineHeight: 1, padding: '24px 0'}}> |
||||
<Spin size="small"/> |
||||
<span>加载中...</span> |
||||
</Space> |
||||
) |
||||
|
||||
export const DoadLazy: FC<DoadLazyProps> = (props) => { |
||||
if (props.children == null) { |
||||
return null |
||||
} |
||||
return ( |
||||
<Suspense fallback={props.loading ?? defaultLoading}> |
||||
{createElement(lazy(props.children))} |
||||
</Suspense> |
||||
) |
||||
} |
||||
|
||||
DoadLazy.displayName = 'DoadLazy' |
@ -0,0 +1,36 @@ |
||||
import {CSSProperties, FC, ReactNode} from 'react'; |
||||
import {mergeStyles} from '../utils'; |
||||
|
||||
export const doadLogoStyle: CSSProperties = { |
||||
display: 'flex', |
||||
justifyContent: 'center', |
||||
alignItems: 'center', |
||||
flexWrap: 'nowrap', |
||||
} |
||||
|
||||
export const defaultLogoElement = ( |
||||
<div style={{display: 'flex', alignItems: 'center', gap: '8px'}}> |
||||
<img |
||||
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg" |
||||
height="32" |
||||
width="32" |
||||
alt="logo" |
||||
/> |
||||
<span>Ant Design</span> |
||||
</div> |
||||
) |
||||
|
||||
export interface DoadLogoProps { |
||||
style?: CSSProperties |
||||
element?: ReactNode |
||||
} |
||||
|
||||
export const DoadLogo: FC<DoadLogoProps> = (props) => { |
||||
return ( |
||||
<div style={mergeStyles(doadLogoStyle, props.style)}> |
||||
{props.element ?? defaultLogoElement} |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
DoadLogo.displayName = 'DoadLogo' |
@ -0,0 +1,114 @@ |
||||
import {FC} from "react"; |
||||
import {useNavigate} from "react-router-dom"; |
||||
import {Menu, MenuProps, Space} from "antd"; |
||||
import { useDoadActiveMenuValue, useDoadMenuItemsValue } from "../state"; |
||||
import {DoadMenuItem} from "../types"; |
||||
|
||||
export const doadMenuWidth = 228 |
||||
|
||||
type MenuItem = Required<MenuProps>['items'][number]; |
||||
|
||||
function getMenuItem(items: DoadMenuItem[], key: string): DoadMenuItem | null { |
||||
for (const item of items) { |
||||
if (item.key === key) { |
||||
return item |
||||
} |
||||
if (item.children != null) { |
||||
const sub = getMenuItem(item.children, key) |
||||
if (sub != null) { |
||||
return sub |
||||
} |
||||
} |
||||
} |
||||
return null |
||||
} |
||||
|
||||
function toRawMenuItems(items: DoadMenuItem[], deep = 0): MenuItem[] { |
||||
return items.reduce((list, item, i) => { |
||||
let label = item.title |
||||
if (deep === 0 && item.icon != null) { |
||||
label = ( |
||||
<Space style={{color: 'rgba(0,0,0,.88)', fontSize: '18px', width: '100%'}} size='middle'> |
||||
<span style={{opacity: 0.6}}>{item.icon}</span> |
||||
<span>{item.title}</span> |
||||
</Space> |
||||
) |
||||
} |
||||
let type: 'group' | undefined |
||||
if (deep == 0) { |
||||
type = 'group' |
||||
} |
||||
let children: MenuItem[] | undefined |
||||
if (item.children?.length) { |
||||
children = toRawMenuItems(item.children, deep + 1) |
||||
} else if (deep == 0) { |
||||
children = toRawMenuItems([item], deep + 1) |
||||
} |
||||
if (deep == 0 && i !== items.length - 1) { |
||||
children ??= [] |
||||
children.push( { |
||||
type: "divider", |
||||
style: { |
||||
width: '43%', |
||||
margin: '12px 0 12px 50px', |
||||
} |
||||
}) |
||||
} |
||||
|
||||
list.push({ |
||||
key: item.key, |
||||
label, |
||||
type, |
||||
children, |
||||
style: { |
||||
padding: type ? "0 18px" : "0 18px 0 46px", |
||||
textAlign: "left", |
||||
} |
||||
}) |
||||
|
||||
return list |
||||
}, [] as MenuItem[]) |
||||
} |
||||
|
||||
export interface DoadMenuProps { |
||||
onClick?: (item: DoadMenuItem, handled: boolean) => void |
||||
} |
||||
|
||||
export const DoadMenu: FC<DoadMenuProps> = (props) => { |
||||
const menuItems = useDoadMenuItemsValue() |
||||
const activeMenuKey = useDoadActiveMenuValue() |
||||
const navigate = useNavigate(); |
||||
|
||||
const handleClick: MenuProps['onClick'] = (e) => { |
||||
const item = getMenuItem(menuItems, e.key) |
||||
let handled = true |
||||
if (item?.handle != null) { |
||||
item.handle(item, navigate) |
||||
} else if (item?.path != null) { |
||||
navigate(item.path) |
||||
} else { |
||||
handled = false |
||||
} |
||||
if (item != null) { |
||||
props.onClick?.(item, handled) |
||||
} |
||||
} |
||||
|
||||
return ( |
||||
<Menu |
||||
selectedKeys={[activeMenuKey].filter(Boolean) as string[]} |
||||
style={{ |
||||
width: doadMenuWidth, |
||||
margin: '40px 0', |
||||
userSelect: 'none', |
||||
backgroundColor: 'transparent', |
||||
}} |
||||
mode="inline" |
||||
inlineIndent={0} |
||||
items={toRawMenuItems(menuItems)} |
||||
onClick={handleClick} |
||||
/> |
||||
) |
||||
} |
||||
|
||||
DoadMenu.displayName = 'DoadMenu' |
@ -0,0 +1,48 @@ |
||||
import {CSSProperties, FC, ReactNode} from "react"; |
||||
import {Layout} from "antd"; |
||||
import {DoadSearchProps} from "./search"; |
||||
import {DoadMenu, DoadMenuProps, doadMenuWidth} from "./menu"; |
||||
import {DoadHead} from "./head"; |
||||
|
||||
export const doadHeaderStyle: CSSProperties = { |
||||
position: 'sticky', |
||||
top: 0, |
||||
zIndex: 3, |
||||
textAlign: 'center', |
||||
height: 64, |
||||
paddingInline: 40, |
||||
lineHeight: '64px', |
||||
backgroundColor: '#ffffff', |
||||
boxShadow: '0 1px 2px rgba(150, 150, 150, 0.3)', |
||||
} |
||||
|
||||
export interface DoadRootProps { |
||||
logo?: ReactNode |
||||
footer?: ReactNode |
||||
children?: ReactNode |
||||
search?: DoadSearchProps | boolean |
||||
onMenuClick?: DoadMenuProps['onClick'] |
||||
} |
||||
|
||||
export const DoadRoot: FC<DoadRootProps> = (props) => { |
||||
return ( |
||||
<Layout style={{backgroundColor: 'transparent'}}> |
||||
<Layout.Header style={{...doadHeaderStyle}}> |
||||
<DoadHead search={props.search} logo={props.logo}/> |
||||
</Layout.Header> |
||||
<Layout style={{backgroundColor: 'transparent'}}> |
||||
<Layout.Sider style={{backgroundColor: 'transparent'}} width={doadMenuWidth}> |
||||
<DoadMenu onClick={props.onMenuClick}/> |
||||
</Layout.Sider> |
||||
<Layout.Content> |
||||
{props.children} |
||||
</Layout.Content> |
||||
</Layout> |
||||
{props.footer && <Layout.Footer> |
||||
{props.footer} |
||||
</Layout.Footer>} |
||||
</Layout> |
||||
) |
||||
} |
||||
|
||||
DoadRoot.displayName = 'DoadRoot' |
@ -0,0 +1,36 @@ |
||||
import {CSSProperties, FC, ReactNode} from "react"; |
||||
import {Input} from "antd"; |
||||
import {SearchOutlined} from "@ant-design/icons"; |
||||
import {mergeStyles} from "../utils"; |
||||
|
||||
export const doadSearchShortcutStyle: CSSProperties = { |
||||
color: '#ced4d9', |
||||
backgroundColor: 'rgba(150, 150, 150, 0.06)', |
||||
border: '1px solid rgba(100, 100, 100, 0.2)', |
||||
borderRadius: '4px', |
||||
padding: '4px 8px', |
||||
fontSize: '12px', |
||||
lineHeight: 1, |
||||
} |
||||
|
||||
export interface DoadSearchProps { |
||||
placeholder?: string |
||||
shortcut?: ReactNode |
||||
bordered?: boolean |
||||
style?: CSSProperties |
||||
shortcutStyle?: CSSProperties |
||||
} |
||||
|
||||
export const DoadSearch: FC<DoadSearchProps> = (props) => { |
||||
return ( |
||||
<Input |
||||
style={props.style} |
||||
prefix={<SearchOutlined/>} |
||||
suffix={props.shortcut ?? <span style={mergeStyles(doadSearchShortcutStyle, props.shortcutStyle)}>Ctrl+K</span>} |
||||
placeholder={props.placeholder ?? '请输入关键字搜索'} |
||||
bordered={props.bordered ?? false} |
||||
/> |
||||
) |
||||
} |
||||
|
||||
DoadSearch.displayName = 'DoadSearch' |
@ -0,0 +1,47 @@ |
||||
import {FC, ReactNode} from "react"; |
||||
import {Col, Row} from "antd"; |
||||
|
||||
export interface DoadTitleProps { |
||||
title?: ReactNode |
||||
tailing?: ReactNode |
||||
} |
||||
|
||||
export const DoadTitle: FC<DoadTitleProps> = (props) => { |
||||
if (props.title == null) { |
||||
return null |
||||
} |
||||
if (props.tailing == null) { |
||||
return ( |
||||
<h2 style={{ |
||||
fontSize: '26px', |
||||
lineHeight: '36px', |
||||
marginBottom: '12px', |
||||
fontWeight: 400, |
||||
fontStyle: 'normal', |
||||
}}>{props.title}</h2> |
||||
) |
||||
} |
||||
return ( |
||||
<Row |
||||
style={{marginBottom: '12px'}} |
||||
justify="space-between" |
||||
align="middle" |
||||
gutter={36} |
||||
wrap={false} |
||||
> |
||||
<Col flex='none'> |
||||
<h2 style={{ |
||||
fontSize: '26px', |
||||
lineHeight: '36px', |
||||
fontWeight: 400, |
||||
fontStyle: 'normal', |
||||
}}>{props.title}</h2> |
||||
</Col> |
||||
<Col flex='none'> |
||||
{props.tailing} |
||||
</Col> |
||||
</Row> |
||||
) |
||||
} |
||||
|
||||
DoadTitle.displayName = 'DoadTitle' |
@ -0,0 +1,10 @@ |
||||
export * from './components/access' |
||||
export * from './components/head' |
||||
export * from './components/layout' |
||||
export * from './components/lazy' |
||||
export * from './components/logo' |
||||
export * from './components/menu' |
||||
export * from './components/root' |
||||
export * from './components/search' |
||||
export * from './state' |
||||
export * from './types' |
@ -0,0 +1,59 @@ |
||||
import { |
||||
DefaultValue, |
||||
atom, |
||||
selector, |
||||
useRecoilState, |
||||
useRecoilValue, |
||||
useSetRecoilState |
||||
} from "recoil"; |
||||
import {DoadProfile} from "./types"; |
||||
|
||||
export const radProfile = atom<DoadProfile>({ |
||||
key: 'acsProfile', |
||||
default: { |
||||
menuItems: [], |
||||
routes: [], |
||||
permissions: [], |
||||
} |
||||
}) |
||||
|
||||
function createSelector<K extends keyof DoadProfile>( |
||||
key: string, |
||||
field: K, |
||||
defValue?: () => DoadProfile[K], |
||||
) { |
||||
return selector<DoadProfile[K]>({ |
||||
key, |
||||
get({get}) { |
||||
return get(radProfile)[field] |
||||
}, |
||||
set({set}, newValue) { |
||||
set(radProfile, v => ({ |
||||
...v, |
||||
[field]: newValue instanceof DefaultValue ? defValue?.() : newValue, |
||||
})) |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
const activeMenuKeyState = createSelector('doadActiveMenuKeyState', 'activeMenuKey') |
||||
const menuItemsState = createSelector('doadMenuItemsState', 'menuItems', () => []) |
||||
const userState = createSelector('doadUserState', 'user') |
||||
const permissionsState = createSelector('doadPermissionsState', 'permissions', () => []) |
||||
const routesState = createSelector('doadRoutesState', 'routes', () => []) |
||||
|
||||
export const useDoadActiveMenuState = () => useRecoilState(activeMenuKeyState) |
||||
export const useDoadActiveMenuSetter = () => useSetRecoilState(activeMenuKeyState) |
||||
export const useDoadActiveMenuValue = () => useRecoilValue(activeMenuKeyState) |
||||
export const useDoadMenuItemsState = () => useRecoilState(menuItemsState) |
||||
export const useDoadMenuItemsSetter = () => useSetRecoilState(menuItemsState) |
||||
export const useDoadMenuItemsValue = () => useRecoilValue(menuItemsState) |
||||
export const useDoadUserState = () => useRecoilState(userState) |
||||
export const useDoadUserSetter = () => useSetRecoilState(userState) |
||||
export const useDoadUserValue = () => useRecoilValue(userState) |
||||
export const useDoadPermissionState = () => useRecoilState(permissionsState) |
||||
export const useDoadPermissionSetter = () => useSetRecoilState(permissionsState) |
||||
export const useDoadPermissionValue = () => useRecoilValue(permissionsState) |
||||
export const useDoadRoutesState = () => useRecoilState(routesState) |
||||
export const useDoadRoutesSetter = () => useSetRecoilState(routesState) |
||||
export const useDoadRoutesValue = () => useRecoilValue(routesState) |
@ -0,0 +1,57 @@ |
||||
/** 布局菜单选项 */ |
||||
export interface DoadMenuItem { |
||||
/** 菜单唯一键 */ |
||||
key: string |
||||
/** 菜单标题 */ |
||||
title: import('react').ReactNode |
||||
/** 菜单图标 */ |
||||
icon?: import('react').ReactNode |
||||
/** 菜单跳转路径 */ |
||||
path?: string |
||||
/** 子菜单 */ |
||||
children?: DoadMenuItem[] |
||||
/** 自定义点击函数,优先级高于 {@link DoadMenuItem.path} */ |
||||
handle?: (item: DoadMenuItem, navigate: import('react-router-dom').NavigateFunction) => void |
||||
/** 其它自定义数据 */ |
||||
[key: string]: unknown |
||||
} |
||||
|
||||
/** 登录用户信息 */ |
||||
export interface DoadUser { |
||||
/** 用户昵称 */ |
||||
nickname: string |
||||
/** 用户头像 */ |
||||
avatar?: string |
||||
/** 其它自定义数据 */ |
||||
[key: string]: unknown |
||||
} |
||||
|
||||
/** 权限 */ |
||||
export interface DoadPermission { |
||||
/** 权限唯一键 */ |
||||
key: string | number |
||||
/** 权限名称 */ |
||||
title?: string |
||||
/** 其它自定义数据 */ |
||||
[key: string]: unknown |
||||
} |
||||
|
||||
|
||||
/** 当前用户环境 */ |
||||
export interface DoadProfile { |
||||
/** 登录的用户信息 */ |
||||
user?: DoadUser |
||||
/** 有效的菜单 */ |
||||
menuItems: DoadMenuItem[] |
||||
/** 有效的路由 */ |
||||
routes: string[] |
||||
/** 被允许的权限 */ |
||||
permissions: DoadPermission[] |
||||
/** 当前菜单键 */ |
||||
activeMenuKey?: string |
||||
} |
||||
|
||||
export type DoadLayoutStatus = |
||||
| 'loading' |
||||
| 'success' |
||||
| 'error' |
@ -0,0 +1,12 @@ |
||||
import {ComponentType} from "react"; |
||||
|
||||
export function toArray<T>(t: T | T[]): T[] | undefined { |
||||
if (t == null) return undefined |
||||
return Array.isArray(t) ? t : [t] |
||||
} |
||||
|
||||
export function getDisplayName(Component: unknown) { |
||||
return (Component as ComponentType).displayName |
||||
|| (Component as ComponentType).name |
||||
|| 'Component' |
||||
} |
@ -0,0 +1,11 @@ |
||||
import {CSSProperties} from "react"; |
||||
|
||||
export const mergeStyles = (style: CSSProperties, ...others: Array<CSSProperties | undefined>): CSSProperties => { |
||||
let merged = {...style} |
||||
for (const other of others) { |
||||
if (other != null) { |
||||
merged = {...merged, ...other} |
||||
} |
||||
} |
||||
return merged |
||||
} |
@ -0,0 +1,2 @@ |
||||
export * from './array' |
||||
export * from './css' |
@ -0,0 +1,26 @@ |
||||
{ |
||||
"compilerOptions": { |
||||
"target": "ES2020", |
||||
"useDefineForClassFields": true, |
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"], |
||||
"module": "ESNext", |
||||
"skipLibCheck": true, |
||||
|
||||
/* Bundler mode */ |
||||
"moduleResolution": "bundler", |
||||
"allowImportingTsExtensions": true, |
||||
"resolveJsonModule": true, |
||||
"isolatedModules": true, |
||||
"noEmit": true, |
||||
"jsx": "react-jsx", |
||||
"baseUrl": ".", |
||||
|
||||
/* Linting */ |
||||
"strict": true, |
||||
"noUnusedLocals": true, |
||||
"noUnusedParameters": true, |
||||
"noFallthroughCasesInSwitch": true |
||||
}, |
||||
"include": ["src"], |
||||
"references": [{ "path": "./tsconfig.node.json" }] |
||||
} |
@ -0,0 +1,10 @@ |
||||
{ |
||||
"compilerOptions": { |
||||
"composite": true, |
||||
"skipLibCheck": true, |
||||
"module": "ESNext", |
||||
"moduleResolution": "bundler", |
||||
"allowSyntheticDefaultImports": true |
||||
}, |
||||
"include": ["vite.config.ts"] |
||||
} |
@ -0,0 +1,7 @@ |
||||
{ |
||||
"name": "@doad/mock", |
||||
"version": "1.0.0", |
||||
"dependencies": { |
||||
"path-to-regexp": "^6.2.1" |
||||
} |
||||
} |
@ -0,0 +1,28 @@ |
||||
import {DefineOptions, MockMethod} from "./types"; |
||||
import {setMockData} from "./utils"; |
||||
|
||||
export function defineMock(opts: DefineOptions) { |
||||
if (!(import.meta as any).env?.VITE_USE_MOCK) { |
||||
return |
||||
} |
||||
let items: MockMethod | Array<MockMethod> | undefined |
||||
if (typeof opts === 'function') { |
||||
items = opts() |
||||
} else if (Array.isArray(opts)) { |
||||
items = opts |
||||
} else if ('setup' in opts) { |
||||
if (opts.mock || opts.mock == null) { |
||||
items = opts.setup() |
||||
} |
||||
} else { |
||||
items = opts |
||||
} |
||||
if (!items) { |
||||
return |
||||
} |
||||
if (Array.isArray(items)) { |
||||
items.forEach(setMockData) |
||||
} else { |
||||
setMockData(items) |
||||
} |
||||
} |
@ -0,0 +1,99 @@ |
||||
// import {Connect} from "vite";
|
||||
// import IncomingMessage = Connect.IncomingMessage;
|
||||
// import {ServerResponse} from "http";
|
||||
// import TypedArray = NodeJS.TypedArray;
|
||||
//
|
||||
// export type MockMethod =
|
||||
// | 'get'
|
||||
// | 'post'
|
||||
// | 'put'
|
||||
// | 'delete'
|
||||
// | 'patch'
|
||||
// | 'head'
|
||||
// | 'options'
|
||||
//
|
||||
// export interface MockContext {
|
||||
// params: Record<string, any> | null
|
||||
// query: URLSearchParams
|
||||
// body: () => Promise<Record<string, any> | null>
|
||||
// headers?: Record<string, string>
|
||||
// }
|
||||
//
|
||||
// export interface ServerMockContext extends MockContext {
|
||||
// request: IncomingMessage
|
||||
// response: ServerResponse
|
||||
// json: () => unknown
|
||||
// text: () => string
|
||||
// buffer: () => TypedArray
|
||||
// }
|
||||
//
|
||||
// export interface MockItem<T = unknown> {
|
||||
// url: string
|
||||
// method?: MockMethod
|
||||
// timeout?: number
|
||||
// statusCode?: number
|
||||
// response?: (ctx: MockContext) => Promise<T> | T
|
||||
// }
|
||||
//
|
||||
// export type DefineFunction = () => MockItem | MockItem[]
|
||||
//
|
||||
// export interface DefineMockOptions {
|
||||
// mock?: boolean,
|
||||
// setup: DefineFunction
|
||||
// }
|
||||
//
|
||||
// export type DefineOptions =
|
||||
// | DefineMockOptions
|
||||
// | DefineFunction
|
||||
// | MockItem
|
||||
// | MockItem[]
|
||||
|
||||
import { IncomingMessage, ServerResponse as RawServerResponse } from 'http' |
||||
|
||||
export type MethodType = 'get' | 'post' | 'put' | 'delete' | 'patch' |
||||
|
||||
export interface ServerResponse extends RawServerResponse { |
||||
text(): Promise<string> |
||||
json(): Promise<unknown> |
||||
buffer(): Promise<Uint8Array> |
||||
} |
||||
|
||||
export interface MockContext { |
||||
/** 发起请求的路径 */ |
||||
url: string |
||||
/** 发起请求的参数 */ |
||||
method: string |
||||
/** 路径上面解析到的参数 */ |
||||
params: Record<string, any> | null |
||||
/** 解析到的 QueryString 参数 */ |
||||
query: URLSearchParams |
||||
/** 提交的报头 */ |
||||
headers?: Record<string, string> |
||||
/** 提交的内容 */ |
||||
body: () => Promise<Record<string, any> | string | null> |
||||
/** 请求结构,利用服务器端 mock 时有效 */ |
||||
request?: IncomingMessage |
||||
/** 响应结构,利用服务器端 mock 时有效 */ |
||||
response: ServerResponse |
||||
} |
||||
|
||||
export interface MockMethod { |
||||
url: string |
||||
method?: MethodType | MethodType[] |
||||
timeout?: number |
||||
statusCode?: number |
||||
response: (ctx: MockContext) => unknown |
||||
} |
||||
|
||||
export type DefineFunction = () => MockMethod | Array<MockMethod> |
||||
|
||||
export interface DefineMockOptions { |
||||
mock?: boolean, |
||||
setup: DefineFunction |
||||
} |
||||
|
||||
export type DefineOptions = |
||||
| DefineMockOptions |
||||
| DefineFunction |
||||
| MockMethod |
||||
| Array<MockMethod> |
@ -0,0 +1,28 @@ |
||||
import { match, pathToRegexp } from 'path-to-regexp' |
||||
import {MockContext, MockMethod} from "./types"; |
||||
|
||||
interface MockItemNormalized<T = unknown> { |
||||
url: string |
||||
method: string |
||||
regex: RegExp |
||||
match: (path: string) => Record<string, unknown> | null |
||||
timeout?: number |
||||
statusCode?: number |
||||
response?: (ctx: MockContext) => T |
||||
} |
||||
|
||||
const mockItems: MockItemNormalized[] = [] |
||||
|
||||
export function setMockData(data: MockMethod) { |
||||
mockItems.push({ |
||||
...data, |
||||
method: ((data.method ?? 'get') as string).toUpperCase(), |
||||
regex: pathToRegexp(data.url), |
||||
match(path: string): Record<string, unknown> | null { |
||||
const urlMatch = match(data.url, { decode: decodeURIComponent }) |
||||
const res = urlMatch(path) |
||||
if (!res) return null |
||||
return res.params as Record<string, unknown> |
||||
}, |
||||
}) |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,3 @@ |
||||
packages: |
||||
- 'packages/*' |
||||
- 'example' |
@ -0,0 +1,27 @@ |
||||
// @ts-check
|
||||
import chalk from 'chalk' |
||||
import {readFileSync} from 'node:fs' |
||||
import path from 'node:path' |
||||
|
||||
const msgPath = path.resolve('.git/COMMIT_EDITMSG') |
||||
const msg = readFileSync(msgPath, 'utf-8').trim() |
||||
const commitRE = |
||||
/^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release)(\(.+\))?: .{1,50}/ |
||||
|
||||
if (!commitRE.test(msg)) { |
||||
console.log() |
||||
console.error( |
||||
` ${chalk.bgRed.white(' ERROR ')} ${chalk.red( |
||||
`invalid commit message format.` |
||||
)}\n\n` +
|
||||
chalk.red( |
||||
` Proper commit message format is required for automated changelog generation. Examples:\n\n` |
||||
) + |
||||
` ${chalk.green(`feat(compiler): add 'comments' option`)}\n` + |
||||
` ${chalk.green( |
||||
`fix(v-model): handle events on blur (close #28)` |
||||
)}\n\n` +
|
||||
chalk.red(` See .github/commit-convention.md for more details.\n`) |
||||
) |
||||
process.exit(1) |
||||
} |
@ -0,0 +1,27 @@ |
||||
{ |
||||
"compilerOptions": { |
||||
"target": "ES2020", |
||||
"useDefineForClassFields": true, |
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"], |
||||
"module": "ESNext", |
||||
"skipLibCheck": true, |
||||
|
||||
/* Bundler mode */ |
||||
"moduleResolution": "bundler", |
||||
"allowImportingTsExtensions": true, |
||||
"resolveJsonModule": true, |
||||
"isolatedModules": true, |
||||
"noEmit": true, |
||||
"jsx": "react-jsx", |
||||
"rootDir": ".", |
||||
/* Linting */ |
||||
"strict": true, |
||||
"strictNullChecks": true, |
||||
"noUnusedLocals": true, |
||||
"noUnusedParameters": true, |
||||
"noFallthroughCasesInSwitch": true |
||||
}, |
||||
"include": ["src"], |
||||
"exclude": ["node_modules"], |
||||
"references": [{ "path": "./tsconfig.node.json" }] |
||||
} |
@ -0,0 +1,10 @@ |
||||
{ |
||||
"compilerOptions": { |
||||
"composite": true, |
||||
"skipLibCheck": true, |
||||
"module": "ESNext", |
||||
"moduleResolution": "bundler", |
||||
"allowSyntheticDefaultImports": true |
||||
}, |
||||
"include": ["vite.config.ts"] |
||||
} |
@ -0,0 +1,12 @@ |
||||
import {defineConfig} from "vitest/config"; |
||||
|
||||
export default defineConfig({ |
||||
define: { |
||||
__DEV__: true, |
||||
__VERSION__: '"test"', |
||||
}, |
||||
test: { |
||||
globals: true, |
||||
threads: !process.env.GITHUB_ACTIONS, |
||||
} |
||||
}) |
Loading…
Reference in new issue