Commit 7af2c21d authored by 蒋佳奇's avatar 蒋佳奇

feat: Frist Version

parents
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
registry = http://npm.dui88.com/
\ No newline at end of file
# node
# Version v22
FROM duiba-habor-registry.cn-hangzhou.cr.aliyuncs.com/duiba_front/node:22.14.0
MAINTAINER op@duiba.com.cn
ARG appname
ARG NODE_ENV
RUN echo "export LANG=en_US.UTF-8" && echo "Asia/Shanghai" > /etc/timezone
ENV LANG='en_US.UTF-8'
RUN mkdir /root/duiba-deploy/
ADD ./duiba-deploy /root/duiba-deploy/
WORKDIR /root/duiba-deploy/
#define entry point which will be run first when the container starts up
ENTRYPOINT node server
\ No newline at end of file
# Tuia-Manager-System-Demo
此工程为推啊后台管理系统DEMO,基于React + TypeScript + Vite + Mobx搭建。
[TOC]
## 项目结构
**工程目录结构**
```
tuia-manager-system-demo
├── dist # 构建目录
├── mock # 模拟数据
├── server # 部署脚本
├── src
│   ├── apis # 接口服务
│   ├── components
│   │ ├── common # 公共组件库
│   │ ├── **/* # 业务组件库(不接入autoImport)
│   ├── constants # 常量
│   ├── hooks # 常用hooks
│   ├── layouts # 布局组件(不接入autoImport)
│   ├── pages # 页面组件(已接入文件系统路由)
│   ├── stores # mobx store
│ │   utils # 工具库
│ │   main.tsx # 入口文件
├── README.md
└── vite.config.ts
```
## Proxy
### Yapi-MOCK助手插件
1. 安装YAPI-MOCK助手(https://marketplace.visualstudio.com/items?itemName=Hidetoxic.yapi-mock-helper)
2. 在yapi-mock.config.json中指定项目代理服务器端口
3. 在vite.config.ts的server.proxy指定代理地址为本地指定端口(demo端口为10089,建议fork后修改)
4. 启动项目后,从资源管理器,启动YAPI-MOCK助手
![alt text](https://yun.dui88.com/jjq/images/d2skfpns6sl6jgf1rylpd.png)
5. 通过VSCode状态栏,以切换代理地址
![alt text](https://yun.dui88.com/jjq/images/e5b63_q7vskpu9_v3s14_.png)
![alt text](https://yun.dui88.com/jjq/images/sxemnih-qnsdesqkhaxi8.png)
Tips: 虽然目前无法配置Yapi,但启用【YAPI-MOCK缓存模式(默认)】时,可通过手动维护mock文件夹目录,使用本地代理。支持热更新。
### vite代理
你也可以通过修改vite.config.ts的server.proxy配置,自定义代理。
## Auto Import
除业务组件、布局组件(见目录结构)外,项目绝大部份类型声明及变量定义,使用unplugin-auto-import自动导入,无需手动引入。
tips: 请注意开发过程中终端输出的错误提示,<span style="color:red">[重复导入错误]</span>。此错误会导致构建失败。
## 文件系统路由
此工程使用文件系统路由,参考[nuxt路由](https://nuxt.zhcndoc.com/docs/getting-started/routing)
## TODO
- 组件库完善: 大家在系统开发过程中,如编写了通用的组件,请同步到demo中。
\ No newline at end of file
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const Button: typeof import('antd')['Button']
const Card: typeof import('antd')['Card']
const Form: typeof import('antd')['Form']
const Input: typeof import('antd')['Input']
const Layout: typeof import('antd')['Layout']
const Link: typeof import('react-router-dom')['Link']
const Menu: typeof import('antd')['Menu']
const NavLink: typeof import('react-router-dom')['NavLink']
const Navigate: typeof import('react-router-dom')['Navigate']
const Observer: typeof import('mobx-react-lite')['Observer']
const Outlet: typeof import('react-router-dom')['Outlet']
const REGEX: typeof import('pixiu-number-toolkit')['REGEX']
const Result: typeof import('antd')['Result']
const Role: typeof import('./src/constants/enums/index')['Role']
const Route: typeof import('react-router-dom')['Route']
const Routes: typeof import('react-router-dom')['Routes']
const Rules: typeof import('./src/utils/REG')['Rules']
const Spin: typeof import('antd')['Spin']
const UserList: typeof import('./src/constants/userList')['UserList']
const action: typeof import('mobx')['action']
const autorun: typeof import('mobx')['autorun']
const axios: typeof import('axios')['default']
const buildREG: typeof import('./src/utils/REG')['buildREG']
const changeDate: typeof import('./src/utils/date')['changeDate']
const computed: typeof import('mobx')['computed']
const cookie: typeof import('./src/utils/cookie')['default']
const counterStore: typeof import('./src/stores/counterStore')['counterStore']
const createRef: typeof import('react')['createRef']
const createRequester: typeof import('./src/utils/request')['createRequester']
const digitUppercase: typeof import('./src/utils/money')['digitUppercase']
const extendObservable: typeof import('mobx')['extendObservable']
const fenToYuan: typeof import('./src/utils/money')['fenToYuan']
const flow: typeof import('mobx')['flow']
const flowResult: typeof import('mobx')['flowResult']
const forwardRef: typeof import('react')['forwardRef']
const getCustomList: typeof import('./src/apis/index')['getCustomList']
const getFailedRecords: typeof import('./src/apis/index')['getFailedRecords']
const getRealTime: typeof import('./src/apis/index')['getRealTime']
const getTopOnlineApp: typeof import('./src/apis/index')['getTopOnlineApp']
const getUserById: typeof import('./src/apis/user/index')['getUserById']
const getUserList: typeof import('./src/apis/user/index')['getUserList']
const intercept: typeof import('mobx')['intercept']
const isEmail: typeof import('./src/utils/REG')['isEmail']
const isNothing: typeof import('./src/utils/index')['isNothing']
const lazy: typeof import('react')['lazy']
const liToYuan: typeof import('./src/utils/money')['liToYuan']
const makeAutoObservable: typeof import('mobx')['makeAutoObservable']
const makeObservable: typeof import('mobx')['makeObservable']
const memo: typeof import('react')['memo']
const message: typeof import('antd')['message']
const observable: typeof import('mobx')['observable']
const observe: typeof import('mobx')['observe']
const observer: typeof import('mobx-react-lite')['observer']
const onBecomeObserved: typeof import('mobx')['onBecomeObserved']
const onBecomeUnobserved: typeof import('mobx')['onBecomeUnobserved']
const onReactionError: typeof import('mobx')['onReactionError']
const permission: typeof import('./src/utils/permission')['default']
const precent: typeof import('./src/utils/number')['precent']
const precentAddSuffix: typeof import('./src/utils/number')['precentAddSuffix']
const prefixYuan: typeof import('./src/utils/money')['prefixYuan']
const rangePresets: typeof import('./src/utils/date')['rangePresets']
const reaction: typeof import('mobx')['reaction']
const record2options: typeof import('./src/utils/index')['record2options']
const request: typeof import('./src/utils/request')['default']
const runInAction: typeof import('mobx')['runInAction']
const smartFenToYuan: typeof import('./src/utils/money')['smartFenToYuan']
const startTransition: typeof import('react')['startTransition']
const theme: typeof import('antd')['theme']
const toJS: typeof import('mobx')['toJS']
const unitYuan: typeof import('./src/utils/money')['unitYuan']
const useCallback: typeof import('react')['useCallback']
const useContext: typeof import('react')['useContext']
const useDebugValue: typeof import('react')['useDebugValue']
const useDeferredValue: typeof import('react')['useDeferredValue']
const useEffect: typeof import('react')['useEffect']
const useHref: typeof import('react-router-dom')['useHref']
const useId: typeof import('react')['useId']
const useImperativeHandle: typeof import('react')['useImperativeHandle']
const useInRouterContext: typeof import('react-router-dom')['useInRouterContext']
const useInsertionEffect: typeof import('react')['useInsertionEffect']
const useLayoutEffect: typeof import('react')['useLayoutEffect']
const useLinkClickHandler: typeof import('react-router-dom')['useLinkClickHandler']
const useLocalObservable: typeof import('mobx-react-lite')['useLocalObservable']
const useLocation: typeof import('react-router-dom')['useLocation']
const useMemo: typeof import('react')['useMemo']
const useNavigate: typeof import('react-router-dom')['useNavigate']
const useNavigationType: typeof import('react-router-dom')['useNavigationType']
const useOutlet: typeof import('react-router-dom')['useOutlet']
const useOutletContext: typeof import('react-router-dom')['useOutletContext']
const useParams: typeof import('react-router-dom')['useParams']
const useReducer: typeof import('react')['useReducer']
const useRef: typeof import('react')['useRef']
const useRequest: typeof import('./src/hooks/useRequest')['default']
const useResolvedPath: typeof import('react-router-dom')['useResolvedPath']
const useRoutes: typeof import('react-router-dom')['useRoutes']
const useSearchParams: typeof import('react-router-dom')['useSearchParams']
const useState: typeof import('react')['useState']
const useSyncExternalStore: typeof import('react')['useSyncExternalStore']
const useTransition: typeof import('react')['useTransition']
const when: typeof import('mobx')['when']
const yuanToFen: typeof import('./src/utils/money')['yuanToFen']
const yuanToLi: typeof import('./src/utils/money')['yuanToLi']
}
// for type re-export
declare global {
// @ts-ignore
export type { UserEntity } from './src/apis/user/type'
import('./src/apis/user/type')
}
import jsConfig from '@tuia/eslint-config-common/global.js'
import reactConfig from '@tuia/eslint-config-common/react.js'
import tsConfig from '@tuia/eslint-config-common/typeScript.js'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [...jsConfig, ...reactConfig, ...tsConfig, { ignores: ['dist'] }, reactRefresh.configs.vite]
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="https://yun.tuisnake.com/tuia/payment/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
This diff is collapsed.
{
"name": "demo",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.0",
"@tuia/eslint-config-common": "3.0.1-beta.2",
"antd": "^5.24.5",
"axios": "^1.8.4",
"dayjs": "^1.11.13",
"mobx": "^6.13.7",
"mobx-react-lite": "^4.1.0",
"numeral": "^2.0.6",
"pixiu-number-toolkit": "^3.0.11",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.4.0",
"react-router-dom": "^7.4.0",
"tailwindcss": "^4.1.0"
},
"devDependencies": {
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@types/numeral": "^2.0.5",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react-swc": "^3.8.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"typescript": "~5.7.2",
"unplugin-auto-import": "^19.1.2",
"vite": "^6.2.0",
"vite-plugin-pages": "^0.32.5"
}
}
import config from '@tuia/eslint-config-common/prettier.js'
export default { ...config }
const NodeDocker = require('@tuia/node-docker')
const Koa = require('koa')
const logger = require('koa-logger')
const onerror = require('koa-onerror')
const Router = require('koa-router')
const koaStatic = require('koa-static')
const path = require('path')
const Eurekaclient = require('@tuia/eureka-client-ts').default
const app = new Koa()
const router = new Router()
// error handler
onerror(app)
app.use(
logger((str, args) => {
!str.includes('/monitor/check') && console.log(str)
})
)
app.use(koaStatic(path.join(__dirname, './dist')))
// 用于中台确认应用是否运行正常
router.get('/monitor/check', function (ctx, next) {
ctx.body = 'ok'
})
app.use(router.routes())
const dockerApp = new NodeDocker({
app,
appType: 'koa',
port: 3000,
})
dockerApp.startServer().then(async () => {
const client = new Eurekaclient({
port: 3000,
appName: '<app-name>',
services: [
// 需要通信的其他应用名
],
})
await client.start()
global.eureka = client
})
// 进程结束主动通知Eureka取消注册
let isShuttingDown = false
async function deregisterEureka() {
if (global.eureka) {
console.log('Deregistering from Eureka...')
await new Promise((resolve) => {
global.eureka.eureka.deregister(() => {
console.log('Deregistered from Eureka...')
resolve()
})
})
}
}
async function gracefulShutdown() {
if (isShuttingDown) return
isShuttingDown = true
console.log('Exitting...')
await deregisterEureka()
process.exit(0)
}
;['exit', 'SIGINT', 'SIGTERM'].forEach((event) => {
process.on(event, gracefulShutdown)
})
{
"name": "tuia-payment-manager-node",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@tuia/eureka-client-ts": "^0.3.8",
"@tuia/node-docker": "^0.0.3",
"koa": "^2.15.3",
"koa-logger": "^3.2.1",
"koa-onerror": "^4.2.0",
"koa-router": "^12.0.1",
"koa-static": "^5.0.0"
},
"devDependencies": {
"@koa/cors": "^5.0.0"
}
}
This diff is collapsed.
import { Suspense } from 'react'
import routes from '~react-pages'
import Layouts from './layouts'
const App: React.FC = () => {
return (
<Suspense fallback={<Spin fullscreen />}>
<Layouts>{useRoutes(routes)}</Layouts>
</Suspense>
)
}
export default App
export const getCustomList = () => {
return request.post<void, number[]>('/custom/list')
}
export const getFailedRecords = () => {
return request.get<void, number[]>('/complain/failedRecords')
}
export const getTopOnlineApp = () => {
return request.get<void, number[]>('/top/online/app')
}
export const getUserList = () => {
return new Promise<Array<UserEntity>>((resolve) => {
setTimeout(() => resolve(UserList), 1000)
})
}
export const getUserById = (id: number) => {
return new Promise<UserEntity | undefined>((resolve) => {
setTimeout(() => resolve(UserList.find((item) => item.id === id)), 1000)
})
}
export interface UserEntity {
id: number
name: string
}
// enum demo
export const Role = {
MANAGER: 1,
EMPLOYEE: 2,
CUSTOM: 3,
} as const
export type Role = (typeof Role)[keyof typeof Role]
export const UserList = [
{ id: 1, name: 'Jia Qi' },
{ id: 2, name: 'Xing Wang' },
]
interface UseRequestOption<TRequest extends unknown[], TResult> {
defaultParams?: TRequest
mannual?: boolean
onSuccess?: (data: TResult) => void
onError?: (error: Error, params: TRequest) => void
onFinally?: () => void
}
interface UseRequestResult<TRequest extends unknown[], TResult> {
data?: Awaited<TResult>
loading: boolean
error?: Error
run: (...args: TRequest) => Promise<TResult>
refresh: () => Promise<TResult>
}
const useRequest = <TRequest extends unknown[], TResult>(
request: (...args: TRequest) => TResult,
options: UseRequestOption<TRequest, TResult> = {}
): UseRequestResult<TRequest, TResult> => {
const [data, setData] = useState<Awaited<TResult>>()
const [loading, setLoading] = useState(false)
const [error, setError] = useState<Error>()
const cacheParams = useRef<TRequest>(options.defaultParams || ([] as unknown as TRequest))
const run = useCallback(async (...args: TRequest) => {
setLoading(true)
try {
cacheParams.current = args
const result = await request(...args)
options.onSuccess?.(result)
setData(result)
return result
} catch (err) {
options.onError?.(err as Error, args)
setError(err as Error)
throw err
} finally {
options.onFinally?.()
setLoading(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const refresh = useCallback(() => {
return run(...cacheParams.current)
}, [run])
useEffect(() => {
if (options.mannual) {
return
}
refresh()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return {
data,
loading,
error,
run,
refresh,
}
}
export default useRequest
@import "tailwindcss"
\ No newline at end of file
const AuthLayout: React.FC<React.PropsWithChildren> = ({ children }) => {
return children
}
export default AuthLayout
import { useNavigate } from 'react-router-dom'
import { DashboardOutlined } from '@ant-design/icons'
// src/layouts/BasicLayout.tsx
import { Layout, Menu, theme } from 'antd'
const { Header, Content, Sider } = Layout
const menuItems = [
{
key: '/',
icon: <DashboardOutlined />,
label: 'Dashboard',
},
]
const BasicLayout: React.FC<React.PropsWithChildren> = ({ children }) => {
const {
token: { colorBgContainer },
} = theme.useToken()
const navigate = useNavigate()
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider breakpoint='lg'>
<div className='demo-logo-vertical' />
<Menu theme='dark' mode='vertical' items={menuItems} onSelect={({ key }) => navigate(key)} />
</Sider>
<Layout>
<Header style={{ padding: 0, background: colorBgContainer }} />
<Content style={{ margin: '24px 16px 0' }}>
<div style={{ padding: 24, background: colorBgContainer }}>{children}</div>
</Content>
</Layout>
</Layout>
)
}
export default BasicLayout
import AuthLayout from './AuthLayout'
import BasicLayout from './BasicLayout'
const Layout: React.FC<React.PropsWithChildren> = ({ children }) => {
const location = useLocation()
const Layout = useMemo(() => (location.pathname === '/login' ? AuthLayout : BasicLayout), [location.pathname])
const navigate = useNavigate()
const [loading, setLoading] = useState(true)
useEffect(() => {
const token = localStorage.getItem('token')
if (location.pathname !== '/login' && !token) {
navigate('/login')
} else {
setLoading(false)
}
}, [location.pathname, navigate])
if (loading) return <Spin fullscreen />
return <Layout>{children}</Layout>
}
export default Layout
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { HashRouter } from 'react-router-dom'
import App from './App'
import './index.css'
import '@ant-design/v5-patch-for-react-19'
import 'antd/dist/reset.css'
const app = createRoot(document.getElementById('root')!)
app.render(
<StrictMode>
<HashRouter>
<App />
</HashRouter>
</StrictMode>
)
const About = () => <div>About</div>
export default About
const Counter = observer(() => {
const { count, doubleCount, loading, syncIncrement, asyncIncrement } = counterStore
return (
<div>
<p>Count: {count}</p>
<p>Double Count: {doubleCount}</p>
<Button onClick={syncIncrement}>+1</Button>
<Button onClick={asyncIncrement} disabled={loading}>
{loading ? 'Loading...' : 'Fetch Data'}
</Button>
</div>
)
})
export default Counter
// src/pages/dashboard/index.tsx
export default function Dashboard() {
const navigate = useNavigate()
const handleLogout = () => {
localStorage.removeItem('token')
navigate('/login')
}
return (
<Card title='控制台' extra={<Button onClick={handleLogout}>退出登录</Button>}>
<p>欢迎使用后台管理系统</p>
</Card>
)
}
import { LockOutlined, UserOutlined } from '@ant-design/icons'
type FieldType = {
username?: string
password?: string
}
export default function LoginPage() {
const navigate = useNavigate()
const [messageApi, contextHolder] = message.useMessage()
const onFinish = (values: FieldType) => {
if (values.username === 'admin' && values.password === '123456') {
localStorage.setItem('token', 'fake-jwt-token')
navigate('/')
messageApi.success('登录成功!')
} else {
messageApi.error('账号或密码错误')
}
}
return (
<div style={{ width: 360, margin: '100px auto' }}>
{contextHolder}
<h1 style={{ textAlign: 'center', marginBottom: 24 }}>后台管理系统</h1>
<Form name='login' initialValues={{ remember: true }} onFinish={onFinish}>
<Form.Item<FieldType> name='username' rules={[{ required: true, message: '请输入用户名' }]}>
<Input prefix={<UserOutlined />} placeholder='用户名' />
</Form.Item>
<Form.Item<FieldType> name='password' rules={[{ required: true, message: '请输入密码' }]}>
<Input.Password prefix={<LockOutlined />} placeholder='密码' />
</Form.Item>
<Form.Item>
<Button type='primary' htmlType='submit' block>
登录
</Button>
</Form.Item>
</Form>
</div>
)
}
const User: React.FC = () => {
return (
<div>
<header>
<h1 className='text-3xl font-bold underline'>Hello world!</h1>
</header>
<main>
<Outlet />
</main>
</div>
)
}
export default User
const SpercificUser = function () {
const { id } = useParams()
return <div>User {id}</div>
}
export default SpercificUser
const Index = () => <div>User Index</div>
export default Index
const UtilsExample: React.FC = () => {
// 金额对于后端来说只能是number & string & 空 & null & undefined
const a = prefixYuan(yuanToLi('300000', 2))
const b = smartFenToYuan('0', 3)
const c = liToYuan('0')
const d = digitUppercase('0')
// 正则
const e = buildREG(REGEX.INTEGER, 'undefined')
const f = precentAddSuffix(35)
return (
<div>
<p>1: {a}</p>
<p>2: {b}</p>
<p>3: {c}</p>
<p>4: {d}</p>
<p>5: {e}</p>
<p>6: {f}</p>
</div>
)
}
export default UtilsExample
import { useNavigate } from 'react-router-dom'
export default function NotFound() {
const navigate = useNavigate()
return (
<Result
status='404'
title='404'
subTitle='抱歉,您访问的页面不存在'
extra={
<Button type='primary' onClick={() => navigate('/')}>
返回首页
</Button>
}
/>
)
}
import React from 'react'
const Home: React.FC = () => {
const { data, run, loading } = useRequest(getUserList, { mannual: true })
const { run: userRun } = useRequest(getUserById, { defaultParams: [1], mannual: true })
const { data: failedRecords } = useRequest(getTopOnlineApp)
useEffect(() => {
userRun(1)
run()
}, [failedRecords, run, userRun])
return (
<div>
<header>首页</header>
<Button loading={loading} onClick={() => run()}>
测试
</Button>
{data?.map((user) => (
<React.Fragment key={user.id}>
<div>
<strong>id:</strong>
{user.id}
</div>
<div>
<strong>name:</strong>
{user.name}
</div>
</React.Fragment>
))}
</div>
)
}
export default Home
class CounterStore {
count = 0
loading = false
constructor() {
makeAutoObservable(this) // 自动推断类型
}
// Computed:计算属性
get doubleCount() {
return this.count * 2
}
// Action:同步修改状态
syncIncrement = () => {
this.count += 1
}
// Action:异步修改状态
asyncIncrement = async () => {
this.loading = true
await new Promise((resolve) => setTimeout(resolve, 1000))
this.count = 100
this.loading = false
}
}
export const counterStore = new CounterStore()
/**
* 常用正则表达式库
*/
import { REGEX, buildRegex } from 'pixiu-number-toolkit'
const isEmail = (email: string): boolean => {
if (isNothing(email)) {
return false
} else {
return REGEX.EMAIL.test(email)
}
}
/**
* @param REGEX 正则表达式= [
* https://www.muchappy.com/open_source/pixiu-number-toolkit/basic/regex
* NUMBER: 基础数字,包括正数、负数和小数
* INTEGER: 整数,包括正整数、负整数和零
* POSITIVE_INTEGER: 正整数
* NEGATIVE_INTEGER: 负整数
* NON_POSITIVE_INTEGER: 非正整数(包括负数和零)
* FLOAT: 浮点数
* POSITIVE_FLOAT: 正浮点数
* NEGATIVE_FLOAT: 负浮点数
* EMAIL: 电子邮箱地址
* CHINESE_NAME: 中文姓名
* ENGLISH_LETTER: 英文字母
* LOWERCASE_ENGLISH_LETTER: 小写英文字母
* UPPERCASE_ENGLISH_LETTER: 大写英文字母
* ]
* @param value 待验证的值
* @description 通过正则表达式验证值是否符合规则
* @returns true:符合规则 false:不符合规则
*/
const buildREG = (REGEX: RegExp, value: string | number): boolean => {
const strictRegex = buildRegex(REGEX, { strict: true })
return strictRegex.test(value + '')
}
const Rules = {
required: {
required: true,
message: '必填',
},
id: {
pattern: /^\d{0,20}$/,
message: '只能是数字',
},
ids: {
pattern: /^\d{0,20}(,\d{0,20})*$/,
message: '输入的格式有误',
},
name: {
max: 20,
message: '最多不超过20个字',
},
noCN: {
pattern: /^[^\u4e00-\u9fa5]+$/,
message: '不能出现中文',
},
email: {
pattern: REGEX.EMAIL,
message: '请输入正确的邮箱格式',
},
url: {
pattern: REGEX.URL,
message: '请输入正确的网址',
},
positiveInt: {
pattern: REGEX.POSITIVE_INTEGER,
message: '请输入正整数',
},
intLt1e100: {
pattern: /^([1-9][0-9]{0,1}|100)$/,
message: '请输入1-100整数',
},
intLt0e100: {
pattern: /^100$|^(\d|[1-9]\d)$/,
message: '请输入0-100整数',
},
float0e100: {
pattern: /^(?:100(?:\.00?)?|\d{1,2}(?:\.\d{1,2})?)$/,
message: '请输入0-100之间的数字,最多保留两位小数',
},
// 金额校验 包含正负 0 两位小数
money: {
pattern: /(^([+-]?)[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^([+-]?)(0){1}$)|(^([+-]?)[0-9]\.[0-9]([0-9])?$)/,
message: '请输入两位小数以内的正负金额数',
},
percent: {
pattern: /^(\d|[1-9]\d|100)(\.\d{1,2})?$/,
message: '请输入正确的百分比',
},
}
export { isEmail, buildREG, Rules }
function getCookie(name: string) {
const reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)')
const matches = reg.exec(document.cookie)
return matches ? matches[2] : null
}
function setCookie(name: string, value: string, expired: number) {
const d = new Date()
d.setTime(d.getTime() + expired * 24 * 60 * 60 * 1000)
const expires = 'expires=' + d.toUTCString()
document.cookie = name + '=' + value + '; ' + expires
}
export default {
get: getCookie,
set: setCookie,
}
import type { TimeRangePickerProps } from 'antd'
import dayjs from 'dayjs'
/**
* 时间处理工具
**/
export const rangePresets: TimeRangePickerProps['presets'] = [
{ label: '今天', value: [dayjs(), dayjs()] },
{ label: '昨天', value: [dayjs().add(-1, 'd'), dayjs().add(-1, 'd')] },
{ label: '最近三天', value: [dayjs().add(-2, 'd'), dayjs()] },
{ label: '最近七天', value: [dayjs().add(-6, 'd'), dayjs()] },
{ label: '最近三十一天', value: [dayjs().add(-31, 'd'), dayjs()] },
{ label: '本月', value: [dayjs().startOf('month'), dayjs().endOf('month')] },
]
export const changeDate = (num: number, format = 'YYYY-MM-DD') => {
return dayjs().add(num, 'days').format(format)
}
export function isNothing(value: number | void | string | null) {
return (
value === '' ||
value === undefined ||
value === null ||
(typeof value === 'number' && (isNaN(value) || !isFinite(value)))
)
}
import numeral from 'numeral'
import { numberToChineseUppercaseCurrency, currency } from 'pixiu-number-toolkit'
/**
* 金额转换工具
* /
/**
*
* @param num 金额|单位分
* @description 1元 = 100分
* @param precision 小数点位数
* @returns 格式化的金额|单位元
* @description 默认保留2位小数
*/
const fenToYuan = (num: number | string, precision = 2): number | string => {
if (isNothing(num)) {
return ''
} else {
return numeral(num)
.divide(100)
.format(`0.${Array(precision).fill(0).join('')}`)
}
}
/**
*
* @param num 金额|单位分
* @description 1元 = 100分
* @param precision 小数点位数
* @returns 格式化的金额 |单位元
* @description 返回金额为整数,无小数点
* @description 返回金额为小数,保留precision小数点
*/
const smartFenToYuan = (num: number | string, precision = 2): number | string => {
if (isNothing(num)) {
return ''
}
return numeral(num)
.divide(100)
.format(`0[.]${Array(precision).fill(0).join('')}`)
}
/**
* @param num 金额|单位厘
* @description 1元 = 1000厘
* @param precision 小数点位数
* @returns 格式化的金额 |单位元
* @description 默认保留2位小数
*/
const liToYuan = (num: number | string, precision = 2): number | string => {
if (isNothing(num)) {
return ''
} else {
return numeral(num)
.divide(1000)
.format(`0.${Array(precision).fill(0).join('')}`)
}
}
/**
*
* @param num 金额|单位元
* @description 1元 = 100分
* @param precision 小数点位数
* @returns 格式化的金额|单位分
* 默认保留2位小数
*/
const yuanToFen = (num: number | string, precision = 2): number | string => {
if (isNothing(num)) {
return ''
} else {
return numeral(num)
.multiply(100)
.format(`0.${Array(precision).fill(0).join('')}`)
}
}
/**
* @param num 金额|单位元
* @description 1元 = 1000厘
* @param precision 小数点位数
* @returns 格式化的金额 |单位厘
* @description 默认保留2位小数
*/
const yuanToLi = (num: number | string, precision = 2): number | string => {
if (isNothing(num)) {
return ''
} else {
return numeral(num)
.multiply(1000)
.format(`0.${Array(precision).fill(0).join('')}`)
}
}
/**
*
* @param num 金额|单位元
* @returns 大写金额
* @description 小数点最多读取2位,角、分
* @description 负数,前面加"欠"
*/
const digitUppercase = (num: number | string): string => {
if (isNothing(num)) {
return ''
} else {
return numberToChineseUppercaseCurrency(Number(num))
}
}
/**
* 添加单位元
* @param num 数字
* @returns num + '元'
* @description 默认保留2位小数
*/
const unitYuan = (num: string | number, precision = 2): string => {
return currency(num, { suffix: '元', minimumFractionDigits: precision })
}
/**
* 添加前缀¥
* @param num 数字
* @returns ¥num
* @description 默认保留2位小数
*/
const prefixYuan = (num: string | number, precision = 2): string => {
return currency(num, { prefix: '¥', minimumFractionDigits: precision })
}
export { fenToYuan, smartFenToYuan, liToYuan, yuanToFen, yuanToLi, digitUppercase, unitYuan, prefixYuan }
import { currency } from 'pixiu-number-toolkit'
/**
* 数据处理工具
* /
/**
* 百分比格式|*100%
* @param num 数字
* @returns 百分比格式
* @example currency(0.35, { style: 'percent' }) = 35%
*/
const precent = (num: number | string, precision = 2): string | number => {
if (isNothing(num)) return '-'
return currency(num, { style: 'percent', minimumFractionDigits: precision })
}
/**
* 百分比格式|直接加后缀%
* @param num 数字
* @returns 百分比格式
* @example currency(35, { suffix: '%', minimumFractionDigits: precision }) = 35%
*/
const precentAddSuffix = (num: number | string, precision = 2): string | number => {
if (isNothing(num)) return '-'
return currency(num, { suffix: '%', minimumFractionDigits: precision })
}
export { precent, precentAddSuffix }
/**
* 权限工具类
*/
let permissionCodes: string[] = []
const Permissions = {
getPermissions() {
return permissionCodes
},
setPermissions(permissions: string[]) {
if (permissions && permissions.length) {
permissionCodes = [...permissions]
}
},
authRender<T>(item: T, code: string | string[]) {
if (this.ifRender(code)) {
return item
} else {
return null
}
},
// 判断是否需要渲染权限组件
ifRender(code: string | string[]): boolean {
// 用于判断是否需要渲染
let shouldRender = false
// 如果传入的是个权限数组,那么需要所有权限都有的情况下才能渲染
if (typeof code !== 'string') {
shouldRender = code.every((el) => {
return permissionCodes.some((item) => item.includes(el))
})
} else {
// 传入的是字符串则匹配是否有该权限
shouldRender = permissionCodes.some((item) => item.includes(code))
}
return shouldRender
},
}
export default Permissions
const XCsrfToken = cookie.get('csrf_token') || ''
export function createRequester() {
const requester = axios.create({
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Csrf-Token': XCsrfToken,
'X-Proxy-Enabled': 'enabled',
},
withCredentials: true,
})
return requester
}
const request = createRequester()
request.interceptors.request.use(function (config) {
const { params } = config
for (const key in params) {
if (isNothing(params[key])) {
delete params[key]
}
}
return { ...config, params }
})
request.interceptors.response.use(async function (response) {
const { data, success, desc } = response.data
if (success) return data
message.error(desc)
return Promise.reject(desc || '服务异常')
})
export default request
// const ssoRequest = createRequester()
// ssoRequest.interceptors.response.use(async function (response) {
// const { success, message: desc, code } = response.data
// if (success) return response.data
// // 无应用访问权限
// if (code === "SSO:01002") return response.data
// // 未登录
// message.error(desc)
// window.location.href = `https://sso.duiba.com.cn/login?systemId=677&redirect=${encodeURIComponent(
// window.location.origin
// )}`
// return response.data
// })
// export { ssoRequest }
/// <reference types="vite/client" />
/// <reference types="vite-plugin-pages/client-react" />
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": [
"src",
"src/main.tsx",
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"auto-imports.d.ts"
]
}
{
"files": [],
"compilerOptions": {
"jsx": "react-jsx",
"sourceMap": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
},
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
import type { Plugin } from 'vite'
// 自定义插件:将 unplugin-auto-import 的重复导入警告转为错误
export function AutoImportError(): Plugin {
return {
name: 'auto-import-error',
configResolved() {
// 在 Vite 配置解析后注入逻辑
const logger = console
const originalWarning = logger.warn
// 劫持unimport的warn输出
logger.warn = (msg: string) => {
// 匹配 unplugin-auto-import 的重复导入警告
const match = msg.match(/Duplicated imports "(.+)", the one from "(.+)" has been ignored and "(.+)" is used/)
if (match) {
// 抛出错误并终止进程
const [, name, path1, path2] = match
const modifiedMsg = `[重复导入错误] ${name}${path1}${path2} 中重复导入`
// 提示:在开发环境下,抛出错误并终止进程
if (process.env.NODE_ENV === 'production') {
throw new Error(modifiedMsg)
}
logger.error(modifiedMsg)
return
}
// 其他警告正常输出
originalWarning.call(logger, msg)
}
},
}
}
import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react-swc'
import AutoImport from 'unplugin-auto-import'
import { defineConfig } from 'vite'
import Pages from 'vite-plugin-pages'
import { AutoImportError } from './vite-plugins/auto-import-error'
// https://vite.dev/config/
export default defineConfig({
server: {
proxy: {
'^/(.*)': {
target: 'http://localhost:10089',
changeOrigin: true,
bypass: (req) => {
const url = req.url
if (req.headers['x-proxy-enabled']) {
return null
}
return url
},
},
},
},
plugins: [
AutoImportError(),
AutoImport.vite({
include: [/\.tsx?$/],
dirs: [
'src/apis/**',
'src/components/common/**',
'src/constants/**',
'src/hooks/**',
'src/utils/**',
'src/stores/**',
],
imports: [
{
antd: ['message', 'Spin', 'Layout', 'Menu', 'theme', 'Button', 'Form', 'Input', 'Card', 'Result'],
axios: [['default', 'axios']],
'pixiu-number-toolkit': ['REGEX'],
},
'react',
'react-router-dom',
'mobx',
'mobx-react-lite',
],
}),
react(),
Pages({
routeStyle: 'next',
importMode: 'async',
dirs: 'src/pages',
exclude: ['**/components/*.ts[x]?', '**/hooks/*.ts[x]?'],
extendRoute(route) {
if (route.path === '/login') {
return route
}
return {
...route,
meta: {
requiresAuth: true,
layout: 'index',
},
}
},
}),
tailwindcss(),
],
})
{
"baseUrl": "https://docs.dui88.com",
"apiMap": [
{
"projectId": "<number>",
"apiPrefix": "/<apiPrefix>",
"token": "09bcbc7dfc228daaf9671db239d876836228791fb2488429c9f57146380fe46f"
}
],
"proxy": {
"targets": [
{
"name": "测试环境",
"target": "https://eye.dui8pre.com"
},
{
"name": "生产环境",
"target": "https://eye.dui88.com"
}
],
"port": 10089
}
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment