初始化项目

main
熊二 1 year ago
commit 6177c7cd10
  1. 13
      .editorconfig
  2. 2
      .eslintignore
  3. 13
      .eslintrc.cjs
  4. 9
      .gitignore
  5. 11
      .npmrc
  6. 23
      example/.eslintrc.cjs
  7. 27
      example/README.md
  8. 13
      example/index.html
  9. 41
      example/package.json
  10. 15
      example/src/components/layouts/AdminLayout.tsx
  11. 50
      example/src/components/layouts/ErrorPage.tsx
  12. 24
      example/src/index.css
  13. 120
      example/src/main.tsx
  14. 50
      example/src/routes/test.tsx
  15. 50
      example/src/services/api.d.ts
  16. 1
      example/src/vite-env.d.ts
  17. 29
      example/tsconfig.json
  18. 10
      example/tsconfig.node.json
  19. 13
      example/vite.config.ts
  20. 34
      package.json
  21. 22
      packages/doad/.eslintrc.cjs
  22. 41
      packages/doad/package.json
  23. 123
      packages/doad/src/components/access.tsx
  24. 39
      packages/doad/src/components/head.tsx
  25. 7
      packages/doad/src/components/layout.css
  26. 87
      packages/doad/src/components/layout.tsx
  27. 29
      packages/doad/src/components/lazy.tsx
  28. 36
      packages/doad/src/components/logo.tsx
  29. 114
      packages/doad/src/components/menu.tsx
  30. 48
      packages/doad/src/components/root.tsx
  31. 36
      packages/doad/src/components/search.tsx
  32. 47
      packages/doad/src/components/title.tsx
  33. 10
      packages/doad/src/index.ts
  34. 59
      packages/doad/src/state.ts
  35. 57
      packages/doad/src/types.ts
  36. 12
      packages/doad/src/utils/array.ts
  37. 11
      packages/doad/src/utils/css.ts
  38. 2
      packages/doad/src/utils/index.ts
  39. 26
      packages/doad/tsconfig.json
  40. 10
      packages/doad/tsconfig.node.json
  41. 7
      packages/mock/package.json
  42. 28
      packages/mock/src/mock.ts
  43. 99
      packages/mock/src/types.ts
  44. 28
      packages/mock/src/utils.ts
  45. 3962
      pnpm-lock.yaml
  46. 3
      pnpm-workspace.yaml
  47. 27
      scripts/verifyCommit.mjs
  48. 27
      tsconfig.json
  49. 10
      tsconfig.node.json
  50. 12
      vitest.config.ts

@ -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',
}

9
.gitignore vendored

@ -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 :
*
* - GETSELECT
* - POSTCREATE
* - PUTUPDATE
* - PATCHUPDATE
* - DELETEDELETE */
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…
Cancel
Save