Init
This commit is contained in:
40
.eslintrc.cjs
Normal file
40
.eslintrc.cjs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
'./.eslintrc-auto-import.json'
|
||||||
|
],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
project: './tsconfig*.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
'react-refresh',
|
||||||
|
'prettier'],
|
||||||
|
rules: {
|
||||||
|
'no-cond-assign': 'error',
|
||||||
|
'eqeqeq': 'error',
|
||||||
|
'indent': ['error', 4, { 'SwitchCase': 1 }],
|
||||||
|
'prettier/prettier': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
endOfLine: 'auto'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true }
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off'
|
||||||
|
}
|
||||||
|
}
|
||||||
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# 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?
|
||||||
|
|
||||||
|
# Auto generated
|
||||||
|
/auto-imports.d.ts
|
||||||
|
/.eslintrc-auto-import.json
|
||||||
8
.prettierrc.json
Normal file
8
.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
||||||
421
build/resolvers/antd.ts
Normal file
421
build/resolvers/antd.ts
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
export function kebabCase(key: string): string {
|
||||||
|
const result: string = key.replace(/([A-Z])/g, ' $1').trim()
|
||||||
|
return result.split(' ').join('-').toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Awaitable<T> = T | PromiseLike<T>
|
||||||
|
|
||||||
|
export interface ImportInfo {
|
||||||
|
as?: string
|
||||||
|
name?: string
|
||||||
|
from: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SideEffectsInfo = (ImportInfo | string)[] | ImportInfo | string | undefined
|
||||||
|
|
||||||
|
export interface ComponentInfo extends ImportInfo {
|
||||||
|
sideEffects?: SideEffectsInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComponentResolveResult = Awaitable<string | ComponentInfo | null | undefined | void>
|
||||||
|
|
||||||
|
export type ComponentResolverFunction = (name: string) => ComponentResolveResult
|
||||||
|
|
||||||
|
export interface ComponentResolverObject {
|
||||||
|
type: 'component' | 'directive'
|
||||||
|
resolve: ComponentResolverFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComponentResolver = ComponentResolverFunction | ComponentResolverObject
|
||||||
|
|
||||||
|
interface IMatcher {
|
||||||
|
pattern: RegExp
|
||||||
|
styleDir: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchComponents: IMatcher[] = [
|
||||||
|
{
|
||||||
|
pattern: /^Avatar/,
|
||||||
|
styleDir: 'avatar'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^AutoComplete/,
|
||||||
|
styleDir: 'auto-complete'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Anchor/,
|
||||||
|
styleDir: 'anchor'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
pattern: /^Badge/,
|
||||||
|
styleDir: 'badge'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Breadcrumb/,
|
||||||
|
styleDir: 'breadcrumb'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Button/,
|
||||||
|
styleDir: 'button'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Checkbox/,
|
||||||
|
styleDir: 'checkbox'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Card/,
|
||||||
|
styleDir: 'card'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Collapse/,
|
||||||
|
styleDir: 'collapse'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Descriptions/,
|
||||||
|
styleDir: 'descriptions'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^RangePicker|^WeekPicker|^MonthPicker/,
|
||||||
|
styleDir: 'date-picker'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Dropdown/,
|
||||||
|
styleDir: 'dropdown'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
pattern: /^Form/,
|
||||||
|
styleDir: 'form'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^InputNumber/,
|
||||||
|
styleDir: 'input-number'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
pattern: /^Input|^Textarea/,
|
||||||
|
styleDir: 'input'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Statistic/,
|
||||||
|
styleDir: 'statistic'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^CheckableTag/,
|
||||||
|
styleDir: 'tag'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^TimeRangePicker/,
|
||||||
|
styleDir: 'time-picker'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Layout/,
|
||||||
|
styleDir: 'layout'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Menu|^SubMenu/,
|
||||||
|
styleDir: 'menu'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
pattern: /^Table/,
|
||||||
|
styleDir: 'table'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^TimePicker|^TimeRangePicker/,
|
||||||
|
styleDir: 'time-picker'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Radio/,
|
||||||
|
styleDir: 'radio'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
pattern: /^Image/,
|
||||||
|
styleDir: 'image'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
pattern: /^List/,
|
||||||
|
styleDir: 'list'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
pattern: /^Tab/,
|
||||||
|
styleDir: 'tabs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Mentions/,
|
||||||
|
styleDir: 'mentions'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
pattern: /^Step/,
|
||||||
|
styleDir: 'steps'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Skeleton/,
|
||||||
|
styleDir: 'skeleton'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
pattern: /^Select/,
|
||||||
|
styleDir: 'select'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^TreeSelect/,
|
||||||
|
styleDir: 'tree-select'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Tree|^DirectoryTree/,
|
||||||
|
styleDir: 'tree'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Typography/,
|
||||||
|
styleDir: 'typography'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Timeline/,
|
||||||
|
styleDir: 'timeline'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^Upload/,
|
||||||
|
styleDir: 'upload'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export interface AntDesignResolverOptions {
|
||||||
|
/**
|
||||||
|
* exclude components that do not require automatic import
|
||||||
|
*
|
||||||
|
* @default []
|
||||||
|
*/
|
||||||
|
exclude?: string[]
|
||||||
|
/**
|
||||||
|
* import style along with components
|
||||||
|
*
|
||||||
|
* @default 'css'
|
||||||
|
*/
|
||||||
|
importStyle?: boolean | 'css' | 'less'
|
||||||
|
/**
|
||||||
|
* resolve `antd' icons
|
||||||
|
*
|
||||||
|
* requires package `@ant-design/icons-vue`
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
resolveIcons?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use `importStyle: 'css'` instead
|
||||||
|
*/
|
||||||
|
importCss?: boolean
|
||||||
|
/**
|
||||||
|
* @deprecated use `importStyle: 'less'` instead
|
||||||
|
*/
|
||||||
|
importLess?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* use commonjs build default false
|
||||||
|
*/
|
||||||
|
cjs?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* rename package
|
||||||
|
*
|
||||||
|
* @default 'antd'
|
||||||
|
*/
|
||||||
|
packageName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyleDir = (compName: string): string => {
|
||||||
|
for (const matchComponent of matchComponents) {
|
||||||
|
if (compName.match(matchComponent.pattern)) {
|
||||||
|
return matchComponent.styleDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return kebabCase(compName)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSideEffects = (compName: string, options: AntDesignResolverOptions): SideEffectsInfo => {
|
||||||
|
const { importStyle = true } = options
|
||||||
|
|
||||||
|
if (!importStyle) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const lib = options.cjs ? 'lib' : 'es'
|
||||||
|
const packageName = options?.packageName || 'antd'
|
||||||
|
|
||||||
|
const styleDir = getStyleDir(compName)
|
||||||
|
return `${packageName}/${lib}/${styleDir}/style`
|
||||||
|
}
|
||||||
|
|
||||||
|
const primitiveNames = [
|
||||||
|
'Affix',
|
||||||
|
'Anchor',
|
||||||
|
'AnchorLink',
|
||||||
|
'AutoComplete',
|
||||||
|
'AutoCompleteOptGroup',
|
||||||
|
'AutoCompleteOption',
|
||||||
|
'Alert',
|
||||||
|
'Avatar',
|
||||||
|
'AvatarGroup',
|
||||||
|
'BackTop',
|
||||||
|
'Badge',
|
||||||
|
'BadgeRibbon',
|
||||||
|
'Breadcrumb',
|
||||||
|
'BreadcrumbItem',
|
||||||
|
'BreadcrumbSeparator',
|
||||||
|
'Button',
|
||||||
|
'ButtonGroup',
|
||||||
|
'Calendar',
|
||||||
|
'Card',
|
||||||
|
'CardGrid',
|
||||||
|
'CardMeta',
|
||||||
|
'Collapse',
|
||||||
|
'CollapsePanel',
|
||||||
|
'Carousel',
|
||||||
|
'Cascader',
|
||||||
|
'Checkbox',
|
||||||
|
'CheckboxGroup',
|
||||||
|
'Col',
|
||||||
|
'Comment',
|
||||||
|
'ConfigProvider',
|
||||||
|
'DatePicker',
|
||||||
|
'MonthPicker',
|
||||||
|
'WeekPicker',
|
||||||
|
'RangePicker',
|
||||||
|
'QuarterPicker',
|
||||||
|
'Descriptions',
|
||||||
|
'DescriptionsItem',
|
||||||
|
'Divider',
|
||||||
|
'Dropdown',
|
||||||
|
'DropdownButton',
|
||||||
|
'Drawer',
|
||||||
|
'Empty',
|
||||||
|
'Form',
|
||||||
|
'FormItem',
|
||||||
|
'FormItemRest',
|
||||||
|
'Grid',
|
||||||
|
'Input',
|
||||||
|
'InputGroup',
|
||||||
|
'InputPassword',
|
||||||
|
'InputSearch',
|
||||||
|
'Textarea',
|
||||||
|
'Image',
|
||||||
|
'ImagePreviewGroup',
|
||||||
|
'InputNumber',
|
||||||
|
'Layout',
|
||||||
|
'LayoutHeader',
|
||||||
|
'LayoutSider',
|
||||||
|
'LayoutFooter',
|
||||||
|
'LayoutContent',
|
||||||
|
'List',
|
||||||
|
'ListItem',
|
||||||
|
'ListItemMeta',
|
||||||
|
'Menu',
|
||||||
|
'MenuDivider',
|
||||||
|
'MenuItem',
|
||||||
|
'MenuItemGroup',
|
||||||
|
'SubMenu',
|
||||||
|
'Mentions',
|
||||||
|
'MentionsOption',
|
||||||
|
'Modal',
|
||||||
|
'Statistic',
|
||||||
|
'StatisticCountdown',
|
||||||
|
'PageHeader',
|
||||||
|
'Pagination',
|
||||||
|
'Popconfirm',
|
||||||
|
'Popover',
|
||||||
|
'Progress',
|
||||||
|
'Radio',
|
||||||
|
'RadioButton',
|
||||||
|
'RadioGroup',
|
||||||
|
'Rate',
|
||||||
|
'Result',
|
||||||
|
'Row',
|
||||||
|
'Select',
|
||||||
|
'SelectOptGroup',
|
||||||
|
'SelectOption',
|
||||||
|
'Skeleton',
|
||||||
|
'SkeletonButton',
|
||||||
|
'SkeletonAvatar',
|
||||||
|
'SkeletonInput',
|
||||||
|
'SkeletonImage',
|
||||||
|
'Slider',
|
||||||
|
'Space',
|
||||||
|
'Spin',
|
||||||
|
'Steps',
|
||||||
|
'Step',
|
||||||
|
'Switch',
|
||||||
|
'Table',
|
||||||
|
'TableColumn',
|
||||||
|
'TableColumnGroup',
|
||||||
|
'TableSummary',
|
||||||
|
'TableSummaryRow',
|
||||||
|
'TableSummaryCell',
|
||||||
|
'Transfer',
|
||||||
|
'Tree',
|
||||||
|
'TreeNode',
|
||||||
|
'DirectoryTree',
|
||||||
|
'TreeSelect',
|
||||||
|
'TreeSelectNode',
|
||||||
|
'Tabs',
|
||||||
|
'TabPane',
|
||||||
|
'Tag',
|
||||||
|
'CheckableTag',
|
||||||
|
'TimePicker',
|
||||||
|
'TimeRangePicker',
|
||||||
|
'Timeline',
|
||||||
|
'TimelineItem',
|
||||||
|
'Tooltip',
|
||||||
|
'Typography',
|
||||||
|
'TypographyLink',
|
||||||
|
'TypographyParagraph',
|
||||||
|
'TypographyText',
|
||||||
|
'TypographyTitle',
|
||||||
|
'Upload',
|
||||||
|
'UploadDragger',
|
||||||
|
'LocaleProvider'
|
||||||
|
]
|
||||||
|
|
||||||
|
const prefix = 'Antd'
|
||||||
|
|
||||||
|
let antdNames: Set<string>
|
||||||
|
|
||||||
|
const genAntdNames = (primitiveNames: string[]): void => {
|
||||||
|
antdNames = new Set(primitiveNames.map((name) => `${prefix}${name}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
genAntdNames(primitiveNames)
|
||||||
|
|
||||||
|
const isAntd = (compName: string): boolean => {
|
||||||
|
return antdNames.has(compName)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AntDesignResolver(options: AntDesignResolverOptions = {}): ComponentResolver {
|
||||||
|
return {
|
||||||
|
type: 'component',
|
||||||
|
resolve: (name: string) => {
|
||||||
|
if (options.resolveIcons && name.match(/(Outlined|Filled|TwoTone)$/)) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
from: '@ant-design/icons'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAntd(name) && !options?.exclude?.includes(name)) {
|
||||||
|
const importName = name.slice(prefix.length)
|
||||||
|
const { cjs = false, packageName = 'antd' } = options
|
||||||
|
const path = `${packageName}/${cjs ? 'lib' : 'es'}`
|
||||||
|
return {
|
||||||
|
name: importName,
|
||||||
|
from: path,
|
||||||
|
sideEffects: getSideEffects(importName, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<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>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
55
package.json
Normal file
55
package.json
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"name": "fatweb-ui",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"dev-host": "vite --host 0.0.0.0",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"format": "prettier --write src/",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.2.5",
|
||||||
|
"antd": "^5.8.5",
|
||||||
|
"axios": "^1.5.0",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
|
"localforage": "^1.10.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"match-sorter": "^6.3.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router": "^6.15.0",
|
||||||
|
"react-router-dom": "^6.15.0",
|
||||||
|
"sort-by": "^0.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@svgr/core": "^8.1.0",
|
||||||
|
"@svgr/plugin-jsx": "^8.1.0",
|
||||||
|
"@types/jsdom": "^21.1.2",
|
||||||
|
"@types/lodash": "^4.14.197",
|
||||||
|
"@types/node": "^20.5.7",
|
||||||
|
"@types/react": "^18.2.21",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.5.0",
|
||||||
|
"@typescript-eslint/parser": "^6.5.0",
|
||||||
|
"@vitejs/plugin-react": "^4.0.4",
|
||||||
|
"eslint": "^8.48.0",
|
||||||
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-config-standard-with-typescript": "^39.0.0",
|
||||||
|
"eslint-plugin-import": "^2.28.1",
|
||||||
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
|
"eslint-plugin-promise": "^6.1.1",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
|
"jsdom": "^22.1.0",
|
||||||
|
"prettier": "^3.0.3",
|
||||||
|
"stylelint-config-prettier": "^9.0.5",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"unplugin-auto-import": "^0.16.6",
|
||||||
|
"unplugin-icons": "^0.16.6",
|
||||||
|
"vite": "^4.4.9"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
42
src/App.css
Normal file
42
src/App.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
12
src/App.tsx
Normal file
12
src/App.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
|
const App: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
20
src/AuthRoute.tsx
Normal file
20
src/AuthRoute.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { getLoginStatus } from '@/utils/auth.ts'
|
||||||
|
|
||||||
|
const AuthRoute = () => {
|
||||||
|
const match = useMatches()[1]
|
||||||
|
const handle = match.handle as RouteHandle
|
||||||
|
const outlet = useOutlet()
|
||||||
|
const isLogin = getLoginStatus()
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (handle?.auth && !isLogin) {
|
||||||
|
return <Navigate to="/login" />
|
||||||
|
}
|
||||||
|
if (isLogin && match.pathname === '/login') {
|
||||||
|
return <Navigate to="/" />
|
||||||
|
}
|
||||||
|
return outlet
|
||||||
|
}, [handle?.auth, isLogin, match.pathname, outlet])
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthRoute
|
||||||
63
src/assets/css/base.css
Normal file
63
src/assets/css/base.css
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
em,
|
||||||
|
i {
|
||||||
|
font-style: normal
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
list-style: none
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
vertical-align: middle
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #666;
|
||||||
|
text-decoration: none
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
font-family: Microsoft YaHei, Heiti SC, tahoma, arial, Hiragino Sans GB, "\5B8B\4F53", sans-serif;
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
background-color: #fff;
|
||||||
|
font: 12px/1.5 Microsoft YaHei, Heiti SC, tahoma, arial, Hiragino Sans GB, "\5B8B\4F53", sans-serif;
|
||||||
|
color: #666
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide,
|
||||||
|
.none {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearfix:after {
|
||||||
|
visibility: hidden;
|
||||||
|
clear: both;
|
||||||
|
display: block;
|
||||||
|
content: ".";
|
||||||
|
height: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearfix {
|
||||||
|
*zoom: 1
|
||||||
|
}
|
||||||
106
src/assets/css/common.css
Normal file
106
src/assets/css/common.css
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
:root {
|
||||||
|
--main-color: #00D4FF;
|
||||||
|
--background-color: #F5F5F5;
|
||||||
|
--font-main-color: #4D4D4D;
|
||||||
|
--font-secondary-color: #9E9E9E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
color: var(--font-main-color);
|
||||||
|
user-select: none;
|
||||||
|
min-width: 1800px;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-with {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-height {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-white {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-box {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-center-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-center-box {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-size-xs {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-size-xs > use {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-size-sm {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-size-sm > use {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-size-md {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-size-md > use {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-size-lg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-size-lg > use {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-size-xl {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-size-xl > use {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-size-menu {
|
||||||
|
width: 23px;
|
||||||
|
height: 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-size-menu > use {
|
||||||
|
width: 23px;
|
||||||
|
height: 23px;
|
||||||
|
}
|
||||||
63
src/assets/css/login.css
Normal file
63
src/assets/css/login.css
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
.login-background {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #B3E5FC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box {
|
||||||
|
display: flex;
|
||||||
|
width: 800px;
|
||||||
|
height: 450px;
|
||||||
|
background-color: #448AFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box-left {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box-left-text {
|
||||||
|
font-size: 3rem;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box-left-text>div:last-child {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box-right {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
flex: 3;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-from-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 60px;
|
||||||
|
left: 40px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-from {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-from-item {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
35
src/assets/css/manager.css
Normal file
35
src/assets/css/manager.css
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
.top-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
background-color: #317ece;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar > button:hover {
|
||||||
|
color: #F5F5F5;
|
||||||
|
border-color: #F5F5F5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin: 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-row > * {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-row > *:not(.operation-buttons) > *:last-child {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
1
src/assets/svg/home.svg
Normal file
1
src/assets/svg/home.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 24 24"><g style="mix-blend-mode:passthrough"><g style="mix-blend-mode:passthrough"><path d="M11.2633,0.229798C11.6966,-0.0765992,12.3034,-0.0765992,12.7367,0.229798C12.7367,0.229798,23.5367,7.86616,23.5367,7.86616C23.829,8.07284,24,8.39063,24,8.72727C24,8.72727,24,20.7273,24,20.7273C24,21.5953,23.6207,22.4277,22.9456,23.0414C22.2704,23.6552,21.3548,24,20.4,24C20.4,24,3.6,24,3.6,24C2.64522,24,1.72955,23.6552,1.05442,23.0414C0.379284,22.4277,0,21.5953,0,20.7273C0,20.7273,0,8.72727,0,8.72727C0,8.39063,0.170968,8.07284,0.463271,7.86616C0.463271,7.86616,11.2633,0.229798,11.2633,0.229798C11.2633,0.229798,11.2633,0.229798,11.2633,0.229798ZM9.6,21.8182C9.6,21.8182,14.4,21.8182,14.4,21.8182C14.4,21.8182,14.4,13.0909,14.4,13.0909C14.4,13.0909,9.6,13.0909,9.6,13.0909C9.6,13.0909,9.6,21.8182,9.6,21.8182C9.6,21.8182,9.6,21.8182,9.6,21.8182ZM16.8,21.8182C16.8,21.8182,16.8,12,16.8,12C16.8,11.3975,16.2628,10.9091,15.6,10.9091C15.6,10.9091,8.4,10.9091,8.4,10.9091C7.73726,10.9091,7.2,11.3975,7.2,12C7.2,12,7.2,21.8182,7.2,21.8182C7.2,21.8182,3.6,21.8182,3.6,21.8182C3.28174,21.8182,2.97652,21.7032,2.75147,21.4987C2.52643,21.2941,2.4,21.0166,2.4,20.7273C2.4,20.7273,2.4,9.26081,2.4,9.26081C2.4,9.26081,12,2.47294,12,2.47294C12,2.47294,21.6,9.26081,21.6,9.26081C21.6,9.26081,21.6,20.7273,21.6,20.7273C21.6,21.0166,21.4735,21.2941,21.2485,21.4987C21.0235,21.7032,20.7182,21.8182,20.4,21.8182C20.4,21.8182,16.8,21.8182,16.8,21.8182C16.8,21.8182,16.8,21.8182,16.8,21.8182Z" fill-rule="evenodd" fill-opacity="1"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
85
src/constants/Common.constants.ts
Normal file
85
src/constants/Common.constants.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
const PRODUCTION_NAME = 'Pinnacle OA'
|
||||||
|
const TOKEN_NAME = 'JWT_TOKEN'
|
||||||
|
const COLOR_PRODUCTION = '#00D4FF'
|
||||||
|
const COLOR_BACKGROUND = '#F5F5F5'
|
||||||
|
const COLOR_TOP = 'rgba(234,46,13,0.85)'
|
||||||
|
const COLOR_FONT_MAIN = '#4D4D4D'
|
||||||
|
const COLOR_FONT_SECONDARY = '#9E9E9E'
|
||||||
|
const SIZE_ICON_XS = '16px'
|
||||||
|
const SIZE_ICON_SM = '20px'
|
||||||
|
const SIZE_ICON_MD = '24px'
|
||||||
|
const SIZE_ICON_LG = '32px'
|
||||||
|
const SIZE_ICON_XL = '64px'
|
||||||
|
|
||||||
|
// Response Code
|
||||||
|
const SYSTEM_OK = 20000
|
||||||
|
const LOGIN_SUCCESS = 20010
|
||||||
|
const LOGIN_USERNAME_PASSWORD_ERROR = 20011
|
||||||
|
const OLD_PASSWORD_NOT_MATCH = 20012
|
||||||
|
const LOGOUT_SUCCESS = 20015
|
||||||
|
const LOGOUT_FAILED = 20016
|
||||||
|
const TOKEN_IS_ILLEGAL = 20017
|
||||||
|
const TOKEN_HAS_EXPIRED = 20018
|
||||||
|
const TOKEN_RENEW_SUCCESS = 20019
|
||||||
|
const DATABASE_SELECT_OK = 20021
|
||||||
|
const DATABASE_SAVE_OK = 20022
|
||||||
|
const DATABASE_UPDATE_OK = 20023
|
||||||
|
const DATABASE_DELETE_OK = 20024
|
||||||
|
const DATABASE_SELECT_ERROR = 20031
|
||||||
|
const DATABASE_SAVE_ERROR = 20032
|
||||||
|
const DATABASE_UPDATE_ERROR = 20033
|
||||||
|
const DATABASE_DELETE_ERROR = 20034
|
||||||
|
const DATABASE_TIMEOUT_ERROR = 20035
|
||||||
|
const DATABASE_CONNECT_ERROR = 20036
|
||||||
|
const DATABASE_DATA_TO_LONG = 20037
|
||||||
|
const DATABASE_DATA_VALIDATION_FAILED = 20038
|
||||||
|
const DATABASE_EXECUTE_ERROR = 20039
|
||||||
|
|
||||||
|
const UNAUTHORIZED = 30010
|
||||||
|
const ACCESS_DENIED = 30030
|
||||||
|
const USER_DISABLE = 30031
|
||||||
|
|
||||||
|
const SYSTEM_ERROR = 50001
|
||||||
|
const SYSTEM_TIMEOUT = 50002
|
||||||
|
|
||||||
|
export {
|
||||||
|
PRODUCTION_NAME,
|
||||||
|
TOKEN_NAME,
|
||||||
|
COLOR_PRODUCTION,
|
||||||
|
COLOR_BACKGROUND,
|
||||||
|
COLOR_FONT_MAIN,
|
||||||
|
COLOR_FONT_SECONDARY,
|
||||||
|
COLOR_TOP,
|
||||||
|
SIZE_ICON_XS,
|
||||||
|
SIZE_ICON_SM,
|
||||||
|
SIZE_ICON_MD,
|
||||||
|
SIZE_ICON_LG,
|
||||||
|
SIZE_ICON_XL,
|
||||||
|
SYSTEM_OK,
|
||||||
|
LOGIN_SUCCESS,
|
||||||
|
LOGIN_USERNAME_PASSWORD_ERROR,
|
||||||
|
OLD_PASSWORD_NOT_MATCH,
|
||||||
|
LOGOUT_SUCCESS,
|
||||||
|
LOGOUT_FAILED,
|
||||||
|
TOKEN_IS_ILLEGAL,
|
||||||
|
TOKEN_HAS_EXPIRED,
|
||||||
|
TOKEN_RENEW_SUCCESS,
|
||||||
|
DATABASE_SELECT_OK,
|
||||||
|
DATABASE_SAVE_OK,
|
||||||
|
DATABASE_UPDATE_OK,
|
||||||
|
DATABASE_DELETE_OK,
|
||||||
|
DATABASE_SELECT_ERROR,
|
||||||
|
DATABASE_SAVE_ERROR,
|
||||||
|
DATABASE_UPDATE_ERROR,
|
||||||
|
DATABASE_DELETE_ERROR,
|
||||||
|
DATABASE_TIMEOUT_ERROR,
|
||||||
|
DATABASE_CONNECT_ERROR,
|
||||||
|
DATABASE_DATA_TO_LONG,
|
||||||
|
DATABASE_DATA_VALIDATION_FAILED,
|
||||||
|
DATABASE_EXECUTE_ERROR,
|
||||||
|
UNAUTHORIZED,
|
||||||
|
ACCESS_DENIED,
|
||||||
|
USER_DISABLE,
|
||||||
|
SYSTEM_ERROR,
|
||||||
|
SYSTEM_TIMEOUT
|
||||||
|
}
|
||||||
69
src/index.css
Normal file
69
src/index.css
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/main.tsx
Normal file
14
src/main.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App.tsx'
|
||||||
|
import '@/assets/css/base.css'
|
||||||
|
import '@/assets/css/common.css'
|
||||||
|
import zh_CN from 'antd/locale/zh_CN'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<AntdConfigProvider locale={zh_CN}>
|
||||||
|
<App />
|
||||||
|
</AntdConfigProvider>
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
||||||
11
src/pages/Home.tsx
Normal file
11
src/pages/Home.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const Home: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>FatWeb</h1>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home
|
||||||
106
src/pages/Login.tsx
Normal file
106
src/pages/Login.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { login } from '@/utils/auth.ts'
|
||||||
|
import { LOGIN_SUCCESS, LOGIN_USERNAME_PASSWORD_ERROR } from '@/constants/Common.constants.ts'
|
||||||
|
import { setToken } from '@/utils/common.ts'
|
||||||
|
import '@/assets/css/login.css'
|
||||||
|
|
||||||
|
const Login: React.FC = () => {
|
||||||
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [isLoggingIn, setIsLoggingIn] = useState(false)
|
||||||
|
|
||||||
|
const onFinish = (values: LoginForm) => {
|
||||||
|
setIsLoggingIn(true)
|
||||||
|
void login(values.username, values.password).then((value) => {
|
||||||
|
const res = value.data
|
||||||
|
const { code, data } = res
|
||||||
|
switch (code) {
|
||||||
|
case LOGIN_SUCCESS:
|
||||||
|
setToken(data?.token ?? '')
|
||||||
|
void messageApi.success('登录成功')
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate('/')
|
||||||
|
}, 1500)
|
||||||
|
break
|
||||||
|
case LOGIN_USERNAME_PASSWORD_ERROR:
|
||||||
|
void messageApi.error(
|
||||||
|
<>
|
||||||
|
<strong>用户名</strong>或<strong>密码</strong>错误,请重试
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
setIsLoggingIn(false)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
void messageApi.error(
|
||||||
|
<>
|
||||||
|
<strong>服务器出错了</strong>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
setIsLoggingIn(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{contextHolder}
|
||||||
|
<div className={'login-background'}>
|
||||||
|
<div className={'login-box'}>
|
||||||
|
<div className={'login-box-left'}>
|
||||||
|
<div className={'login-box-left-text'}>
|
||||||
|
<div>欢迎回来</div>
|
||||||
|
<div>Welcome back</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={'login-box-right'}>
|
||||||
|
<div className={'login-from-text'}>
|
||||||
|
<span>登 录</span>
|
||||||
|
</div>
|
||||||
|
<AntdForm
|
||||||
|
name="login-form"
|
||||||
|
autoComplete="on"
|
||||||
|
onFinish={onFinish}
|
||||||
|
className={'login-from'}
|
||||||
|
>
|
||||||
|
<AntdForm.Item
|
||||||
|
className={'login-from-item'}
|
||||||
|
name={'username'}
|
||||||
|
rules={[{ required: true, message: '用户名为空' }]}
|
||||||
|
>
|
||||||
|
<AntdInput
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
placeholder={'用户名'}
|
||||||
|
disabled={isLoggingIn}
|
||||||
|
/>
|
||||||
|
</AntdForm.Item>
|
||||||
|
<AntdForm.Item
|
||||||
|
className={'login-from-item'}
|
||||||
|
name={'password'}
|
||||||
|
rules={[{ required: true, message: '密码为空' }]}
|
||||||
|
>
|
||||||
|
<AntdInput.Password
|
||||||
|
prefix={<LockOutlined />}
|
||||||
|
placeholder={'密码'}
|
||||||
|
disabled={isLoggingIn}
|
||||||
|
/>
|
||||||
|
</AntdForm.Item>
|
||||||
|
<AntdForm.Item className={'login-from-item'}>
|
||||||
|
<AntdButton
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
type={'primary'}
|
||||||
|
htmlType={'submit'}
|
||||||
|
disabled={isLoggingIn}
|
||||||
|
loading={isLoggingIn}
|
||||||
|
>
|
||||||
|
登    录
|
||||||
|
</AntdButton>
|
||||||
|
</AntdForm.Item>
|
||||||
|
</AntdForm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login
|
||||||
37
src/router/index.tsx
Normal file
37
src/router/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const routes: RouteObject[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
Component: React.lazy(() => import('@/AuthRoute')),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
id: 'login',
|
||||||
|
Component: React.lazy(() => import('@/pages/Login'))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
id: 'manager',
|
||||||
|
Component: React.lazy(() => import('@/pages/Home')),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'manager-sub',
|
||||||
|
path: 'sub',
|
||||||
|
Component: React.lazy(() => import('@/pages/Home'))
|
||||||
|
}
|
||||||
|
],
|
||||||
|
handle: {
|
||||||
|
auth: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '*',
|
||||||
|
element: <Navigate to="/" replace />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createBrowserRouter(routes)
|
||||||
|
export default router
|
||||||
155
src/services/index.tsx
Normal file
155
src/services/index.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import axios, { type AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
|
import jwtDecode, { JwtPayload } from 'jwt-decode'
|
||||||
|
import { clearLocalStorage, getToken, setToken } from '@/utils/common'
|
||||||
|
import {
|
||||||
|
ACCESS_DENIED,
|
||||||
|
DATABASE_DATA_TO_LONG,
|
||||||
|
DATABASE_DATA_VALIDATION_FAILED,
|
||||||
|
DATABASE_EXECUTE_ERROR,
|
||||||
|
TOKEN_HAS_EXPIRED,
|
||||||
|
TOKEN_IS_ILLEGAL,
|
||||||
|
TOKEN_RENEW_SUCCESS,
|
||||||
|
UNAUTHORIZED
|
||||||
|
} from '@/constants/Common.constants'
|
||||||
|
import { message } from 'antd'
|
||||||
|
|
||||||
|
const service: AxiosInstance = axios.create({
|
||||||
|
baseURL: 'http://localhost:8181',
|
||||||
|
timeout: 10000,
|
||||||
|
withCredentials: false
|
||||||
|
})
|
||||||
|
|
||||||
|
service.defaults.paramsSerializer = (params: Record<string, string>) => {
|
||||||
|
return Object.keys(params)
|
||||||
|
.filter((it) => {
|
||||||
|
return Object.prototype.hasOwnProperty.call(params, it)
|
||||||
|
})
|
||||||
|
.reduce((pre, curr) => {
|
||||||
|
return params[curr] !== null
|
||||||
|
? (pre !== '' ? pre + '&' : '') + curr + '=' + encodeURIComponent(params[curr])
|
||||||
|
: pre
|
||||||
|
}, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
service.interceptors.request.use(
|
||||||
|
async (config) => {
|
||||||
|
let token = getToken()
|
||||||
|
if (token !== null) {
|
||||||
|
const jwt = jwtDecode<JwtPayload>(token)
|
||||||
|
if (!jwt.exp) {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
jwt.exp * 1000 - new Date().getTime() < 1200000 &&
|
||||||
|
jwt.exp * 1000 - new Date().getTime() > 0
|
||||||
|
) {
|
||||||
|
await axios
|
||||||
|
.get('http://localhost:8181/token', {
|
||||||
|
headers: { token }
|
||||||
|
})
|
||||||
|
.then((value: AxiosResponse<_Response<Token>>) => {
|
||||||
|
const response = value.data
|
||||||
|
if (response.code === TOKEN_RENEW_SUCCESS) {
|
||||||
|
setToken(response.data?.token ?? '')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
token = getToken()
|
||||||
|
config.headers.set('token', token)
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
async (error) => {
|
||||||
|
return await Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
service.interceptors.response.use(
|
||||||
|
(response: AxiosResponse<_Response<never>>) => {
|
||||||
|
switch (response.data.code) {
|
||||||
|
case UNAUTHORIZED:
|
||||||
|
case TOKEN_IS_ILLEGAL:
|
||||||
|
case TOKEN_HAS_EXPIRED:
|
||||||
|
clearLocalStorage()
|
||||||
|
void message.error(
|
||||||
|
<>
|
||||||
|
<strong>登录已过期</strong>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
setTimeout(function () {
|
||||||
|
location.reload()
|
||||||
|
}, 1500)
|
||||||
|
throw response?.data
|
||||||
|
case ACCESS_DENIED:
|
||||||
|
void message.error(
|
||||||
|
<>
|
||||||
|
<strong>暂无权限操作</strong>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
throw response?.data
|
||||||
|
case DATABASE_DATA_TO_LONG:
|
||||||
|
void message.error(
|
||||||
|
<>
|
||||||
|
<strong>数据过长</strong>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
throw response?.data
|
||||||
|
case DATABASE_DATA_VALIDATION_FAILED:
|
||||||
|
void message.error(
|
||||||
|
<>
|
||||||
|
<strong>数据验证失败</strong>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
throw response?.data
|
||||||
|
case DATABASE_EXECUTE_ERROR:
|
||||||
|
void message.error(
|
||||||
|
<>
|
||||||
|
<strong>数据库执行出错</strong>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
throw response?.data
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
async (error: AxiosError) => {
|
||||||
|
void message.error(
|
||||||
|
<>
|
||||||
|
<strong>服务器出错</strong>,请稍后重试
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
return await Promise.reject(error?.response?.data)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
async get<T>(url: string, data?: object): Promise<AxiosResponse<_Response<T>>> {
|
||||||
|
return await request.request('GET', url, { params: data })
|
||||||
|
},
|
||||||
|
async post<T>(url: string, data?: object): Promise<AxiosResponse<_Response<T>>> {
|
||||||
|
return await request.request('POST', url, { data })
|
||||||
|
},
|
||||||
|
async put<T>(url: string, data?: object): Promise<AxiosResponse<_Response<T>>> {
|
||||||
|
return await request.request('PUT', url, { data })
|
||||||
|
},
|
||||||
|
async delete<T>(url: string, data?: object): Promise<AxiosResponse<_Response<T>>> {
|
||||||
|
return await request.request('DELETE', url, { params: data })
|
||||||
|
},
|
||||||
|
async request<T>(
|
||||||
|
method = 'GET',
|
||||||
|
url: string,
|
||||||
|
data?: AxiosRequestConfig
|
||||||
|
): Promise<AxiosResponse<_Response<T>>> {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
service({ method, url, ...data })
|
||||||
|
.then((res) => {
|
||||||
|
resolve(res as unknown as Promise<AxiosResponse<_Response<T>>>)
|
||||||
|
})
|
||||||
|
.catch((e: Error | AxiosError) => {
|
||||||
|
reject(e)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default request
|
||||||
65
src/utils/auth.ts
Normal file
65
src/utils/auth.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { clearLocalStorage, getCaptcha, getLocalStorage, setLocalStorage } from './common'
|
||||||
|
import { DATABASE_SELECT_OK, TOKEN_NAME } from '@/constants/Common.constants'
|
||||||
|
import request from '@/services'
|
||||||
|
|
||||||
|
let captcha: Captcha
|
||||||
|
|
||||||
|
export async function login(username: string, password: string) {
|
||||||
|
return await request.post<Token>('/login', {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout(): void {
|
||||||
|
void request.get('/logout').finally(() => {
|
||||||
|
clearLocalStorage()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLoginStatus(): boolean {
|
||||||
|
return getLocalStorage(TOKEN_NAME) !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUser(): Promise<User> {
|
||||||
|
if (getLocalStorage('userInfo') !== null) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve(JSON.parse(getLocalStorage('userInfo') as string) as User)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return requestUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestUser(): Promise<User> {
|
||||||
|
let user: User | null
|
||||||
|
|
||||||
|
await request.get<User>('/user/info').then((value) => {
|
||||||
|
const response = value.data
|
||||||
|
if (response.code === DATABASE_SELECT_OK) {
|
||||||
|
user = response.data
|
||||||
|
setLocalStorage('userInfo', JSON.stringify(user))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Promise<User>((resolve, reject) => {
|
||||||
|
if (user) {
|
||||||
|
resolve(user)
|
||||||
|
}
|
||||||
|
reject(user)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUsername(): Promise<string> {
|
||||||
|
const user = await getUser()
|
||||||
|
|
||||||
|
return user.username
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCaptchaSrc(): string {
|
||||||
|
captcha = getCaptcha(300, 150, 4)
|
||||||
|
return captcha.base64Src
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyCaptcha(value: string): boolean {
|
||||||
|
return captcha.value.toLowerCase() === value.replace(/\s*/g, '').toLowerCase()
|
||||||
|
}
|
||||||
136
src/utils/common.ts
Normal file
136
src/utils/common.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { TOKEN_NAME } from '@/constants/Common.constants'
|
||||||
|
|
||||||
|
export function getQueryVariable(variable: string): string | null {
|
||||||
|
const query = window.location.search.substring(1)
|
||||||
|
const vars = query.split('&')
|
||||||
|
for (const value of vars) {
|
||||||
|
const pair = value.split('=')
|
||||||
|
if (pair[0] === variable) {
|
||||||
|
return decodeURIComponent(pair[1].replace(/\+/g, ' '))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCookie(
|
||||||
|
name: string,
|
||||||
|
value: string,
|
||||||
|
daysToLive: number | null,
|
||||||
|
path: string | null
|
||||||
|
): void {
|
||||||
|
let cookie = name + '=' + encodeURIComponent(value)
|
||||||
|
|
||||||
|
if (typeof daysToLive === 'number') {
|
||||||
|
cookie = `${cookie}; max-age=${daysToLive * 24 * 60 * 60}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof path === 'string') {
|
||||||
|
cookie += '; path=' + path
|
||||||
|
}
|
||||||
|
|
||||||
|
document.cookie = cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLocalStorage(name: string, value: string): void {
|
||||||
|
localStorage.setItem(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setToken(token: string): void {
|
||||||
|
setLocalStorage(TOKEN_NAME, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCookie(name: string): string | null {
|
||||||
|
const cookieArr = document.cookie.split(';')
|
||||||
|
|
||||||
|
for (const cookie of cookieArr) {
|
||||||
|
const cookiePair = cookie.split('=')
|
||||||
|
if (cookiePair[0].trim() === name) {
|
||||||
|
return decodeURIComponent(cookiePair[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocalStorage(name: string): string | null {
|
||||||
|
return localStorage.getItem(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToken(): string | null {
|
||||||
|
return getLocalStorage(TOKEN_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeCookie(name: string): void {
|
||||||
|
document.cookie = name + '=; max-age=0'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeLocalStorage(name: string): void {
|
||||||
|
localStorage.removeItem(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeToken(): void {
|
||||||
|
removeLocalStorage(TOKEN_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearLocalStorage(): void {
|
||||||
|
localStorage.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCaptcha(width: number, high: number, num: number): Captcha {
|
||||||
|
const CHARTS = '23456789ABCDEFGHJKLMNPRSTUVWXYZabcdefghijklmnpqrstuvwxyz'.split('')
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
|
||||||
|
|
||||||
|
ctx.rect(0, 0, width, high)
|
||||||
|
ctx.clip()
|
||||||
|
|
||||||
|
ctx.fillStyle = randomColor(200, 250)
|
||||||
|
ctx.fillRect(0, 0, width, high)
|
||||||
|
|
||||||
|
for (let i = 0.05 * width * high; i > 0; i--) {
|
||||||
|
ctx.fillStyle = randomColor(0, 256)
|
||||||
|
ctx.fillRect(randomInt(0, width), randomInt(0, high), 1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.font = `${high - 4}px Consolas`
|
||||||
|
ctx.fillStyle = randomColor(160, 200)
|
||||||
|
let value = ''
|
||||||
|
for (let i = 0; i < num; i++) {
|
||||||
|
const x = ((width - 10) / num) * i + 5
|
||||||
|
const y = high - 12
|
||||||
|
const r = Math.PI * randomFloat(-0.12, 0.12)
|
||||||
|
const ch = CHARTS[randomInt(0, CHARTS.length)]
|
||||||
|
value += ch
|
||||||
|
ctx.translate(x, y)
|
||||||
|
ctx.rotate(r)
|
||||||
|
ctx.fillText(ch, 0, 0)
|
||||||
|
ctx.rotate(-r)
|
||||||
|
ctx.translate(-x, -y)
|
||||||
|
}
|
||||||
|
|
||||||
|
const base64Src = canvas.toDataURL('image/jpg')
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
base64Src
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomInt(start: number, end: number): number {
|
||||||
|
if (start > end) {
|
||||||
|
const t = start
|
||||||
|
start = end
|
||||||
|
end = t
|
||||||
|
}
|
||||||
|
start = Math.ceil(start)
|
||||||
|
end = Math.floor(end)
|
||||||
|
return start + Math.floor(Math.random() * (end - start))
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomFloat(start: number, end: number): number {
|
||||||
|
return start + Math.random() * (end - start)
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomColor(start: number, end: number): string {
|
||||||
|
return `rgb(${randomInt(start, end)},${randomInt(start, end)},${randomInt(start, end)})`
|
||||||
|
}
|
||||||
31
src/vite-env.d.ts
vendored
Normal file
31
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
type Captcha = {
|
||||||
|
value: string
|
||||||
|
base64Src: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RouteHandle = {
|
||||||
|
auth: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type _Response<T> = {
|
||||||
|
code: number
|
||||||
|
msg: string
|
||||||
|
data: T | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type Token = {
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
enable: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginForm = {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
30
tsconfig.json
Normal file
30
tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
"types": ["unplugin-icons/types/react"]
|
||||||
|
},
|
||||||
|
"include": ["src", "auto-imports.d.ts"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
11
tsconfig.node.json
Normal file
11
tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts", "build/resolvers/*"]
|
||||||
|
}
|
||||||
82
vite.config.ts
Normal file
82
vite.config.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig, PluginOption } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import Icons from 'unplugin-icons/vite'
|
||||||
|
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
|
||||||
|
import IconsResolver from 'unplugin-icons/resolver'
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import { AntDesignResolver } from './build/resolvers/antd'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
|
AutoImport({
|
||||||
|
// targets to transform
|
||||||
|
include: [
|
||||||
|
/\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
|
||||||
|
/\.md$/ // .md
|
||||||
|
],
|
||||||
|
|
||||||
|
// global imports to register
|
||||||
|
imports: [
|
||||||
|
'react',
|
||||||
|
'react-router',
|
||||||
|
'react-router-dom',
|
||||||
|
{
|
||||||
|
'react-router': ['useMatches', 'RouterProvider'],
|
||||||
|
'react-router-dom': ['createBrowserRouter'],
|
||||||
|
antd: ['message']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'react-router',
|
||||||
|
imports: ['RouteObject'],
|
||||||
|
type: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Filepath to generate corresponding .d.ts file.
|
||||||
|
// Defaults to './auto-imports.d.ts' when `typescript` is installed locally.
|
||||||
|
// Set `false` to disable.
|
||||||
|
dts: './auto-imports.d.ts',
|
||||||
|
|
||||||
|
// Custom resolvers, compatible with `unplugin-vue-components`
|
||||||
|
// see https://github.com/antfu/unplugin-auto-import/pull/23/
|
||||||
|
resolvers: [
|
||||||
|
IconsResolver({
|
||||||
|
prefix: 'icon',
|
||||||
|
extension: 'jsx',
|
||||||
|
customCollections: ['fatweb']
|
||||||
|
}),
|
||||||
|
AntDesignResolver({
|
||||||
|
resolveIcons: true
|
||||||
|
})
|
||||||
|
],
|
||||||
|
|
||||||
|
// Generate corresponding .eslintrc-auto-import.json file.
|
||||||
|
// eslint globals Docs - https://eslint.org/docs/user-guide/configuring/language-options#specifying-globals
|
||||||
|
eslintrc: {
|
||||||
|
enabled: true, // Default `false`
|
||||||
|
filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
|
||||||
|
globalsPropValue: true // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
|
||||||
|
}
|
||||||
|
}) as PluginOption,
|
||||||
|
Icons({
|
||||||
|
compiler: 'jsx',
|
||||||
|
jsx: 'react',
|
||||||
|
autoInstall: true,
|
||||||
|
customCollections: {
|
||||||
|
fatweb: FileSystemIconLoader('src/assets/svg', (svg) =>
|
||||||
|
svg.replace(/^svg /, '<svg fill="currentColor"')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user