commit 2920a8d6021f55ff7b1cea427b7cd229544b0af6 Author: alex Date: Sat Mar 22 00:01:11 2025 +0800 init diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..b3b3663 --- /dev/null +++ b/.env.development @@ -0,0 +1,2 @@ +VITE_USER_NAME=admin +VITE_PASSWORD=88888888 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d78e2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# 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? + + +# 临时文件 +.tmp +*.js +*.tsbuildinfo + +# 忽略eslint缓存 +.eslintcache diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..21292b8 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"], + "singleQuote": true, + "trailingComma": "all", + "printWidth": 120 +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..60c0a77 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# 项目结构 + +``` +project-root/ +├── src/ +│ ├── components/ +│ ├── services/ # 服务层 +│ ├── models/ # Mobx状态管理 +│ ├── layouts/ # 布局组件 +│ ├── pages/ # 页面路由 +│ ├── locales/ # 国际化配置 +│ │ ├── zh-CN/ +│ │ └── en-US/ +│ ├── assets/ # 静态资源 +│ │ ├── colors.css +│ │ ├── medias.css +│ │ └── variables.css +│ ├── App.tsx # 主应用组件 +│ ├── main.tsx # 入口文件 +│ └──typings.d.ts # 类型定义 +├── public/ # 公共静态文件 +│ └── index.html +├── package.json # 项目配置 +├── tsconfig.json # TypeScript配置 +└── vite.config.ts # 构建配置 +``` + +# 技术栈 + +- 前端框架:React +- 前端语言:TypeScript +- UI 库:Ant Design +- 状态管理:Mobx +- 网络请求:Axios +- 样式管理:CSS-in-JS +- 路由管理:React Router +- 国际化:React intl +- 代码规范:ESLint +- 代码格式化:Prettier +- 构建工具:Vite +- 其他: + - 图表库:Recharts + - 富文本编辑器:React Quill + - 图片上传:React Dropzone + - MD5 加密:spark-md5 + - 文件上传:react-dropzone + +# 约定路由 + +约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过目录和文件及其命名分析出路由配置。 + +1. 分析 src/pages 目录拿到路由配置。 +2. 约定 [ $] 包裹的文件或文件夹为动态可选路由。 +3. 约定目录下有 `_layout.tsx` 时会生成嵌套路由,以 `_layout.tsx` 为该目录的 layout。 +4. 约定 src/layouts/index.tsx 为全局路由。 +5. 约定 src/pages/404.tsx 为 404 页面。 + +# 布局 + +文件 src/layouts/index.tsx 为全局路由。 + +1. 布局使用 Ant Design 的 Layout 组件,包含头部、侧边栏、底部。 +2. 布局中包含基本的鉴权功能,用户登录后,判定权限是否可以访问当前页面;未登录用户,只能访问登录页面 +3. 布局中包含国际化功能,用户可以选择语言,切换语言后,页面会自动刷新。 +4. 布局中包含主题功能,用户可以选择主题,切换主题后,页面会自动刷新。 +5. 布局中包含暗黑模式功能,用户可以选择暗黑模式,切换暗黑模式后,页面会自动刷新。 diff --git a/index.html b/index.html new file mode 100644 index 0000000..7234572 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Aphrodite + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..bb35572 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "antd", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src", + "preview": "vite preview" + }, + "dependencies": { + "@ant-design/icons": "^5.6.1", + "antd": "^5.24.4", + "mobx": "^6.13.7", + "mobx-react": "^9.2.0", + "react": "^18", + "react-dom": "^18", + "react-intl": "^7.1.6", + "react-router-dom": "^7.3.0", + "spark-md5": "^3.0.2" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@tailwindcss/vite": "^4.0.14", + "@types/node": "^22.13.10", + "@types/react": "^19.0.11", + "@types/react-dom": "^19.0.4", + "@types/react-intl": "^3.0.0", + "@types/react-router-dom": "^5.3.3", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.21.0", + "eslint-formatter-pretty": "^6.0.1", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "postcss": "^8.5.3", + "prettier": "^3.5.3", + "prettier-plugin-tailwindcss": "^0.6.11", + "tailwindcss": "^4.0.14", + "typescript": "~5.7.2", + "typescript-eslint": "^8.24.1", + "vite": "^6.2.0" + } +} diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/@types/spark-md5.d.ts b/src/@types/spark-md5.d.ts new file mode 100644 index 0000000..c2fde4c --- /dev/null +++ b/src/@types/spark-md5.d.ts @@ -0,0 +1,6 @@ +declare module 'spark-md5' { + export function hash(input: string): string; + export function hashBinary(input: ArrayBuffer): string; + export function hashStr(input: string, raw?: boolean): string; + export function setBinary(input: boolean): void; +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..1ec71ab --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,22 @@ +import { App as AntdApp, ConfigProvider } from 'antd'; +import { observer } from 'mobx-react'; +import { IntlProvider } from '@/define'; +import { useSetting } from './models'; +import Routes from './routes'; +import './style.css'; + +function App() { + const setting = useSetting(); + + return ( + + + + + + + + ); +} +const ObserverApp = observer(App); +export default ObserverApp; diff --git a/src/assets/login_bg.svg b/src/assets/login_bg.svg new file mode 100644 index 0000000..89c2597 --- /dev/null +++ b/src/assets/login_bg.svg @@ -0,0 +1,69 @@ + + + + Group 21 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Planet/PageTable.tsx b/src/components/Planet/PageTable.tsx new file mode 100644 index 0000000..7e99d0e --- /dev/null +++ b/src/components/Planet/PageTable.tsx @@ -0,0 +1,82 @@ +import { RecordType } from '@/components/Stars/interface'; +import { StarModel } from '@/components/Stars/StarModel'; +import StarTable from '@/components/Stars/StarTable'; +import { StarTableProps } from '@/components/Stars/StarTable/interface'; +import { observer } from 'mobx-react'; +import { useEffect } from 'react'; + +const PageTable = ( + props: StarTableProps & { + model: StarModel; + defaultParams?: RecordType; + defaultOrders?: RecordType; + onLoading?: () => void; + }, +) => { + const { + model, + model: { + data: { page: data }, + params: { page: params }, + }, + columns, + defaultParams, + defaultOrders, + onLoading, + onQueryClick, + ...rest + } = props; + + /** + * 页面初始化加载数据 + */ + useEffect(() => { + onLoading ? onLoading!() : model.page({ page: 1, size: 10, params: defaultParams, orders: defaultOrders }); + }, []); + + const ds = data && data.data ? data.data.map((d: any) => ({ key: d.id, ...d })) : []; + + return ( + { + params.page = 1; + params.params = { ...defaultParams }; + for (const key of Object.keys(values)) { + if (values[key] !== undefined && values[key] !== null && values[key] !== '') { + params.params[key] = values[key]; + } + } + model.page(params); + onQueryClick && onQueryClick(values); + }} + onPageChanged={(page, size) => { + params.page = page; + params.size = size; + model.page(params); + }} + onRemoveClick={async (v) => { + await model.remove(v); + model.page(params); + }} + onSaveClick={async (values: any, mode) => { + if (mode === 'add') await model.add(values); + else await model.update(values); + model.page(params); + }} + operation + toolbar + autoForm + autoQuery + {...rest} + /> + ); +}; +const ObserverPageTable = observer(PageTable); +export default ObserverPageTable; diff --git a/src/components/SelectLang/index.tsx b/src/components/SelectLang/index.tsx new file mode 100644 index 0000000..2332ead --- /dev/null +++ b/src/components/SelectLang/index.tsx @@ -0,0 +1,434 @@ +import { useSetting } from '@/models'; +import { Dropdown, DropDownProps } from 'antd'; +import React from 'react'; +import { RecordType } from '../Stars/interface'; + +export interface HeaderDropdownProps extends DropDownProps { + overlayClassName?: string; + placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter'; +} + +const HeaderDropdown: React.FC = ({ overlayClassName: cls, ...restProps }) => ( + +); + +interface LocalData { + lang: string; + label?: string; + icon?: string; + title?: string; +} + +interface SelectLangProps { + globalIconClassName?: string; + postLocalesData?: (locales: LocalData[]) => LocalData[]; + onItemClick?: (params: RecordType) => void; + className?: string; + icon?: React.ReactNode; + style?: React.CSSProperties; +} + +const defaultLangUConfigMap = { + 'ar-EG': { + lang: 'ar-EG', + label: 'العربية', + icon: '🇪🇬', + title: 'لغة', + }, + 'az-AZ': { + lang: 'az-AZ', + label: 'Azərbaycan dili', + icon: '🇦🇿', + title: 'Dil', + }, + 'bg-BG': { + lang: 'bg-BG', + label: 'Български език', + icon: '🇧🇬', + title: 'език', + }, + 'bn-BD': { + lang: 'bn-BD', + label: 'বাংলা', + icon: '🇧🇩', + title: 'ভাষা', + }, + 'ca-ES': { + lang: 'ca-ES', + label: 'Catalá', + icon: '🇨🇦', + title: 'llengua', + }, + 'cs-CZ': { + lang: 'cs-CZ', + label: 'Čeština', + icon: '🇨🇿', + title: 'Jazyk', + }, + 'da-DK': { + lang: 'da-DK', + label: 'Dansk', + icon: '🇩🇰', + title: 'Sprog', + }, + 'de-DE': { + lang: 'de-DE', + label: 'Deutsch', + icon: '🇩🇪', + title: 'Sprache', + }, + 'el-GR': { + lang: 'el-GR', + label: 'Ελληνικά', + icon: '🇬🇷', + title: 'Γλώσσα', + }, + 'en-GB': { + lang: 'en-GB', + label: 'English', + icon: '🇬🇧', + title: 'Language', + }, + 'en-US': { + lang: 'en-US', + label: 'English', + icon: '🇺🇸', + title: 'Language', + }, + 'es-ES': { + lang: 'es-ES', + label: 'Español', + icon: '🇪🇸', + title: 'Idioma', + }, + 'et-EE': { + lang: 'et-EE', + label: 'Eesti', + icon: '🇪🇪', + title: 'Keel', + }, + 'fa-IR': { + lang: 'fa-IR', + label: 'فارسی', + icon: '🇮🇷', + title: 'زبان', + }, + 'fi-FI': { + lang: 'fi-FI', + label: 'Suomi', + icon: '🇫🇮', + title: 'Kieli', + }, + 'fr-BE': { + lang: 'fr-BE', + label: 'Français', + icon: '🇧🇪', + title: 'Langue', + }, + 'fr-FR': { + lang: 'fr-FR', + label: 'Français', + icon: '🇫🇷', + title: 'Langue', + }, + 'ga-IE': { + lang: 'ga-IE', + label: 'Gaeilge', + icon: '🇮🇪', + title: 'Teanga', + }, + 'he-IL': { + lang: 'he-IL', + label: 'עברית', + icon: '🇮🇱', + title: 'שפה', + }, + 'hi-IN': { + lang: 'hi-IN', + label: 'हिन्दी, हिंदी', + icon: '🇮🇳', + title: 'भाषा: हिन्दी', + }, + 'hr-HR': { + lang: 'hr-HR', + label: 'Hrvatski jezik', + icon: '🇭🇷', + title: 'Jezik', + }, + 'hu-HU': { + lang: 'hu-HU', + label: 'Magyar', + icon: '🇭🇺', + title: 'Nyelv', + }, + 'hy-AM': { + lang: 'hu-HU', + label: 'Հայերեն', + icon: '🇦🇲', + title: 'Լեզու', + }, + 'id-ID': { + lang: 'id-ID', + label: 'Bahasa Indonesia', + icon: '🇮🇩', + title: 'Bahasa', + }, + 'it-IT': { + lang: 'it-IT', + label: 'Italiano', + icon: '🇮🇹', + title: 'Linguaggio', + }, + 'is-IS': { + lang: 'is-IS', + label: 'Íslenska', + icon: '🇮🇸', + title: 'Tungumál', + }, + 'ja-JP': { + lang: 'ja-JP', + label: '日本語', + icon: '🇯🇵', + title: '言語', + }, + 'ku-IQ': { + lang: 'ku-IQ', + label: 'کوردی', + icon: '🇮🇶', + title: 'Ziman', + }, + 'kn-IN': { + lang: 'kn-IN', + label: 'ಕನ್ನಡ', + icon: '🇮🇳', + title: 'ಭಾಷೆ', + }, + 'ko-KR': { + lang: 'ko-KR', + label: '한국어', + icon: '🇰🇷', + title: '언어', + }, + 'lv-LV': { + lang: 'lv-LV', + label: 'Latviešu valoda', + icon: '🇱🇮', + title: 'Kalba', + }, + 'mk-MK': { + lang: 'mk-MK', + label: 'македонски јазик', + icon: '🇲🇰', + title: 'Јазик', + }, + 'mn-MN': { + lang: 'mn-MN', + label: 'Монгол хэл', + icon: '🇲🇳', + title: 'Хэл', + }, + 'ms-MY': { + lang: 'ms-MY', + label: 'بهاس ملايو‎', + icon: '🇲🇾', + title: 'Bahasa', + }, + 'nb-NO': { + lang: 'nb-NO', + label: 'Norsk', + icon: '🇳🇴', + title: 'Språk', + }, + 'ne-NP': { + lang: 'ne-NP', + label: 'नेपाली', + icon: '🇳🇵', + title: 'भाषा', + }, + 'nl-BE': { + lang: 'nl-BE', + label: 'Vlaams', + icon: '🇧🇪', + title: 'Taal', + }, + 'nl-NL': { + lang: 'nl-NL', + label: 'Nederlands', + icon: '🇳🇱', + title: 'Taal', + }, + 'pl-PL': { + lang: 'pl-PL', + label: 'Polski', + icon: '🇵🇱', + title: 'Język', + }, + 'pt-BR': { + lang: 'pt-BR', + label: 'Português', + icon: '🇧🇷', + title: 'Idiomas', + }, + 'pt-PT': { + lang: 'pt-PT', + label: 'Português', + icon: '🇵🇹', + title: 'Idiomas', + }, + 'ro-RO': { + lang: 'ro-RO', + label: 'Română', + icon: '🇷🇴', + title: 'Limba', + }, + 'ru-RU': { + lang: 'ru-RU', + label: 'Русский', + icon: '🇷🇺', + title: 'язык', + }, + 'sk-SK': { + lang: 'sk-SK', + label: 'Slovenčina', + icon: '🇸🇰', + title: 'Jazyk', + }, + 'sr-RS': { + lang: 'sr-RS', + label: 'српски језик', + icon: '🇸🇷', + title: 'Језик', + }, + 'sl-SI': { + lang: 'sl-SI', + label: 'Slovenščina', + icon: '🇸🇱', + title: 'Jezik', + }, + 'sv-SE': { + lang: 'sv-SE', + label: 'Svenska', + icon: '🇸🇪', + title: 'Språk', + }, + 'ta-IN': { + lang: 'ta-IN', + label: 'தமிழ்', + icon: '🇮🇳', + title: 'மொழி', + }, + 'th-TH': { + lang: 'th-TH', + label: 'ไทย', + icon: '🇹🇭', + title: 'ภาษา', + }, + 'tr-TR': { + lang: 'tr-TR', + label: 'Türkçe', + icon: '🇹🇷', + title: 'Dil', + }, + 'uk-UA': { + lang: 'uk-UA', + label: 'Українська', + icon: '🇺🇰', + title: 'Мова', + }, + 'vi-VN': { + lang: 'vi-VN', + label: 'Tiếng Việt', + icon: '🇻🇳', + title: 'Ngôn ngữ', + }, + 'zh-CN': { + lang: 'zh-CN', + label: '简体中文', + icon: '🇨🇳', + title: '语言', + }, + 'zh-TW': { + lang: 'zh-TW', + label: '繁體中文', + icon: '🇭🇰', + title: '語言', + }, +}; + +const getAllLocales = (): string[] => { + // 返回所有可用语言的逻辑 + return ['en-US', 'zh-CN']; // 示例返回值 +}; + +export const SelectLang: React.FC = (props) => { + const { globalIconClassName, postLocalesData, onItemClick, icon, style, ...restProps } = props; + const setting = useSetting(); + + const defaultLangUConfig = getAllLocales().map( + (key) => + defaultLangUConfigMap[key as keyof typeof defaultLangUConfigMap] || { + lang: key, + label: key, + icon: '🌐', + title: key, + }, + ); + + const allLangUIConfig = postLocalesData?.(defaultLangUConfig) || defaultLangUConfig; + const handleClick = onItemClick + ? (params: RecordType) => onItemClick(params) + : ({ key }: RecordType) => { + setting.setLocale(key); + }; + + const menuItemStyle = { minWidth: '160px' }; + const menuItemIconStyle = { marginRight: '8px' }; + + const langMenu = { + selectedKeys: [setting.locale], + onClick: handleClick, + items: allLangUIConfig.map((localeObj: RecordType) => ({ + key: localeObj.lang || localeObj.key, + style: menuItemStyle, + label: ( + <> + + {localeObj?.icon || '🌐'} + + {localeObj?.label || 'en-US'} + + ), + })), + }; + + const inlineStyle = { + cursor: 'pointer', + padding: '12px', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: 18, + verticalAlign: 'middle', + ...style, + }; + + return ( + + + + {icon ? ( + icon + ) : ( + + )} + + + + ); +}; diff --git a/src/components/Stars/StarDatePicker/index.tsx b/src/components/Stars/StarDatePicker/index.tsx new file mode 100644 index 0000000..3238fb5 --- /dev/null +++ b/src/components/Stars/StarDatePicker/index.tsx @@ -0,0 +1,80 @@ +import zh_CN from 'antd/es/date-picker/locale/zh_CN'; +import DatePicker, { DatePickerProps, RangePickerProps } from 'antd/lib/date-picker'; +import datetime, { DateTime, isDateTime } from '../utils/datetime'; +import React from 'react'; + +export type StarDatePickerProps = Omit & { + format?: string; + onChange?: (dt: string) => void; +}; + +export type StarDateRangePickerProps = Omit & { + value?: string[]; + format?: string; + showTime?: boolean; + onRangeChange?: (range: string[]) => void; +}; + +const StarDatePicker: React.FC = (props) => { + const { value, format, onChange, style, ...rest } = props; + + //格式化时间 + let date: DateTime | undefined; + if (isDateTime(value)) date = value; + else date = value ? datetime(value) : undefined; + + const restProps = rest as DatePickerProps; + return ( + { + if (m) { + onChange!(m.format(format || 'yyyy-MM-DDTHH:mm:ss.SSSZ')); + } + }} + {...restProps} + /> + ); +}; + +export default StarDatePicker; + +export const StarDateRangePicker: React.FC = (props) => { + const { value, format, style, showTime, onRangeChange, onChange, ...rest } = props; + const moments: [DateTime | null, DateTime | null] = [null, null]; + if (value) { + if (value[0]) moments[0] = datetime(value[0]); + if (value[1]) moments[1] = datetime(value[1]); + } + + return ( + { + let times: string[] = []; + if (showTime) { + times = m + ? [m[0]!.format(format || 'yyyy-MM-DDTHH:mm:ss.SSSZ'), m[1]!.format(format || 'yyyy-MM-DDTHH:mm:ss.SSSZ')] + : []; + } else { + times = m + ? [ + m[0]!.startOf('day').format(format || 'yyyy-MM-DDTHH:mm:ss.SSSZ'), + m[1]!.endOf('day').format(format || 'yyyy-MM-DDTHH:mm:ss.SSSZ'), + ] + : []; + } + if (m) { + onRangeChange && onRangeChange(times); + } + onChange && onChange(m, s); + }} + {...rest} + /> + ); +}; diff --git a/src/components/Stars/StarElement/index.tsx b/src/components/Stars/StarElement/index.tsx new file mode 100644 index 0000000..241c03e --- /dev/null +++ b/src/components/Stars/StarElement/index.tsx @@ -0,0 +1,190 @@ +import React, { JSX, useEffect, useState } from 'react'; +import { + AutoComplete, + AutoCompleteProps, + Input, + InputNumber, + InputNumberProps, + InputProps, + Select, + SelectProps, + Switch, + SwitchProps, + TreeSelect, + TreeSelectProps, +} from 'antd'; +import { PasswordProps } from 'antd/es/input'; +import Password from 'antd/lib/input/Password'; +import TextArea, { TextAreaProps } from 'antd/lib/input/TextArea'; +import StarDatePicker, { StarDatePickerProps, StarDateRangePicker, StarDateRangePickerProps } from '../StarDatePicker'; +import StarLabel from '../StarLabel'; +import { StarMoneyInput, StarMoneyLabel } from '../StarMoney'; +import { StarMoneyProps } from '../StarMoney/interface'; +import { RecordType } from '../interface'; +import { ElementType, StarElementOption, StarElementProps } from './type'; + +/** + * 组件映射函数,用于根据组件类型返回对应的React组件 + * 使用组件映射代替冗长的switch语句,提高代码可维护性 + */ +const ElementMap: Record JSX.Element> = { + [ElementType.Label]: (props) => { + const { style, value, ...rest } = props as RecordType; + return ; + }, + [ElementType.TextArea]: (props) => { + const elementProps = props as TextAreaProps; + return