Compare commits
10 Commits
c663ed623b
...
e1dbfb010d
Author | SHA1 | Date |
---|---|---|
|
e1dbfb010d | |
|
1a3da2efff | |
|
dc1711f949 | |
|
131db241a3 | |
|
ba0825a019 | |
|
d2489121d9 | |
|
425f0d7635 | |
|
8d1fee793d | |
|
774b52fca9 | |
|
952ff8f2ab |
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "debug",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeArgs": ["run-script", "debug"],
|
||||||
|
"autoAttachChildProcesses": true,
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"runtimeExecutable": "pnpm",
|
||||||
|
"skipFiles": ["<node_internals>/**"],
|
||||||
|
"type": "node"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,18 +1,13 @@
|
||||||
{
|
{
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"[prisma]": {
|
|
||||||
"editor.defaultFormatter": "Prisma.prisma",
|
|
||||||
"editor.formatOnSave": true
|
|
||||||
},
|
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit",
|
"source.fixAll.eslint": "explicit",
|
||||||
"source.fixAll.stylelint": "explicit"
|
"source.fixAll.stylelint": "explicit"
|
||||||
},
|
},
|
||||||
"eslint.workingDirectories": [
|
"[prisma]": {
|
||||||
{
|
"editor.defaultFormatter": "Prisma.prisma",
|
||||||
"mode": "auto"
|
"editor.formatOnSave": true
|
||||||
}
|
},
|
||||||
],
|
|
||||||
"css.validate": false,
|
"css.validate": false,
|
||||||
"less.validate": false,
|
"less.validate": false,
|
||||||
"scss.validate": false,
|
"scss.validate": false,
|
||||||
|
@ -20,15 +15,33 @@
|
||||||
"emmet.includeLanguages": {
|
"emmet.includeLanguages": {
|
||||||
"postcss": "css"
|
"postcss": "css"
|
||||||
},
|
},
|
||||||
"tailwindCSS.experimental.configFile": {
|
|
||||||
"apps/web/src/app/tailwind-config.ts": "apps/web/src/app/**",
|
|
||||||
"apps/web2/src/app/tailwind-config.ts": "apps/web2/src/app/**",
|
|
||||||
},
|
|
||||||
"stylelint.enable": true,
|
"stylelint.enable": true,
|
||||||
"stylelint.snippet": ["css", "scss", "less", "postcss"],
|
"stylelint.snippet": ["css", "scss", "less", "postcss"],
|
||||||
"stylelint.validate": ["css", "scss", "less", "postcss"],
|
"stylelint.validate": ["css", "scss", "less", "postcss"],
|
||||||
|
"tailwindCSS.experimental.configFile": {
|
||||||
|
"apps/web2/src/app/tailwind-config.ts": "apps/web2/src/app/**",
|
||||||
|
},
|
||||||
"javascript.preferences.importModuleSpecifier": "project-relative",
|
"javascript.preferences.importModuleSpecifier": "project-relative",
|
||||||
"typescript.suggest.jsdoc.generateReturns": false,
|
"typescript.suggest.jsdoc.generateReturns": false,
|
||||||
"stylelint.packageManager": "pnpm",
|
"stylelint.packageManager": "pnpm",
|
||||||
"npm.packageManager": "pnpm"
|
"npm.packageManager": "pnpm",
|
||||||
|
"editor.inlineSuggest.enabled": true, // 启用内联建议(Copilot)
|
||||||
|
"editor.suggestSelection": "first", // 默认选择第一个建议
|
||||||
|
"editor.quickSuggestions": {
|
||||||
|
// 启用快速建议
|
||||||
|
"other": true,
|
||||||
|
"comments": false,
|
||||||
|
"strings": true
|
||||||
|
},
|
||||||
|
// 确保默认补全提供程序都启用
|
||||||
|
"typescript.suggest.enabled": true,
|
||||||
|
"javascript.suggest.enabled": true,
|
||||||
|
// Copilot设置
|
||||||
|
"github.copilot.enable": {
|
||||||
|
"*": true,
|
||||||
|
"plaintext": false,
|
||||||
|
"markdown": false,
|
||||||
|
"scminput": false
|
||||||
|
},
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
}
|
}
|
|
@ -1,28 +0,0 @@
|
||||||
dist
|
|
||||||
node_modules
|
|
||||||
pnpm-lock.yaml
|
|
||||||
package-lock.json
|
|
||||||
docker
|
|
||||||
Dockerfile*
|
|
||||||
LICENSE
|
|
||||||
yarn-error.log
|
|
||||||
.history
|
|
||||||
.dockerignore
|
|
||||||
.DS_Store
|
|
||||||
.eslintignore
|
|
||||||
.editorconfig
|
|
||||||
.gitignore
|
|
||||||
.prettierignore
|
|
||||||
.eslintcache
|
|
||||||
**/*.lock
|
|
||||||
**/*.png
|
|
||||||
**/*.eot
|
|
||||||
**/*.ttf
|
|
||||||
**/*.woff
|
|
||||||
**/*.svg
|
|
||||||
**/*.md
|
|
||||||
**/*.svg
|
|
||||||
**/*.ejs
|
|
||||||
**/*.html
|
|
||||||
**/*.png
|
|
||||||
**/*.toml
|
|
|
@ -1,8 +0,0 @@
|
||||||
/** @type {import("eslint").Linter.Config} */
|
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
parserOptions: {
|
|
||||||
project: './tsconfig.eslint.json',
|
|
||||||
},
|
|
||||||
extends: [require.resolve('@3rapp/core/eslint/react')],
|
|
||||||
};
|
|
|
@ -1,26 +0,0 @@
|
||||||
# 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/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
|
@ -1,2 +0,0 @@
|
||||||
/** @format */
|
|
||||||
module.exports = require.resolve('@3rapp/core/prettier')
|
|
|
@ -1,28 +0,0 @@
|
||||||
dist
|
|
||||||
node_modules
|
|
||||||
pnpm-lock.yaml
|
|
||||||
package-lock.json
|
|
||||||
docker
|
|
||||||
Dockerfile*
|
|
||||||
LICENSE
|
|
||||||
yarn-error.log
|
|
||||||
.history
|
|
||||||
.dockerignore
|
|
||||||
.DS_Store
|
|
||||||
.eslintignore
|
|
||||||
.editorconfig
|
|
||||||
.gitignore
|
|
||||||
.prettierignore
|
|
||||||
.eslintcache
|
|
||||||
**/*.lock
|
|
||||||
**/*.png
|
|
||||||
**/*.eot
|
|
||||||
**/*.ttf
|
|
||||||
**/*.woff
|
|
||||||
**/*.svg
|
|
||||||
**/*.md
|
|
||||||
**/*.svg
|
|
||||||
**/*.ejs
|
|
||||||
**/*.html
|
|
||||||
**/*.png
|
|
||||||
**/*.toml
|
|
|
@ -1,28 +0,0 @@
|
||||||
dist
|
|
||||||
node_modules
|
|
||||||
pnpm-lock.yaml
|
|
||||||
package-lock.json
|
|
||||||
docker
|
|
||||||
Dockerfile*
|
|
||||||
LICENSE
|
|
||||||
yarn-error.log
|
|
||||||
.history
|
|
||||||
.dockerignore
|
|
||||||
.DS_Store
|
|
||||||
.eslintignore
|
|
||||||
.editorconfig
|
|
||||||
.gitignore
|
|
||||||
.prettierignore
|
|
||||||
.eslintcache
|
|
||||||
**/*.lock
|
|
||||||
**/*.png
|
|
||||||
**/*.eot
|
|
||||||
**/*.ttf
|
|
||||||
**/*.woff
|
|
||||||
**/*.svg
|
|
||||||
**/*.md
|
|
||||||
**/*.svg
|
|
||||||
**/*.ejs
|
|
||||||
**/*.html
|
|
||||||
**/*.png
|
|
||||||
**/*.toml
|
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
extends: [require.resolve('@3rapp/core/stylelint')],
|
|
||||||
};
|
|
|
@ -1,30 +0,0 @@
|
||||||
# React + TypeScript + Vite
|
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
||||||
|
|
||||||
Currently, two official plugins are available:
|
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
|
||||||
|
|
||||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
|
||||||
|
|
||||||
- Configure the top-level `parserOptions` property like this:
|
|
||||||
|
|
||||||
```js
|
|
||||||
export default {
|
|
||||||
// other rules...
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
sourceType: 'module',
|
|
||||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
|
||||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
|
||||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
|
|
@ -1,13 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<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>
|
|
|
@ -1,54 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@3rapp/admin",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite --no-clearScreen",
|
|
||||||
"build": "tsc && vite build",
|
|
||||||
"start": "pnpm preview",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"lint": "pnpm lint:es && pnpm lint:style",
|
|
||||||
"lint:es": "eslint . --ext ts,tsx --fix --report-unused-disable-directives --max-warnings 0",
|
|
||||||
"lint:style": "stylelint \"**/*.css\" --fix --cache --cache-location node_modules/.cache/stylelint/"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@3rapp/store": "workspace:*",
|
|
||||||
"@3rapp/utils": "workspace:*",
|
|
||||||
"@ant-design/cssinjs": "^1.20.0",
|
|
||||||
"antd": "^5.18.1",
|
|
||||||
"antd-style": "^3.6.2",
|
|
||||||
"axios": "^1.7.0",
|
|
||||||
"classnames": "^2.5.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"dayjs": "^1.11.11",
|
|
||||||
"immer": "^10.1.1",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"lunar-typescript": "^1.7.5",
|
|
||||||
"react": "^18.3.1",
|
|
||||||
"react-dom": "^18.3.1",
|
|
||||||
"react-use": "^17.5.0",
|
|
||||||
"utility-types": "^3.11.0",
|
|
||||||
"zustand": "^4.5.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@3rapp/core": "workspace:*",
|
|
||||||
"@3rapp/utils": "workspace:*",
|
|
||||||
"@types/lodash": "^4.17.5",
|
|
||||||
"@types/node": "^20.12.12",
|
|
||||||
"@types/react": "^18.3.2",
|
|
||||||
"@types/react-dom": "^18.3.0",
|
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
|
||||||
"autoprefixer": "^10.4.19",
|
|
||||||
"eslint": "^8.57.0",
|
|
||||||
"postcss-import": "^16.1.0",
|
|
||||||
"postcss-mixins": "^10.0.1",
|
|
||||||
"postcss-nested": "^6.0.1",
|
|
||||||
"postcss-nesting": "^12.1.4",
|
|
||||||
"prettier": "^3.2.5",
|
|
||||||
"stylelint": "^16.5.0",
|
|
||||||
"tailwindcss": "^3.4.3",
|
|
||||||
"typescript": "^5.4.5",
|
|
||||||
"vite": "^5.2.11"
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,10 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
'postcss-import': {},
|
|
||||||
'postcss-nesting': {},
|
|
||||||
'tailwindcss/nesting': {},
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
'postcss-mixins': {},
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
<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>
|
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1,38 +0,0 @@
|
||||||
import { pathResolve, deepMerge } from '@3rapp/utils';
|
|
||||||
import { ConfigEnv, UserConfig } from 'vite';
|
|
||||||
|
|
||||||
import { createPlugins } from './plugins';
|
|
||||||
import { Configure } from './types';
|
|
||||||
|
|
||||||
export const createConfig = (params: ConfigEnv, configure?: Configure): UserConfig => {
|
|
||||||
const isBuild = params.command === 'build';
|
|
||||||
return deepMerge<UserConfig, UserConfig>(
|
|
||||||
{
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': pathResolve('../src'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
modules: {
|
|
||||||
localsConvention: 'camelCaseOnly',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
host: '0.0.0.0',
|
|
||||||
port: 3001,
|
|
||||||
proxy: {
|
|
||||||
'/api': {
|
|
||||||
target: 'http://localhost:8080/api',
|
|
||||||
changeOrigin: true,
|
|
||||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cors: true,
|
|
||||||
},
|
|
||||||
plugins: createPlugins(isBuild),
|
|
||||||
},
|
|
||||||
typeof configure === 'function' ? configure(params, isBuild) : {},
|
|
||||||
'merge',
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,7 +0,0 @@
|
||||||
import react from '@vitejs/plugin-react';
|
|
||||||
import { PluginOption } from 'vite';
|
|
||||||
|
|
||||||
export function createPlugins(isBuild: boolean) {
|
|
||||||
const vitePlugins: (PluginOption | PluginOption[])[] = [react()];
|
|
||||||
return vitePlugins;
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
import { ConfigEnv, UserConfig } from 'vite';
|
|
||||||
|
|
||||||
export type Configure = (params: ConfigEnv, isBuild: boolean) => UserConfig;
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { clsx } from 'clsx';
|
|
||||||
|
|
||||||
export const app = clsx('tw-flex tw-flex-auto tw-flex-wrap tw-items-center tw-justify-center');
|
|
||||||
export const container = clsx(
|
|
||||||
' tw-bg-neutral-100/40 tw-shadow-black/20 tw-backdrop-blur-sm tw-shadow-md tw-rounded-md tw-p-5 tw-m-5 tw-min-w-[20rem]',
|
|
||||||
);
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { px2remTransformer, StyleProvider } from '@ant-design/cssinjs';
|
|
||||||
import { ConfigProvider, App as AntdApp } from 'antd';
|
|
||||||
import { FC, useMemo } from 'react';
|
|
||||||
|
|
||||||
import { app } from './app.css';
|
|
||||||
import { localeData } from './components/i18n/data';
|
|
||||||
import { useLocalStore } from './components/i18n/store';
|
|
||||||
import Setting from './components/setting';
|
|
||||||
import Theme from './components/theme';
|
|
||||||
import { useAntdAlgorithm } from './components/theme/hook';
|
|
||||||
|
|
||||||
const Wrapper: FC = () => {
|
|
||||||
const lang = useLocalStore((state) => state.lang);
|
|
||||||
const locale = useMemo(() => localeData[lang], [lang]);
|
|
||||||
const algorithm = useAntdAlgorithm();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfigProvider
|
|
||||||
locale={locale.antd}
|
|
||||||
theme={{
|
|
||||||
algorithm,
|
|
||||||
// 启用css变量
|
|
||||||
cssVar: true,
|
|
||||||
hashed: false,
|
|
||||||
token: {},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StyleProvider layer transformers={[px2remTransformer()]}>
|
|
||||||
<AntdApp>
|
|
||||||
<div className={app}>
|
|
||||||
<Setting />
|
|
||||||
</div>
|
|
||||||
</AntdApp>
|
|
||||||
</StyleProvider>
|
|
||||||
</ConfigProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const App: FC = () => {
|
|
||||||
return (
|
|
||||||
<Theme>
|
|
||||||
<Wrapper />
|
|
||||||
</Theme>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default App;
|
|
Binary file not shown.
Before Width: | Height: | Size: 266 KiB |
Binary file not shown.
Before Width: | Height: | Size: 923 KiB |
|
@ -1,8 +0,0 @@
|
||||||
export enum LangType {
|
|
||||||
EN_US = 'en_US',
|
|
||||||
ZH_CN = 'zh_CN',
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 支持的语言类型
|
|
||||||
*/
|
|
||||||
export const langs: `${LangType}`[] = ['en_US', 'zh_CN'];
|
|
|
@ -1,16 +0,0 @@
|
||||||
import enUS from 'antd/es/locale/en_US';
|
|
||||||
import zhCN from 'antd/es/locale/zh_CN';
|
|
||||||
|
|
||||||
import { LangType } from './constants';
|
|
||||||
import { LocaleItem } from './types';
|
|
||||||
|
|
||||||
export const localeData: Record<`${LangType}`, LocaleItem> = {
|
|
||||||
en_US: {
|
|
||||||
label: '🇺🇸 english(US)',
|
|
||||||
antd: enUS,
|
|
||||||
},
|
|
||||||
zh_CN: {
|
|
||||||
label: '🇨🇳 简体中文',
|
|
||||||
antd: zhCN,
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { isNil } from 'lodash';
|
|
||||||
import { FC, ReactNode } from 'react';
|
|
||||||
|
|
||||||
import { LangType } from './constants';
|
|
||||||
import { useLocalStore } from './store';
|
|
||||||
|
|
||||||
const Locale: FC<{ children?: ReactNode } & { lang: `${LangType}` }> = ({ children, lang }) => {
|
|
||||||
const changeLang = useLocalStore((state) => state.changeLang);
|
|
||||||
!isNil(changeLang) && changeLang(lang);
|
|
||||||
return <>{children}</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Locale;
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { createPersistStore } from '@3rapp/store';
|
|
||||||
|
|
||||||
import { isNil } from 'lodash';
|
|
||||||
|
|
||||||
import { LangType, langs } from './constants';
|
|
||||||
|
|
||||||
export const useLocalStore = createPersistStore<{
|
|
||||||
lang: `${LangType}`;
|
|
||||||
changeLang: (name: `${LangType}`) => void;
|
|
||||||
}>(
|
|
||||||
(set) => ({
|
|
||||||
lang: langs[0],
|
|
||||||
changeLang: (name) => {
|
|
||||||
set((state) => {
|
|
||||||
const item = langs.find((lang) => lang === name);
|
|
||||||
!isNil(item) && (state.lang = item);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'local',
|
|
||||||
},
|
|
||||||
);
|
|
|
@ -1,7 +0,0 @@
|
||||||
// antd/es/locale es6版本 antd/lib/locale es5版本
|
|
||||||
import { Locale } from 'antd/es/locale';
|
|
||||||
|
|
||||||
export interface LocaleItem {
|
|
||||||
label: string;
|
|
||||||
antd: Locale;
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
import { Col, Radio, Row, Select } from 'antd';
|
|
||||||
import { Dayjs } from 'dayjs';
|
|
||||||
import { Lunar } from 'lunar-typescript';
|
|
||||||
import { FC } from 'react';
|
|
||||||
|
|
||||||
const getYearLabel = (year: number) => {
|
|
||||||
const d = Lunar.fromDate(new Date(year + 1, 0));
|
|
||||||
return `${d.getYearInChinese()}年(${d.getYearInGanZhi()}${d.getYearShengXiao()}年)`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMonthLabel = (month: number, value: Dayjs) => {
|
|
||||||
const d = Lunar.fromDate(new Date(value.year(), month));
|
|
||||||
const lunar = d.getMonthInChinese();
|
|
||||||
return `${month + 1}月(${lunar}月)`;
|
|
||||||
};
|
|
||||||
interface CustomHeaderProps {
|
|
||||||
value: Dayjs;
|
|
||||||
type: string;
|
|
||||||
onChange: (date: Dayjs) => void;
|
|
||||||
onTypeChange: (e: any) => void;
|
|
||||||
}
|
|
||||||
const CustomHeader: FC<CustomHeaderProps> = ({ value, type, onChange, onTypeChange }) => {
|
|
||||||
const start = 0;
|
|
||||||
const end = 12;
|
|
||||||
const monthOptions = [];
|
|
||||||
|
|
||||||
let current = value.clone();
|
|
||||||
const localeDate = (value as any).localeData();
|
|
||||||
const months = [];
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
current = current.month(i);
|
|
||||||
months.push(localeDate.monthsShort(current));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = start; i < end; i++) {
|
|
||||||
monthOptions.push({
|
|
||||||
label: getMonthLabel(i, value),
|
|
||||||
value: i,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const year = value.year();
|
|
||||||
const month = value.month();
|
|
||||||
const options = [];
|
|
||||||
for (let i = year - 10; i < year + 10; i += 1) {
|
|
||||||
options.push({
|
|
||||||
label: getYearLabel(i),
|
|
||||||
value: i,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Row justify="end" gutter={8} style={{ padding: 8 }}>
|
|
||||||
<Col>
|
|
||||||
<Select
|
|
||||||
size="small"
|
|
||||||
className="my-year-select"
|
|
||||||
value={year}
|
|
||||||
options={options}
|
|
||||||
onChange={(newYear) => {
|
|
||||||
const now = value.clone().year(newYear);
|
|
||||||
onChange(now);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col>
|
|
||||||
<Select
|
|
||||||
size="small"
|
|
||||||
value={month}
|
|
||||||
options={monthOptions}
|
|
||||||
onChange={(newMonth) => {
|
|
||||||
const now = value.clone().month(newMonth);
|
|
||||||
onChange(now);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col>
|
|
||||||
<Radio.Group
|
|
||||||
size="small"
|
|
||||||
onChange={(e) => onTypeChange(e.target.value)}
|
|
||||||
value={type}
|
|
||||||
>
|
|
||||||
<Radio.Button value="month">月</Radio.Button>
|
|
||||||
<Radio.Button value="year">年</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default CustomHeader;
|
|
|
@ -1,235 +0,0 @@
|
||||||
/* eslint-disable react/no-unstable-nested-components */
|
|
||||||
import { Calendar, CalendarProps, Col, Row, Select, Switch, Radio } from 'antd';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
|
||||||
import { debounce } from 'lodash';
|
|
||||||
import { HolidayUtil, Lunar } from 'lunar-typescript';
|
|
||||||
import { cloneElement, useCallback, useContext, useState } from 'react';
|
|
||||||
|
|
||||||
import { useStore } from 'zustand';
|
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
|
||||||
|
|
||||||
import { container } from '@/app.css';
|
|
||||||
|
|
||||||
import { langs } from '../i18n/constants';
|
|
||||||
import { localeData as langeLocalData } from '../i18n/data';
|
|
||||||
import { useLocalStore } from '../i18n/store';
|
|
||||||
import { ThemeContext } from '../theme/hook';
|
|
||||||
|
|
||||||
import { useStyle } from './settingStore';
|
|
||||||
|
|
||||||
const ThemeSetting: React.FC = () => {
|
|
||||||
const themeStore = useContext(ThemeContext);
|
|
||||||
// const { mode, compact } = themeStore.getState();
|
|
||||||
const { mode, compact } = useStore(themeStore, (state) => ({
|
|
||||||
mode: state.mode,
|
|
||||||
compact: state.compact,
|
|
||||||
}));
|
|
||||||
const dispatch = useShallow(themeStore((state) => state.dispatch));
|
|
||||||
const toggleMode = useCallback(
|
|
||||||
debounce(() => dispatch({ type: 'toggle_mode' }), 100, {}),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const toggleCompact = useCallback(
|
|
||||||
debounce(() => dispatch({ type: 'toggle_compact' }), 100, {}),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Switch
|
|
||||||
checkedChildren="🌛"
|
|
||||||
unCheckedChildren="☀️"
|
|
||||||
onChange={toggleMode}
|
|
||||||
checked={mode === 'dark'}
|
|
||||||
defaultChecked={mode === 'dark'}
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
checkedChildren="紧凑"
|
|
||||||
unCheckedChildren="正常"
|
|
||||||
onChange={toggleCompact}
|
|
||||||
checked={compact}
|
|
||||||
defaultChecked={compact}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LocaleSetting: React.FC = () => {
|
|
||||||
const locale = useLocalStore((state) => state.lang);
|
|
||||||
const changeLocale = useLocalStore((state) => state.changeLang);
|
|
||||||
return (
|
|
||||||
<Select defaultValue={locale} style={{ width: 120 }} onChange={changeLocale}>
|
|
||||||
{langs.map((name) => (
|
|
||||||
<Select.Option key={name} value={name}>
|
|
||||||
{langeLocalData[name].label}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Setting: React.FC = () => {
|
|
||||||
const { styles } = useStyle({ test: true });
|
|
||||||
const [selectDate, setSelectDate] = useState<Dayjs>(dayjs());
|
|
||||||
const [panelDateDate, setPanelDate] = useState<Dayjs>(dayjs());
|
|
||||||
const onPanelChange = (value: Dayjs, _mode: CalendarProps<Dayjs>['mode']) => {
|
|
||||||
setPanelDate(value);
|
|
||||||
};
|
|
||||||
const onDateChange: CalendarProps<Dayjs>['onSelect'] = (value, selectInfo) => {
|
|
||||||
if (selectInfo.source === 'date') {
|
|
||||||
setSelectDate(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const cellRender: CalendarProps<Dayjs>['fullCellRender'] = (date, info) => {
|
|
||||||
const d = Lunar.fromDate(date.toDate());
|
|
||||||
const lunar = d.getDayInChinese();
|
|
||||||
const solarTerm = d.getJieQi();
|
|
||||||
const isWeekend = date.day() === 6 || date.day() === 0;
|
|
||||||
const h = HolidayUtil.getHoliday(date.get('year'), date.get('month') + 1, date.get('date'));
|
|
||||||
const displayHoliday = h?.getTarget() === h?.getDay() ? h?.getName() : undefined;
|
|
||||||
if (info.type === 'date') {
|
|
||||||
return cloneElement(info.originNode, {
|
|
||||||
...info.originNode.props,
|
|
||||||
className: classNames(styles.dateCell, {
|
|
||||||
[styles.current]: selectDate.isSame(date, 'date'),
|
|
||||||
[styles.today]: date.isSame(dayjs(), 'date'),
|
|
||||||
}),
|
|
||||||
children: (
|
|
||||||
<div className={styles.text}>
|
|
||||||
<span
|
|
||||||
className={classNames({
|
|
||||||
[styles.weekend]: isWeekend,
|
|
||||||
gray: !panelDateDate.isSame(date, 'month'),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{date.get('date')}
|
|
||||||
</span>
|
|
||||||
{info.type === 'date' && (
|
|
||||||
<div className={styles.lunar}>
|
|
||||||
{displayHoliday || solarTerm || lunar}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.type === 'month') {
|
|
||||||
// Due to the fact that a solar month is part of the lunar month X and part of the lunar month X+1,
|
|
||||||
// when rendering a month, always take X as the lunar month of the month
|
|
||||||
const d2 = Lunar.fromDate(new Date(date.get('year'), date.get('month')));
|
|
||||||
const month = d2.getMonthInChinese();
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(styles.monthCell, {
|
|
||||||
[styles.monthCellCurrent]: selectDate.isSame(date, 'month'),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{date.get('month') + 1}月({month}月)
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
const getYearLabel = (year: number) => {
|
|
||||||
const d = Lunar.fromDate(new Date(year + 1, 0));
|
|
||||||
return `${d.getYearInChinese()}年(${d.getYearInGanZhi()}${d.getYearShengXiao()}年)`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMonthLabel = (month: number, value: Dayjs) => {
|
|
||||||
const d = Lunar.fromDate(new Date(value.year(), month));
|
|
||||||
const lunar = d.getMonthInChinese();
|
|
||||||
return `${month + 1}月(${lunar}月)`;
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className={container}>
|
|
||||||
<h2 className="tw-text-center">Setting Demo</h2>
|
|
||||||
<div className="tw-flex tw-items-center tw-flex-col">
|
|
||||||
<div className="tw-flex-auto tw-mb-5">
|
|
||||||
<ThemeSetting />
|
|
||||||
</div>
|
|
||||||
<div className="tw-flex-auto tw-mb-5">
|
|
||||||
<LocaleSetting />
|
|
||||||
</div>
|
|
||||||
<div className={styles.wrapper}>
|
|
||||||
<Calendar
|
|
||||||
fullCellRender={cellRender}
|
|
||||||
fullscreen={false}
|
|
||||||
onSelect={onDateChange}
|
|
||||||
onPanelChange={onPanelChange}
|
|
||||||
headerRender={({ value, type, onChange, onTypeChange }) => {
|
|
||||||
const start = 0;
|
|
||||||
const end = 12;
|
|
||||||
const monthOptions = [];
|
|
||||||
|
|
||||||
let current = value.clone();
|
|
||||||
const localeData = (value as any).localeData();
|
|
||||||
const months = [];
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
current = current.month(i);
|
|
||||||
months.push(localeData.monthsShort(current));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = start; i < end; i++) {
|
|
||||||
monthOptions.push({
|
|
||||||
label: getMonthLabel(i, value),
|
|
||||||
value: i,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const year = value.year();
|
|
||||||
const month = value.month();
|
|
||||||
const options = [];
|
|
||||||
for (let i = year - 10; i < year + 10; i += 1) {
|
|
||||||
options.push({
|
|
||||||
label: getYearLabel(i),
|
|
||||||
value: i,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Row justify="end" gutter={8} style={{ padding: 8 }}>
|
|
||||||
<Col>
|
|
||||||
<Select
|
|
||||||
size="small"
|
|
||||||
dropdownMatchSelectWidth={false}
|
|
||||||
className="my-year-select"
|
|
||||||
value={year}
|
|
||||||
options={options}
|
|
||||||
onChange={(newYear) => {
|
|
||||||
const now = value.clone().year(newYear);
|
|
||||||
onChange(now);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col>
|
|
||||||
<Select
|
|
||||||
size="small"
|
|
||||||
dropdownMatchSelectWidth={false}
|
|
||||||
value={month}
|
|
||||||
options={monthOptions}
|
|
||||||
onChange={(newMonth) => {
|
|
||||||
const now = value.clone().month(newMonth);
|
|
||||||
onChange(now);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col>
|
|
||||||
<Radio.Group
|
|
||||||
size="small"
|
|
||||||
onChange={(e) => onTypeChange(e.target.value)}
|
|
||||||
value={type}
|
|
||||||
>
|
|
||||||
<Radio.Button value="month">月</Radio.Button>
|
|
||||||
<Radio.Button value="year">年</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default Setting;
|
|
|
@ -1,85 +0,0 @@
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
|
|
||||||
export const useStyle = createStyles(({ token, css, cx }) => {
|
|
||||||
const lunar = css`
|
|
||||||
color: ${token.colorTextTertiary};
|
|
||||||
font-size: ${token.fontSizeSM}px;
|
|
||||||
`;
|
|
||||||
return {
|
|
||||||
wrapper: css`
|
|
||||||
width: 450px;
|
|
||||||
border: 1px solid ${token.colorBorderSecondary};
|
|
||||||
border-radius: ${token.borderRadiusOuter};
|
|
||||||
padding: 5px;
|
|
||||||
`,
|
|
||||||
dateCell: css`
|
|
||||||
position: relative;
|
|
||||||
&:before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin: auto;
|
|
||||||
max-width: 40px;
|
|
||||||
max-height: 40px;
|
|
||||||
background: transparent;
|
|
||||||
transition: background 300ms;
|
|
||||||
border-radius: ${token.borderRadiusOuter}px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
&:hover:before {
|
|
||||||
background: rgba(0, 0, 0, 0.04);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
today: css`
|
|
||||||
&:before {
|
|
||||||
border: 1px solid ${token.colorPrimary};
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
text: css`
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
`,
|
|
||||||
lunar,
|
|
||||||
current: css`
|
|
||||||
color: ${token.colorTextLightSolid};
|
|
||||||
&:before {
|
|
||||||
background: ${token.colorPrimary};
|
|
||||||
}
|
|
||||||
&:hover:before {
|
|
||||||
background: ${token.colorPrimary};
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
.${cx(lunar)} {
|
|
||||||
color: ${token.colorTextLightSolid};
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
monthCell: css`
|
|
||||||
width: 120px;
|
|
||||||
color: ${token.colorTextBase};
|
|
||||||
border-radius: ${token.borderRadiusOuter}px;
|
|
||||||
padding: 5px 0;
|
|
||||||
&:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.04);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
monthCellCurrent: css`
|
|
||||||
color: ${token.colorTextLightSolid};
|
|
||||||
background: ${token.colorPrimary};
|
|
||||||
&:hover {
|
|
||||||
background: ${token.colorPrimary};
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
weekend: css`
|
|
||||||
color: ${token.colorError};
|
|
||||||
&.gray {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
});
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { ThemeOptions } from './types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 主题模式
|
|
||||||
*/
|
|
||||||
export enum ThemeMode {
|
|
||||||
LIGHT = 'light',
|
|
||||||
DARK = 'dark',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 主题操作类型
|
|
||||||
*/
|
|
||||||
export enum ThemeActions {
|
|
||||||
// 切换主题黑亮
|
|
||||||
CHANGE_MODE = 'change_mode',
|
|
||||||
// 反转主题黑亮
|
|
||||||
TOOGLE_MODE = 'toggle_mode',
|
|
||||||
// 切换紧凑主题
|
|
||||||
CHANGE_COMPACT = 'change_compact',
|
|
||||||
// 反转紧凑主题
|
|
||||||
TOOGLE_COMPACT = 'toggle_compact',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认配置
|
|
||||||
*/
|
|
||||||
export const defaultThemeOptions: ThemeOptions = {
|
|
||||||
mode: 'light',
|
|
||||||
compact: false,
|
|
||||||
};
|
|
|
@ -1,65 +0,0 @@
|
||||||
import { theme } from 'antd';
|
|
||||||
import { debounce } from 'lodash';
|
|
||||||
import { createContext, useCallback, useContext, useMemo } from 'react';
|
|
||||||
|
|
||||||
import { useStore } from 'zustand';
|
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
|
||||||
|
|
||||||
import { ThemeMode } from './constants';
|
|
||||||
import { ThemeState, ThemeStoreType } from './types';
|
|
||||||
|
|
||||||
export const ThemeContext = createContext<ThemeStoreType | null>(null);
|
|
||||||
|
|
||||||
// const themeStore = useContext(ThemeContext);
|
|
||||||
export function useThemeStore() {
|
|
||||||
const store = useContext(ThemeContext);
|
|
||||||
if (!store) throw new Error('Missing Theme Component in the tree');
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useThemeState<T>(selector: (state: ThemeState) => T): T {
|
|
||||||
const store = useThemeStore();
|
|
||||||
return useStore(store, selector);
|
|
||||||
}
|
|
||||||
// const { mode, compact } = themeStore.getState();
|
|
||||||
export const useTheme = () =>
|
|
||||||
useThemeState((state) => ({ mode: state.mode, compact: state.compact }));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取Antd主题算法
|
|
||||||
*/
|
|
||||||
export const useAntdAlgorithm = () => {
|
|
||||||
const { mode, compact } = useTheme();
|
|
||||||
return useMemo(() => {
|
|
||||||
const result = [compact ? theme.compactAlgorithm : theme.defaultAlgorithm];
|
|
||||||
if (mode === 'dark') result.push(theme.darkAlgorithm);
|
|
||||||
return result;
|
|
||||||
}, [mode, compact]);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 主题操作函数
|
|
||||||
*/
|
|
||||||
export const useThemeActions = () => {
|
|
||||||
const dispatch = useShallow(useThemeState((state) => state.dispatch));
|
|
||||||
return {
|
|
||||||
changeMode: useCallback(
|
|
||||||
// 防抖限制,防止快速操作
|
|
||||||
debounce(
|
|
||||||
(v: `${ThemeMode}`) => () => dispatch({ type: 'change_mode', value: v }),
|
|
||||||
100,
|
|
||||||
{},
|
|
||||||
),
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
toggleMode: useCallback(
|
|
||||||
debounce(() => dispatch({ type: 'toggle_mode' }), 100, {}),
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
changeCompact: useCallback(
|
|
||||||
(v: boolean) => dispatch({ type: 'change_compact', value: v }),
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
toggleCompact: useCallback(() => dispatch({ type: 'toggle_compact' }), []),
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,54 +0,0 @@
|
||||||
import { isNil } from 'lodash';
|
|
||||||
import { FC, PropsWithChildren, ReactNode, useContext, useRef } from 'react';
|
|
||||||
|
|
||||||
import { useLifecycles } from 'react-use';
|
|
||||||
|
|
||||||
import { ThemeContext } from './hook';
|
|
||||||
import { ThemeStoreType, createThemeStore } from './store';
|
|
||||||
import { ThemeOptions } from './types';
|
|
||||||
|
|
||||||
// 主题组件将主题配置存储在context中
|
|
||||||
const Theme: FC<PropsWithChildren<Partial<ThemeOptions>>> = ({ children, ...props }) => {
|
|
||||||
// 使用ref 持久化储存store
|
|
||||||
const storeRef = useRef<ThemeStoreType | null>(null);
|
|
||||||
if (!storeRef.current) {
|
|
||||||
storeRef.current = createThemeStore(props);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<ThemeContext.Provider value={storeRef.current}>
|
|
||||||
<ThemeSubscriber>{children}</ThemeSubscriber>
|
|
||||||
</ThemeContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ThemeSubscriber: FC<{ children: ReactNode }> = ({ children }) => {
|
|
||||||
const store = useContext(ThemeContext);
|
|
||||||
if (!store) {
|
|
||||||
throw new Error('ThemeSubscriber must be used within a Theme');
|
|
||||||
}
|
|
||||||
let unSub: () => void;
|
|
||||||
useLifecycles(
|
|
||||||
() => {
|
|
||||||
store.subscribe(
|
|
||||||
(state) => state.mode,
|
|
||||||
(m) => {
|
|
||||||
const body = document.getElementsByTagName('body');
|
|
||||||
if (body.length) {
|
|
||||||
body[0].classList.remove('light');
|
|
||||||
body[0].classList.remove('dark');
|
|
||||||
body[0].classList.add(m === 'dark' ? 'dark' : 'light');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fireImmediately: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
if (!isNil(unSub)) unSub();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return <>{children}</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Theme;
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { createPersistReduxStore } from '@3rapp/store';
|
|
||||||
import { produce } from 'immer';
|
|
||||||
|
|
||||||
import { Reducer } from 'react';
|
|
||||||
|
|
||||||
import { ThemeActions, defaultThemeOptions } from './constants';
|
|
||||||
import { ThemeDispatchs, ThemeOptions } from './types';
|
|
||||||
// 生成dispatch的reducer 没有类型会在使用的时候报错
|
|
||||||
const ThemeReducer: Reducer<ThemeOptions, ThemeDispatchs> = produce((draft, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case ThemeActions.CHANGE_MODE:
|
|
||||||
draft.mode = action.value;
|
|
||||||
break;
|
|
||||||
case ThemeActions.TOOGLE_MODE:
|
|
||||||
draft.mode = draft.mode === 'light' ? 'dark' : 'light';
|
|
||||||
break;
|
|
||||||
case ThemeActions.CHANGE_COMPACT:
|
|
||||||
draft.compact = action.value;
|
|
||||||
break;
|
|
||||||
case ThemeActions.TOOGLE_COMPACT:
|
|
||||||
draft.compact = !draft.compact;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
export const createThemeStore = (initialState: Partial<ThemeOptions> = {}) =>
|
|
||||||
createPersistReduxStore(
|
|
||||||
ThemeReducer,
|
|
||||||
{ ...initialState, ...defaultThemeOptions },
|
|
||||||
// partialize 用于过滤需要存储的数据
|
|
||||||
{ name: 'theme', partialize: (state) => ({ mode: state.mode, compact: state.compact }) },
|
|
||||||
{ name: 'theme' },
|
|
||||||
);
|
|
||||||
// 使用useref持久化储存store 以便在组件生命周期内共享
|
|
||||||
export type ThemeStoreType = ReturnType<typeof createThemeStore>;
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { ThemeActions, ThemeMode } from './constants';
|
|
||||||
import { createThemeStore } from './store';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 状态类型
|
|
||||||
*/
|
|
||||||
export type ThemeOptions = {
|
|
||||||
mode: `${ThemeMode}`;
|
|
||||||
compact: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redux dispatch
|
|
||||||
*/
|
|
||||||
export type ThemeDispatchs =
|
|
||||||
| { type: `${ThemeActions.CHANGE_MODE}`; value: `${ThemeMode}` }
|
|
||||||
| { type: `${ThemeActions.TOOGLE_MODE}` }
|
|
||||||
| { type: `${ThemeActions.CHANGE_COMPACT}`; value: boolean }
|
|
||||||
| { type: `${ThemeActions.TOOGLE_COMPACT}` };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 状态参数
|
|
||||||
*/
|
|
||||||
export type ThemeState = ThemeOptions & {
|
|
||||||
dispatch: (action: ThemeDispatchs) => ThemeDispatchs;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 状态池类型
|
|
||||||
*/
|
|
||||||
export type ThemeStoreType = ReturnType<typeof createThemeStore>;
|
|
|
@ -1,11 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom/client';
|
|
||||||
|
|
||||||
import App from './app.tsx';
|
|
||||||
import './styles/index.css';
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>,
|
|
||||||
);
|
|
|
@ -1,14 +0,0 @@
|
||||||
html,
|
|
||||||
body,
|
|
||||||
#root,
|
|
||||||
.ant-app {
|
|
||||||
@apply tw-h-[100vh] tw-w-full tw-flex tw-p-0 tw-m-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
@apply tw-bg-[url(@/assets/images/bg-light.png)] tw-bg-cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark {
|
|
||||||
@apply tw-bg-[url(@/assets/images/bg-dark.png)] tw-bg-cover;
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
@import './vars.css';
|
|
||||||
@import 'tailwindcss/base';
|
|
||||||
@import './tailwind/base.css';
|
|
||||||
@import 'tailwindcss/components';
|
|
||||||
@import './tailwind/components.css';
|
|
||||||
@import 'tailwindcss/utilities';
|
|
||||||
@import './tailwind/utilities.css';
|
|
||||||
@import './app.css';
|
|
|
@ -1,15 +0,0 @@
|
||||||
@layer tailwind-base , antd;
|
|
||||||
|
|
||||||
@layer tailwind-base {
|
|
||||||
@tailwind base;
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
html {
|
|
||||||
font-family: var(--font-family-standard);
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
@layer components {
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
@layer utilities {
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
:root {
|
|
||||||
--font-size-base: 0.875rem;
|
|
||||||
--font-family-standard: 'Source Sans Pro', 'Hiragino Sans GB', 'Microsoft Yahei', simsun,
|
|
||||||
helvetica, arial, sans-serif, monospace;
|
|
||||||
--font-family-firacode: 'Fira Code', 'Source Sans Pro', 'Hiragino Sans GB', 'Microsoft Yahei',
|
|
||||||
simsun, helvetica, arial, sans-serif, monospace;
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
/// <reference types="vite/client" />
|
|
|
@ -1,23 +0,0 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
prefix: 'tw-',
|
|
||||||
darkMode: 'class',
|
|
||||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
|
||||||
theme: {
|
|
||||||
screens: {
|
|
||||||
xs: '480px',
|
|
||||||
sm: '576px',
|
|
||||||
md: '768px',
|
|
||||||
lg: '992px',
|
|
||||||
xl: '1200px',
|
|
||||||
'2xl': '1400px',
|
|
||||||
},
|
|
||||||
extend: {
|
|
||||||
fontFamily: {
|
|
||||||
standard: 'var(--font-family-standard)',
|
|
||||||
firacode: 'var(--font-family-firacode)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
};
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"include": ["./src", "./test", "./typings", "./scripts", "**.cjs", "**.ts"],
|
|
||||||
"exclude": ["node_modules"]
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "@3rapp/core/tsconfig/react.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "./dist",
|
|
||||||
"baseUrl": "./",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["src/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": ["src"],
|
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts", "./scripts"]
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { ConfigEnv, UserConfig, defineConfig } from 'vite';
|
|
||||||
|
|
||||||
import { createConfig } from './scripts';
|
|
||||||
|
|
||||||
export default defineConfig((params: ConfigEnv): UserConfig => {
|
|
||||||
const config = createConfig(params);
|
|
||||||
return config;
|
|
||||||
});
|
|
|
@ -1,12 +0,0 @@
|
||||||
APP_NAME=3rapp
|
|
||||||
APP_HOST=127.0.0.1
|
|
||||||
APP_PORT=3000
|
|
||||||
APP_SSL=false
|
|
||||||
APP_TIMEZONE=Asia/Shanghai
|
|
||||||
APP_LOCALE=zh_CN
|
|
||||||
APP_FALLBACK_LOCALE=en
|
|
||||||
DB_HOST=172.17.0.3
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_USERNAME=root
|
|
||||||
DB_PASSWORD=123456
|
|
||||||
DB_NAME=nestapp
|
|
|
@ -1,25 +0,0 @@
|
||||||
dist
|
|
||||||
node_modules
|
|
||||||
pnpm-lock.yaml
|
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
||||||
docker
|
|
||||||
Dockerfile*
|
|
||||||
LICENSE
|
|
||||||
yarn-error.log
|
|
||||||
.history
|
|
||||||
.dockerignore
|
|
||||||
.DS_Store
|
|
||||||
.eslintignore
|
|
||||||
.editorconfig
|
|
||||||
.gitignore
|
|
||||||
.prettierignore
|
|
||||||
.eslintcache
|
|
||||||
*.lock
|
|
||||||
**/*.svg
|
|
||||||
**/*.md
|
|
||||||
**/*.svg
|
|
||||||
**/*.ejs
|
|
||||||
**/*.html
|
|
||||||
**/*.png
|
|
||||||
**/*.toml
|
|
|
@ -1,8 +0,0 @@
|
||||||
/** @type {import("eslint").Linter.Config} */
|
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
parserOptions: {
|
|
||||||
project: 'tsconfig.eslint.json',
|
|
||||||
},
|
|
||||||
extends: [require.resolve('@3rapp/core/eslint/node')],
|
|
||||||
};
|
|
|
@ -1,56 +1,28 @@
|
||||||
# compiled output
|
# dev
|
||||||
/dist
|
.yarn/
|
||||||
/node_modules
|
!.yarn/releases
|
||||||
/build
|
.vscode/*
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/*.code-snippets
|
||||||
|
.idea/workspace.xml
|
||||||
|
.idea/usage.statistics.xml
|
||||||
|
.idea/shelf
|
||||||
|
|
||||||
# Logs
|
# deps
|
||||||
logs
|
node_modules/
|
||||||
|
|
||||||
|
# env
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs/
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
pnpm-debug.log*
|
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
# OS
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Tests
|
|
||||||
/coverage
|
|
||||||
/.nyc_output
|
|
||||||
|
|
||||||
# IDEs and editors
|
|
||||||
/.idea
|
|
||||||
.project
|
|
||||||
.classpath
|
|
||||||
.c9/
|
|
||||||
*.launch
|
|
||||||
.settings/
|
|
||||||
*.sublime-workspace
|
|
||||||
|
|
||||||
# IDE - VSCode
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
|
|
||||||
# dotenv environment variable files
|
|
||||||
.env
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
.env.local
|
|
||||||
|
|
||||||
# temp directory
|
|
||||||
.temp
|
|
||||||
.tmp
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
dist
|
|
||||||
node_modules
|
|
||||||
pnpm-lock.yaml
|
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
||||||
docker
|
|
||||||
Dockerfile*
|
|
||||||
LICENSE
|
|
||||||
yarn-error.log
|
|
||||||
.history
|
|
||||||
.dockerignore
|
|
||||||
.DS_Store
|
|
||||||
.eslintignore
|
|
||||||
.editorconfig
|
|
||||||
.gitignore
|
|
||||||
.prettierignore
|
|
||||||
.eslintcache
|
|
||||||
*.lock
|
|
||||||
**/*.svg
|
|
||||||
**/*.md
|
|
||||||
**/*.svg
|
|
||||||
**/*.ejs
|
|
||||||
**/*.html
|
|
||||||
**/*.png
|
|
||||||
**/*.toml
|
|
|
@ -1 +0,0 @@
|
||||||
module.exports = require.resolve('@3rapp/core/prettier');
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
import config from '@3rapp/core/prettierrc';
|
||||||
|
export default config;
|
|
@ -1,5 +1,6 @@
|
||||||
public
|
public
|
||||||
node_modules
|
node_modules
|
||||||
|
ui
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
package-lock.json
|
package-lock.json
|
||||||
docker
|
docker
|
|
@ -1,73 +0,0 @@
|
||||||
<p align="center">
|
|
||||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
|
||||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
|
||||||
|
|
||||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
|
||||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
|
||||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
|
||||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
|
||||||
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
|
||||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
|
||||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
|
||||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
|
||||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
|
|
||||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
|
||||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
|
|
||||||
</p>
|
|
||||||
<!--[](https://opencollective.com/nest#backer)
|
|
||||||
[](https://opencollective.com/nest#sponsor)-->
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running the app
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# development
|
|
||||||
$ pnpm run start
|
|
||||||
|
|
||||||
# watch mode
|
|
||||||
$ pnpm run start:dev
|
|
||||||
|
|
||||||
# production mode
|
|
||||||
$ pnpm run start:prod
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# unit tests
|
|
||||||
$ pnpm run test
|
|
||||||
|
|
||||||
# e2e tests
|
|
||||||
$ pnpm run test:e2e
|
|
||||||
|
|
||||||
# test coverage
|
|
||||||
$ pnpm run test:cov
|
|
||||||
```
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
|
||||||
|
|
||||||
## Stay in touch
|
|
||||||
|
|
||||||
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
|
|
||||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
|
||||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Nest is [MIT licensed](LICENSE).
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"app": {
|
|
||||||
"name": "NomcqhSli"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
import eslint from '@3rapp/core/eslintrc';
|
||||||
|
export default eslint;
|
|
@ -0,0 +1,7 @@
|
||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} **/
|
||||||
|
export default {
|
||||||
|
testEnvironment: 'node',
|
||||||
|
transform: {
|
||||||
|
'^.+.tsx?$': ['ts-jest', {}],
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/nest-cli",
|
|
||||||
"collection": "@nestjs/schematics",
|
|
||||||
"sourceRoot": "src",
|
|
||||||
"compilerOptions": {
|
|
||||||
"deleteOutDir": true,
|
|
||||||
"assets": ["assets/**/*"],
|
|
||||||
"typeCheck": true,
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "@nestjs/swagger",
|
|
||||||
"options": {
|
|
||||||
"introspectComments": true,
|
|
||||||
"controllerKeyOfComment": "summary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,111 +1,54 @@
|
||||||
{
|
{
|
||||||
"name": "@3rapp/api",
|
"name": "@repo/api",
|
||||||
"version": "0.0.1",
|
"type": "module",
|
||||||
"description": "",
|
"exports": {
|
||||||
"author": "",
|
"import": {
|
||||||
"private": true,
|
"types": "./dist/es/index.d.ts",
|
||||||
"license": "UNLICENSED",
|
"default": "./dist/es/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main": "./dist/cjs/index.js",
|
||||||
|
"module": "./dist/es/index.mjs",
|
||||||
|
"types": "./dist/cjs/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start1:dev": "nest start --watch",
|
"build": "bunchee -w --tsconfig tsconfig.build.json",
|
||||||
"cli": "bun --bun src/console/bin.ts",
|
"dev": "tsx watch src/index.ts",
|
||||||
"dev": "cross-env NODE_ENV=development pnpm cli start -w",
|
"dbm": "prisma migrate dev",
|
||||||
"prod": "cross-env NODE_ENV=production pnpm cli start -w -p",
|
"dbp": "prisma db push",
|
||||||
"prodjs": "cross-env NODE_ENV=production pnpm cli start -w -p --no-ts",
|
"dbg": "prisma generate",
|
||||||
"reload": "cross-env NODE_ENV=production pnpm cli start -r",
|
"test": "jest --runInBand --detectOpenHandles --forceExit --verbose"
|
||||||
"build": "cross-env NODE_ENV=production pnpm cli build",
|
|
||||||
"start": "cross-env NODE_ENV=development nest start",
|
|
||||||
"start:dev": "cross-env NODE_ENV=development nest start --watch",
|
|
||||||
"start:debug": "cross-env NODE_ENV=development nest start --debug --watch",
|
|
||||||
"start:prod": "cross-env NODE_ENV=production node dist/main"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@3rapp/utils": "workspace:*",
|
"@3rapp/utils": "workspace:*",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@hono/node-server": "^1.13.8",
|
||||||
"@fastify/static": "^7.0.4",
|
"@hono/swagger-ui": "^0.5.0",
|
||||||
"@nestjs/common": "^10.3.10",
|
"@hono/zod-openapi": "^0.18.3",
|
||||||
"@nestjs/core": "^10.3.10",
|
"@prisma/client": " 6.3.0",
|
||||||
"@nestjs/jwt": "^10.2.0",
|
"form-data": "^4.0.1",
|
||||||
"@nestjs/passport": "^10.0.3",
|
"hono": "^4.6.20",
|
||||||
"@nestjs/platform-fastify": "^10.3.10",
|
|
||||||
"@nestjs/swagger": "^7.4.0",
|
|
||||||
"@nestjs/typeorm": "^10.0.2",
|
|
||||||
"bcrypt": "^5.1.1",
|
|
||||||
"chalk": "4",
|
|
||||||
"chokidar": "^3.6.0",
|
|
||||||
"class-transformer": "^0.5.1",
|
|
||||||
"class-validator": "^0.14.1",
|
|
||||||
"dayjs": "^1.11.11",
|
|
||||||
"dotenv": "^16.4.5",
|
|
||||||
"fastify": "^4.28.1",
|
|
||||||
"find-up": "5",
|
|
||||||
"fs-extra": "^11.2.0",
|
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"meilisearch": "^0.41.0",
|
"openai": "^4.83.0",
|
||||||
"mysql2": "^3.11.0",
|
"prisma-paginate": "^5.2.1",
|
||||||
"ora": "5",
|
"uuid": "^11.0.5"
|
||||||
"passport": "^0.7.0",
|
|
||||||
"passport-jwt": "^4.0.1",
|
|
||||||
"passport-local": "^1.0.0",
|
|
||||||
"pm2": "^5.4.2",
|
|
||||||
"reflect-metadata": "^0.2.2",
|
|
||||||
"rxjs": "^7.8.1",
|
|
||||||
"sanitize-html": "^2.13.0",
|
|
||||||
"typeorm": "^0.3.20",
|
|
||||||
"utility-types": "^3.11.0",
|
|
||||||
"uuid": "^10.0.0",
|
|
||||||
"validator": "^13.12.0",
|
|
||||||
"yaml": "^2.5.0",
|
|
||||||
"yargs": "^17.7.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@3rapp/core": "workspace:*",
|
"@3rapp/core": "workspace:*",
|
||||||
"@nestjs/cli": "^10.4.2",
|
"@types/jest": "^29.5.14",
|
||||||
"@nestjs/schematics": "^10.1.3",
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
"@nestjs/testing": "^10.3.10",
|
"@types/lodash": "^4.17.15",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/node": "^20.11.17",
|
||||||
"@types/fs-extra": "^11.0.4",
|
|
||||||
"@types/jest": "^29.5.12",
|
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
|
||||||
"@types/lodash": "^4.17.7",
|
|
||||||
"@types/node": "^20.14.12",
|
|
||||||
"@types/passport-jwt": "^4.0.1",
|
|
||||||
"@types/passport-local": "^1.0.38",
|
|
||||||
"@types/sanitize-html": "^2.11.0",
|
|
||||||
"@types/supertest": "^6.0.2",
|
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@types/validator": "^13.12.0",
|
"bunchee": "^6.3.2",
|
||||||
"@types/yargs": "^17.0.32",
|
|
||||||
"bun": "^1.1.21",
|
|
||||||
"bun-types": "^1.1.21",
|
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"eslint": "^8.57.0",
|
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"nodemon": "^3.1.4",
|
"prisma": " 6.3.0",
|
||||||
"prettier": "^3.3.3",
|
"ts-jest": "^29.2.5",
|
||||||
"source-map-support": "^0.5.21",
|
"tsx": "^4.7.1"
|
||||||
"supertest": "^7.0.0",
|
|
||||||
"ts-jest": "^29.2.3",
|
|
||||||
"ts-loader": "^9.5.1",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"tsconfig-paths": "^4.2.0",
|
|
||||||
"typescript": "^5.5.4"
|
|
||||||
},
|
},
|
||||||
"jest": {
|
"prisma": {
|
||||||
"moduleFileExtensions": [
|
"schema": "src/database/schema"
|
||||||
"js",
|
|
||||||
"json",
|
|
||||||
"ts"
|
|
||||||
],
|
|
||||||
"rootDir": "src",
|
|
||||||
"testRegex": ".*\\.spec\\.ts$",
|
|
||||||
"transform": {
|
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
|
||||||
},
|
|
||||||
"collectCoverageFrom": [
|
|
||||||
"**/*.(t|j)s"
|
|
||||||
],
|
|
||||||
"coverageDirectory": "../coverage",
|
|
||||||
"testEnvironment": "node"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,22 @@
|
||||||
|
import OpenAI from 'openai';
|
||||||
|
|
||||||
|
const openai = new OpenAI({
|
||||||
|
baseURL: 'https://api.deepseek.com/v1',
|
||||||
|
apiKey: 'sk-626f7d17d5544477a1844eb174e54c4e',
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const completion = await openai.chat.completions.create({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: '網站設計有哪些可以參考的,或者ai設計網站頁面佈局',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
model: 'deepseek-chat',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(completion.choices[0].message.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
|
@ -0,0 +1,6 @@
|
||||||
|
import type { AppConfig } from '@/libs/types';
|
||||||
|
|
||||||
|
export const appConfig: AppConfig = {
|
||||||
|
baseUrl: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
|
||||||
|
apiUrl: process.env.NEXT_PUBLIC_APi_URL || `${process.env.NEXT_PUBLIC_APP_URL}/api`,
|
||||||
|
};
|
|
@ -0,0 +1,6 @@
|
||||||
|
import type { AuthConfig } from '@/server/auth/type';
|
||||||
|
|
||||||
|
export const authConfig: AuthConfig = {
|
||||||
|
jwtSecret: process.env.AUTH_JWT_SECRET || 'hTVLuGqhuKZW9HUnKzs83yvVBitlwc5d0PNfJqDRsRs=',
|
||||||
|
jwtExpiration: '5d',
|
||||||
|
};
|
|
@ -1,49 +0,0 @@
|
||||||
import { Configure } from '@/modules/config/configure';
|
|
||||||
import { ConfigureFactory } from '@/modules/config/types';
|
|
||||||
import * as contentControllers from '@/modules/content/controllers';
|
|
||||||
import { ApiConfig, VersionOption } from '@/modules/restful/types';
|
|
||||||
|
|
||||||
export const v1 = async (configure: Configure): Promise<VersionOption> => {
|
|
||||||
return {
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
name: 'app',
|
|
||||||
path: '/',
|
|
||||||
controllers: [],
|
|
||||||
doc: {
|
|
||||||
// title: '应用接口',
|
|
||||||
description:
|
|
||||||
'3R教室《Nestjs实战开发》课程应用的客户端接口(应用名称随机自动生成)',
|
|
||||||
tags: [
|
|
||||||
{ name: '分类操作', description: '对分类进行CRUD操作' },
|
|
||||||
{ name: '标签操作', description: '对标签进行CRUD操作' },
|
|
||||||
{ name: '文章操作', description: '对文章进行CRUD操作' },
|
|
||||||
{ name: '评论操作', description: '对评论进行CRUD操作' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'app.content',
|
|
||||||
path: 'content',
|
|
||||||
controllers: Object.values(contentControllers),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const api: ConfigureFactory<ApiConfig> = {
|
|
||||||
register: async (configure: Configure) => ({
|
|
||||||
title: configure.env.get(
|
|
||||||
'API_TITLE',
|
|
||||||
`${await configure.get<string>('app.name')} app的API接口`,
|
|
||||||
),
|
|
||||||
// description: configure.env.get('API_DESCRIPTION', '3R教室TS全栈开发教程'),
|
|
||||||
auth: true,
|
|
||||||
docuri: 'api/docs',
|
|
||||||
default: configure.env.get('API_DEFAULT_VERSION', 'v1'),
|
|
||||||
enabled: [],
|
|
||||||
versions: { v1: await v1(configure) },
|
|
||||||
}),
|
|
||||||
};
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { toNumber } from 'lodash';
|
|
||||||
|
|
||||||
import { createAppConfig } from '@/modules/core/config';
|
|
||||||
|
|
||||||
export const app = createAppConfig((configure) => {
|
|
||||||
return {
|
|
||||||
port: configure.env.get('APP_PORT', (v) => toNumber(v), 8080),
|
|
||||||
prefix: configure.env.get('APP_PREFIX', '/apis'),
|
|
||||||
};
|
|
||||||
});
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { ContentConfig } from '@/modules/content/types';
|
|
||||||
|
|
||||||
export const content = (): ContentConfig => ({
|
|
||||||
searchType: 'meilisearch',
|
|
||||||
});
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Configure } from '@/modules/config/configure';
|
|
||||||
|
|
||||||
export const database = (configure: Configure): TypeOrmModuleOptions => {
|
|
||||||
return {
|
|
||||||
type: 'mysql',
|
|
||||||
host: configure.env.get('DB_HOST'),
|
|
||||||
port: configure.env.get('DB_PORT', 3306),
|
|
||||||
username: configure.env.get('DB_USER', 'root'),
|
|
||||||
password: configure.env.get('DB_PASSWORD', '123456'),
|
|
||||||
database: configure.env.get('DB_NAME', 'nestapp'),
|
|
||||||
synchronize: true,
|
|
||||||
autoLoadEntities: true,
|
|
||||||
subscribers: [],
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,6 +0,0 @@
|
||||||
export * from './app.config';
|
|
||||||
export * from './database.config';
|
|
||||||
export * from './content.config';
|
|
||||||
export * from './meili.config';
|
|
||||||
export * from './api.config';
|
|
||||||
export * from './user.config';
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { MelliConfig } from '@/modules/meilisearch/types';
|
|
||||||
|
|
||||||
export const meili = (): MelliConfig => [
|
|
||||||
{
|
|
||||||
name: 'default',
|
|
||||||
host: 'http://192.168.31.43:7700/',
|
|
||||||
},
|
|
||||||
];
|
|
|
@ -1,3 +0,0 @@
|
||||||
import { createUserConfig } from '@/modules/user/config';
|
|
||||||
|
|
||||||
export const user = createUserConfig(() => ({}));
|
|
|
@ -1,36 +0,0 @@
|
||||||
// import { NestFactory } from '@nestjs/core';
|
|
||||||
// import { NestFastifyApplication, FastifyAdapter } from '@nestjs/platform-fastify';
|
|
||||||
|
|
||||||
// import * as configs from '@/configs';
|
|
||||||
|
|
||||||
// import { ContentModule } from './modules/content/content.module';
|
|
||||||
// import { CreateOptions } from './modules/core/core.types';
|
|
||||||
// import { DatabaseModule } from './modules/database/database.module';
|
|
||||||
// import { meiliForRoot } from './modules/meilisearch/melli.module';
|
|
||||||
// import { Restful } from './modules/restful/restful';
|
|
||||||
// import { RestfulModuleForRoot } from './modules/restful/restful.module';
|
|
||||||
|
|
||||||
// export const createData: CreateOptions = {
|
|
||||||
// config: { factories: configs as any, storage: { enabled: true } },
|
|
||||||
// imports: async (configure) => [
|
|
||||||
// DatabaseModule.forRoot(configure),
|
|
||||||
// meiliForRoot(configure),
|
|
||||||
// RestfulModuleForRoot(configure),
|
|
||||||
// ContentModule.forRoot(configure),
|
|
||||||
// ],
|
|
||||||
// globals: {},
|
|
||||||
// builder: async ({ configure, BootModule }) => {
|
|
||||||
// const container = await NestFactory.create<NestFastifyApplication>(
|
|
||||||
// BootModule,
|
|
||||||
// new FastifyAdapter(),
|
|
||||||
// {
|
|
||||||
// cors: true,
|
|
||||||
// logger: ['error', 'warn'],
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
// // 在此处构建swagger文档
|
|
||||||
// const restful = container.get(Restful);
|
|
||||||
// await restful.factoryDocs(container);
|
|
||||||
// return container;
|
|
||||||
// },
|
|
||||||
// };
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'USER');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "users" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"username" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"role" "UserRole" NOT NULL DEFAULT 'USER',
|
||||||
|
|
||||||
|
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
|
@ -0,0 +1,18 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "posts" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"thumb" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"summary" TEXT,
|
||||||
|
"body" TEXT NOT NULL,
|
||||||
|
"slug" TEXT,
|
||||||
|
"keywords" TEXT,
|
||||||
|
"description" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "posts_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "posts_slug_key" ON "posts"("slug");
|
|
@ -0,0 +1,5 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "posts" ADD COLUMN "authorId" TEXT;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "posts" ADD CONSTRAINT "posts_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
@ -0,0 +1,12 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "File" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"fileName" TEXT NOT NULL,
|
||||||
|
"fileType" TEXT NOT NULL,
|
||||||
|
"fileSize" INTEGER NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"filePath" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "File_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (e.g., Git)
|
||||||
|
provider = "postgresql"
|
|
@ -0,0 +1,22 @@
|
||||||
|
model File {
|
||||||
|
/// 文件的唯一标识符
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
/// 文件创建的时间戳
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
/// 文件的名称
|
||||||
|
fileName String
|
||||||
|
|
||||||
|
/// 文件的类型(例如:pdf, docx)
|
||||||
|
fileType String
|
||||||
|
|
||||||
|
/// 文件的大小(单位:字节)
|
||||||
|
fileSize Int
|
||||||
|
|
||||||
|
/// 创建文件的用户ID
|
||||||
|
userId String
|
||||||
|
|
||||||
|
/// 文件在存储系统中的路径
|
||||||
|
filePath String
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
model Post {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
thumb String
|
||||||
|
title String
|
||||||
|
summary String?
|
||||||
|
body String
|
||||||
|
slug String? @unique
|
||||||
|
keywords String?
|
||||||
|
description String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
author User? @relation(fields: [authorId], references: [id])
|
||||||
|
authorId String?
|
||||||
|
|
||||||
|
@@map("posts")
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
enum UserRole {
|
||||||
|
ADMIN
|
||||||
|
USER
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
username String @unique
|
||||||
|
email String @unique
|
||||||
|
password String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
role UserRole @default(USER)
|
||||||
|
posts Post[]
|
||||||
|
|
||||||
|
@@map("users")
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import db from '@/libs/db/prismaClient';
|
||||||
|
const item = async () => {
|
||||||
|
await db.post.create({
|
||||||
|
data: {
|
||||||
|
authorId: '2431e468-dcd0-424a-9270-d29d8df70318',
|
||||||
|
title: '/posts/create',
|
||||||
|
summary: '/posts/create',
|
||||||
|
keywords: '/posts/create',
|
||||||
|
description: '/posts/create',
|
||||||
|
slug: 'posts-create111',
|
||||||
|
body: '/posts/create/posts/create/posts/create/posts/create/posts/create/posts/create',
|
||||||
|
thumb: `/uploads/thumb/post-1.png`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
console.log(item());
|
||||||
|
|
||||||
|
// export const createPostData = async () => {
|
||||||
|
// // 为避免重复添加数据,在重新运行数据填充时,清空已有文章数据
|
||||||
|
// await db.post.$truncate();
|
||||||
|
// for (let index = 0; index < 90; index++) {
|
||||||
|
// await db.post.create({
|
||||||
|
// select: { id: true },
|
||||||
|
// data: {
|
||||||
|
// user: null,
|
||||||
|
|
||||||
|
// // 随机封面图
|
||||||
|
// thumb: `/uploads/thumb/post-${getRandomInt(1, 8)}.png`,
|
||||||
|
// // 生成1到3个段落的标题
|
||||||
|
// title: faker.lorem.paragraph({ min: 1, max: 2 }),
|
||||||
|
// // 生成3-6个段落的内容并把每个段落用换行符换行
|
||||||
|
// body: faker.lorem.paragraphs(getRandomInt(4, 9), '/n'),
|
||||||
|
// // 有49%的机率会生成一段摘要
|
||||||
|
// summary: faker.lorem.paragraph({ min: 1, max: 2 }),
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// };
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './libs/api';
|
||||||
|
export * from './server/main';
|
|
@ -0,0 +1,8 @@
|
||||||
|
import type { AppType } from '@/server/main';
|
||||||
|
import type { ClientRequestOptions } from 'hono/client';
|
||||||
|
|
||||||
|
import { appConfig } from '@/config/app';
|
||||||
|
import { hc } from 'hono/client';
|
||||||
|
|
||||||
|
export const createClient = (options: ClientRequestOptions) =>
|
||||||
|
hc<AppType>(appConfig.baseUrl, options);
|
|
@ -1,15 +1,15 @@
|
||||||
|
/* eslint-disable no-var */
|
||||||
/* eslint-disable vars-on-top */
|
/* eslint-disable vars-on-top */
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
declare global {
|
|
||||||
// eslint-disable-next-line no-var
|
|
||||||
var prismaGlobal: undefined | ReturnType<typeof prismaClientSingleton>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prismaClientSingleton = () => {
|
const prismaClientSingleton = () => {
|
||||||
return new PrismaClient();
|
return new PrismaClient();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var prismaGlobal: undefined | ReturnType<typeof prismaClientSingleton>;
|
||||||
|
}
|
||||||
|
|
||||||
const db = globalThis.prismaGlobal ?? prismaClientSingleton();
|
const db = globalThis.prismaGlobal ?? prismaClientSingleton();
|
||||||
|
|
||||||
export default db;
|
export default db;
|
|
@ -0,0 +1,232 @@
|
||||||
|
// Helpers.
|
||||||
|
const s = 1000;
|
||||||
|
const m = s * 60;
|
||||||
|
const h = m * 60;
|
||||||
|
const d = h * 24;
|
||||||
|
const w = d * 7;
|
||||||
|
const y = d * 365.25;
|
||||||
|
|
||||||
|
type Unit =
|
||||||
|
| 'Years'
|
||||||
|
| 'Year'
|
||||||
|
| 'Yrs'
|
||||||
|
| 'Yr'
|
||||||
|
| 'Y'
|
||||||
|
| 'Weeks'
|
||||||
|
| 'Week'
|
||||||
|
| 'W'
|
||||||
|
| 'Days'
|
||||||
|
| 'Day'
|
||||||
|
| 'D'
|
||||||
|
| 'Hours'
|
||||||
|
| 'Hour'
|
||||||
|
| 'Hrs'
|
||||||
|
| 'Hr'
|
||||||
|
| 'H'
|
||||||
|
| 'Minutes'
|
||||||
|
| 'Minute'
|
||||||
|
| 'Mins'
|
||||||
|
| 'Min'
|
||||||
|
| 'M'
|
||||||
|
| 'Seconds'
|
||||||
|
| 'Second'
|
||||||
|
| 'Secs'
|
||||||
|
| 'Sec'
|
||||||
|
| 's'
|
||||||
|
| 'Milliseconds'
|
||||||
|
| 'Millisecond'
|
||||||
|
| 'Msecs'
|
||||||
|
| 'Msec'
|
||||||
|
| 'Ms';
|
||||||
|
|
||||||
|
type UnitAnyCase = Unit | Uppercase<Unit> | Lowercase<Unit>;
|
||||||
|
|
||||||
|
export type StringValue = `${number}` | `${number}${UnitAnyCase}` | `${number} ${UnitAnyCase}`;
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
/**
|
||||||
|
* Set to `true` to use verbose formatting. Defaults to `false`.
|
||||||
|
*/
|
||||||
|
long?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse or format the given value.
|
||||||
|
*
|
||||||
|
* @param value - The string or number to convert
|
||||||
|
* @param options - Options for the conversion
|
||||||
|
* @throws Error if `value` is not a non-empty string or a number
|
||||||
|
*/
|
||||||
|
function msFn(value: StringValue, options?: Options): number;
|
||||||
|
function msFn(value: number, options?: Options): string;
|
||||||
|
function msFn(value: StringValue | number, options?: Options): number | string {
|
||||||
|
try {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return parse(value);
|
||||||
|
}
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return format(value, options);
|
||||||
|
}
|
||||||
|
throw new Error('Value provided to ms() must be a string or number.');
|
||||||
|
} catch (error) {
|
||||||
|
const message = isError(error)
|
||||||
|
? `${error.message}. value=${JSON.stringify(value)}`
|
||||||
|
: 'An unknown error has occurred.';
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the given string and return milliseconds.
|
||||||
|
*
|
||||||
|
* @param str - A string to parse to milliseconds
|
||||||
|
* @returns The parsed value in milliseconds, or `NaN` if the string can't be
|
||||||
|
* parsed
|
||||||
|
*/
|
||||||
|
export function parse(str: string): number {
|
||||||
|
if (typeof str !== 'string' || str.length === 0 || str.length > 100) {
|
||||||
|
throw new Error(
|
||||||
|
'Value provided to ms.parse() must be a string with length between 1 and 99.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const match =
|
||||||
|
/^(?<value>-?(?:\d+(?:\.\d+)?|\.\d+)) *(?<type>milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(
|
||||||
|
str,
|
||||||
|
);
|
||||||
|
// Named capture groups need to be manually typed today.
|
||||||
|
// https://github.com/microsoft/TypeScript/issues/32098
|
||||||
|
const groups = match?.groups as { value: string; type?: string } | undefined;
|
||||||
|
if (!groups) {
|
||||||
|
return Number.NaN;
|
||||||
|
}
|
||||||
|
const n = Number.parseFloat(groups.value);
|
||||||
|
const type = (groups.type || 'ms').toLowerCase() as Lowercase<Unit>;
|
||||||
|
switch (type) {
|
||||||
|
case 'years':
|
||||||
|
case 'year':
|
||||||
|
case 'yrs':
|
||||||
|
case 'yr':
|
||||||
|
case 'y':
|
||||||
|
return n * y;
|
||||||
|
case 'weeks':
|
||||||
|
case 'week':
|
||||||
|
case 'w':
|
||||||
|
return n * w;
|
||||||
|
case 'days':
|
||||||
|
case 'day':
|
||||||
|
case 'd':
|
||||||
|
return n * d;
|
||||||
|
case 'hours':
|
||||||
|
case 'hour':
|
||||||
|
case 'hrs':
|
||||||
|
case 'hr':
|
||||||
|
case 'h':
|
||||||
|
return n * h;
|
||||||
|
case 'minutes':
|
||||||
|
case 'minute':
|
||||||
|
case 'mins':
|
||||||
|
case 'min':
|
||||||
|
case 'm':
|
||||||
|
return n * m;
|
||||||
|
case 'seconds':
|
||||||
|
case 'second':
|
||||||
|
case 'secs':
|
||||||
|
case 'sec':
|
||||||
|
case 's':
|
||||||
|
return n * s;
|
||||||
|
case 'milliseconds':
|
||||||
|
case 'millisecond':
|
||||||
|
case 'msecs':
|
||||||
|
case 'msec':
|
||||||
|
case 'ms':
|
||||||
|
return n;
|
||||||
|
default:
|
||||||
|
// This should never occur.
|
||||||
|
throw new Error(`The unit ${type as string} was matched, but no matching case exists.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the given StringValue and return milliseconds.
|
||||||
|
*
|
||||||
|
* @param value - A typesafe StringValue to parse to milliseconds
|
||||||
|
* @returns The parsed value in milliseconds, or `NaN` if the string can't be
|
||||||
|
* parsed
|
||||||
|
*/
|
||||||
|
export function parseStrict(value: StringValue): number {
|
||||||
|
return parse(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default msFn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Short format for `ms`.
|
||||||
|
*/
|
||||||
|
function fmtShort(ms: number): StringValue {
|
||||||
|
const msAbs = Math.abs(ms);
|
||||||
|
if (msAbs >= d) {
|
||||||
|
return `${Math.round(ms / d)}d`;
|
||||||
|
}
|
||||||
|
if (msAbs >= h) {
|
||||||
|
return `${Math.round(ms / h)}h`;
|
||||||
|
}
|
||||||
|
if (msAbs >= m) {
|
||||||
|
return `${Math.round(ms / m)}m`;
|
||||||
|
}
|
||||||
|
if (msAbs >= s) {
|
||||||
|
return `${Math.round(ms / s)}s`;
|
||||||
|
}
|
||||||
|
return `${ms}ms`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Long format for `ms`.
|
||||||
|
*/
|
||||||
|
function fmtLong(ms: number): StringValue {
|
||||||
|
const msAbs = Math.abs(ms);
|
||||||
|
if (msAbs >= d) {
|
||||||
|
return plural(ms, msAbs, d, 'day');
|
||||||
|
}
|
||||||
|
if (msAbs >= h) {
|
||||||
|
return plural(ms, msAbs, h, 'hour');
|
||||||
|
}
|
||||||
|
if (msAbs >= m) {
|
||||||
|
return plural(ms, msAbs, m, 'minute');
|
||||||
|
}
|
||||||
|
if (msAbs >= s) {
|
||||||
|
return plural(ms, msAbs, s, 'second');
|
||||||
|
}
|
||||||
|
return `${ms} ms`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the given integer as a string.
|
||||||
|
*
|
||||||
|
* @param ms - milliseconds
|
||||||
|
* @param options - Options for the conversion
|
||||||
|
* @returns The formatted string
|
||||||
|
*/
|
||||||
|
export function format(ms: number, options?: Options): string {
|
||||||
|
if (typeof ms !== 'number' || !Number.isFinite(ms)) {
|
||||||
|
throw new TypeError('Value provided to ms.format() must be of type number.');
|
||||||
|
}
|
||||||
|
return options?.long ? fmtLong(ms) : fmtShort(ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pluralization helper.
|
||||||
|
*/
|
||||||
|
function plural(ms: number, msAbs: number, n: number, name: string): StringValue {
|
||||||
|
const isPlural = msAbs >= n * 1.5;
|
||||||
|
return `${Math.round(ms / n)} ${name}${isPlural ? 's' : ''}` as StringValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type guard for errors.
|
||||||
|
*
|
||||||
|
* @param value - The value to test
|
||||||
|
* @returns A boolean `true` if the provided value is an Error-like object
|
||||||
|
*/
|
||||||
|
function isError(value: unknown): value is Error {
|
||||||
|
return typeof value === 'object' && value !== null && 'message' in value;
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
// eslint-disable-next-line unicorn/prefer-node-protocol
|
||||||
|
import { randomBytes, scryptSync, timingSafeEqual } from 'crypto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成密码哈希
|
||||||
|
* @param password 原始密码
|
||||||
|
* @returns 格式: salt.hash
|
||||||
|
*/
|
||||||
|
export const hashPassword = (password: string) => {
|
||||||
|
try {
|
||||||
|
const salt = randomBytes(16).toString('hex');
|
||||||
|
const hash = scryptSync(password, salt, 64, {
|
||||||
|
N: 16384,
|
||||||
|
r: 8,
|
||||||
|
p: 1,
|
||||||
|
}).toString('hex');
|
||||||
|
return `${salt}.${hash}`;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw new TypeError(error.message);
|
||||||
|
} else {
|
||||||
|
throw new TypeError('Unknown error occurred');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证密码
|
||||||
|
* @param password 原始密码
|
||||||
|
* @param hash 密码哈希
|
||||||
|
* @returns 验证成功返回 true,否则返回 false
|
||||||
|
*/
|
||||||
|
export const verifyPassword = (password: string, hash: string): boolean => {
|
||||||
|
try {
|
||||||
|
const [salt, storedHash] = hash.split('.');
|
||||||
|
if (!salt || !storedHash) return false;
|
||||||
|
const newHashBuffers = scryptSync(password, salt, 64, {
|
||||||
|
N: 16384,
|
||||||
|
r: 8,
|
||||||
|
p: 1,
|
||||||
|
});
|
||||||
|
return timingSafeEqual(newHashBuffers, Buffer.from(storedHash, 'hex'));
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw new TypeError(error.message);
|
||||||
|
} else {
|
||||||
|
throw new TypeError('Unknown error occurred');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,35 @@
|
||||||
|
import type { AuthItem } from '@/server/auth/type';
|
||||||
|
|
||||||
|
import { authConfig } from '@/config/auth';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
import { parseStrict } from './ms';
|
||||||
|
|
||||||
|
export const ACCESS_TOKEN_COOKIE_NAME = 'auth_token';
|
||||||
|
// 生成 token
|
||||||
|
export const generateAccessToken = (user: AuthItem) => {
|
||||||
|
const expiryMs = parseStrict(authConfig.jwtExpiration);
|
||||||
|
return jwt.sign(
|
||||||
|
{
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
role: user.role,
|
||||||
|
exp: Math.floor(Date.now() / 1000) + expiryMs,
|
||||||
|
},
|
||||||
|
authConfig.jwtSecret,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
// 验证 token
|
||||||
|
export const getTokenExpirationTime = (token: string) => {
|
||||||
|
const decoded = jwt.decode(token);
|
||||||
|
|
||||||
|
if (decoded === null || typeof decoded === 'string') throw new Error('令牌解析错误');
|
||||||
|
const expiresIn = Number(decoded.exp) * 1000 - Date.now();
|
||||||
|
if (expiresIn < 0) throw new Error('令牌已过期');
|
||||||
|
return expiresIn;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAccessTokenFromHeader = (req: any) => {
|
||||||
|
const authHeader = req.headers?.authorization;
|
||||||
|
return authHeader.startsWith('Bearer ') ? authHeader.substring(7) : null;
|
||||||
|
};
|
|
@ -0,0 +1,8 @@
|
||||||
|
// 定义一个类型 Date2String,用于将对象中 Date 类型的属性转换为 string 类型 遍歷參數T的每個屬性,如果屬性是 Date 類型,則轉換為 string 類型,否則保持原樣。
|
||||||
|
export type Date2String<T> = {
|
||||||
|
[P in keyof T]: T[P] extends Date ? string : T[P];
|
||||||
|
};
|
||||||
|
export interface AppConfig {
|
||||||
|
apiUrl: string;
|
||||||
|
baseUrl: string;
|
||||||
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
import { createApp, startApp } from './modules/core/helpers/app';
|
|
||||||
import { createOptions } from './options';
|
|
||||||
|
|
||||||
startApp(createApp(createOptions));
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { createMiddleware } from 'hono/factory';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
export const getUserMiddleware = createMiddleware(async (c, next) => {
|
||||||
|
const Authorization = c.req.header('authorization');
|
||||||
|
console.log(Authorization);
|
||||||
|
const token = Authorization?.split(' ')[1] || '';
|
||||||
|
const users = jwt.decode(token) as any;
|
||||||
|
console.log(users);
|
||||||
|
c.set('user', users);
|
||||||
|
await next();
|
||||||
|
});
|
|
@ -1,17 +0,0 @@
|
||||||
import { Configure } from './configure';
|
|
||||||
|
|
||||||
export class ConfigModule {
|
|
||||||
static forRoot(configure: Configure) {
|
|
||||||
return {
|
|
||||||
global: true,
|
|
||||||
exports: [Configure],
|
|
||||||
module: ConfigModule,
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: Configure,
|
|
||||||
useValue: configure,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,219 +0,0 @@
|
||||||
import { deepMerge, isAsyncFn } from '@3rapp/utils';
|
|
||||||
import { get, has, isArray, isFunction, isNil, isObject, omit, set } from 'lodash';
|
|
||||||
|
|
||||||
import { Env } from './env';
|
|
||||||
import { ConfigStorage } from './storage';
|
|
||||||
import { ConfigStorageOption, ConfigureFactory, ConfigureRegister } from './types';
|
|
||||||
|
|
||||||
interface SetStorageOption {
|
|
||||||
/**
|
|
||||||
* 是否存储配置
|
|
||||||
*/
|
|
||||||
enabled?: boolean;
|
|
||||||
/**
|
|
||||||
* 用于指定如果该配置已经存在在config.yml中时要不要更改
|
|
||||||
*/
|
|
||||||
change?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 配置类
|
|
||||||
*/
|
|
||||||
export class Configure {
|
|
||||||
/**
|
|
||||||
* 配置是否被初始化
|
|
||||||
*/
|
|
||||||
protected inited = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 配置构建函数对象
|
|
||||||
*/
|
|
||||||
protected factories: Record<string, ConfigureFactory<Record<string, any>>> = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成的配置
|
|
||||||
*/
|
|
||||||
protected config: Record<string, any> = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 环境变量操作实例
|
|
||||||
*/
|
|
||||||
protected _env: Env;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 存储配置操作实例
|
|
||||||
*/
|
|
||||||
protected storage: ConfigStorage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化配置类
|
|
||||||
* @param configs 配置构造器集合对象
|
|
||||||
* @param option 配置类选项
|
|
||||||
*/
|
|
||||||
async initilize(
|
|
||||||
configs: Record<string, ConfigureFactory<Record<string, any>>> = {},
|
|
||||||
option: ConfigStorageOption = {},
|
|
||||||
) {
|
|
||||||
if (this.inited) return this;
|
|
||||||
this._env = new Env();
|
|
||||||
await this._env.load();
|
|
||||||
const { enabled, filePath } = option;
|
|
||||||
this.storage = new ConfigStorage(enabled, filePath);
|
|
||||||
for (const key in configs) {
|
|
||||||
this.add(key, configs[key]);
|
|
||||||
}
|
|
||||||
await this.sync();
|
|
||||||
this.inited = true;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
get env() {
|
|
||||||
return this._env;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有配置
|
|
||||||
*/
|
|
||||||
all() {
|
|
||||||
return this.config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断配置是否存在
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
has(key: string) {
|
|
||||||
return has(this.config, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取配置值
|
|
||||||
* @param key
|
|
||||||
* @param defaultValue 不存在时返回的默认配置
|
|
||||||
*/
|
|
||||||
async get<T>(key: string, defaultValue?: T): Promise<T> {
|
|
||||||
if (!has(this.config, key) && defaultValue === undefined && has(this.factories, key)) {
|
|
||||||
await this.syncFactory(key);
|
|
||||||
return this.get(key, defaultValue);
|
|
||||||
}
|
|
||||||
return get(this.config, key, defaultValue) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置配置项
|
|
||||||
* 如果storage是一个布尔值则只用于确定是否存储该配置,如果该配置已存在在config.yml中则不更改
|
|
||||||
* 如果storage是一个对象时,那么其中的enabled用于设置是否存储改配置,change用于指定如果该配置已经存在在config.yml中时要不要更改
|
|
||||||
* @param key 配置名
|
|
||||||
* @param value 配置值
|
|
||||||
* @param storage boolean类型时: 是否存储配置; 对象类型时: enabled 是否存储配置, change 当该配置在config.yml存在时是否改变
|
|
||||||
* @param append 如果为true,则如果已经存在的包含数组的配置,使用追加方式合并,否则直接替换
|
|
||||||
*/
|
|
||||||
set<T>(key: string, value: T, storage: SetStorageOption | boolean = false, append = false) {
|
|
||||||
const storageEnable = typeof storage === 'boolean' ? storage : !!storage.enabled;
|
|
||||||
const storageChange = typeof storage === 'boolean' ? false : !!storage.change;
|
|
||||||
if (storageEnable && this.storage.enabled) {
|
|
||||||
this.changeStorageValue(key, value, storageChange, append);
|
|
||||||
} else {
|
|
||||||
set(this.config, key, value);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加一个新配置集
|
|
||||||
* @param key
|
|
||||||
* @param register 配置构造器
|
|
||||||
*/
|
|
||||||
add<T extends Record<string, any>>(
|
|
||||||
key: string,
|
|
||||||
register: ConfigureRegister<T> | ConfigureFactory<T>,
|
|
||||||
) {
|
|
||||||
if (!isFunction(register) && 'register' in register) {
|
|
||||||
this.factories[key] = register as any;
|
|
||||||
} else if (isFunction(register)) {
|
|
||||||
this.factories[key] = { register };
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除配置项
|
|
||||||
* 如果不是存储配置则为临时删除,重启用户后该配置依然存在
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
remove(key: string) {
|
|
||||||
if (has(this.storage.config, key) && this.storage.enabled) {
|
|
||||||
this.storage.remove(key);
|
|
||||||
this.config = deepMerge(this.config, this.storage.config, 'replace');
|
|
||||||
} else if (has(this.config, key)) {
|
|
||||||
this.config = omit(this.config, [key]);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 手动存储一个配置
|
|
||||||
* @param key 配置名
|
|
||||||
* @param change 用于指定如果该配置已经存在在config.yml中时要不要更改
|
|
||||||
* @param append 如果为true,则如果已经存在的包含数组的配置,使用追加方式合并,否则直接替换
|
|
||||||
*/
|
|
||||||
async store(key: string, change = false, append = false) {
|
|
||||||
if (!this.storage.enabled) throw new Error('Must enable storage at first!');
|
|
||||||
this.changeStorageValue(key, await this.get(key, null), change, append);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 同步配置
|
|
||||||
* 添加一个配置构造器后需用使用此方法同步到配置中
|
|
||||||
*/
|
|
||||||
async sync(name?: string) {
|
|
||||||
if (!isNil(name)) await this.syncFactory(name);
|
|
||||||
else {
|
|
||||||
for (const key in this.factories) {
|
|
||||||
await this.syncFactory(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 同步配置构造器
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
protected async syncFactory(key: string) {
|
|
||||||
if (has(this.config, key) || !has(this.factories, key)) return this;
|
|
||||||
const { register, defaultRegister, storage, hook, append } = this.factories[key];
|
|
||||||
let defaultValue = {};
|
|
||||||
let value = isAsyncFn(register) ? await register(this) : register(this);
|
|
||||||
if (!isNil(defaultRegister)) {
|
|
||||||
defaultValue = isAsyncFn(defaultRegister)
|
|
||||||
? await defaultRegister(this)
|
|
||||||
: defaultRegister(this);
|
|
||||||
value = deepMerge(defaultValue, value, 'replace');
|
|
||||||
}
|
|
||||||
if (!isNil(hook)) {
|
|
||||||
value = isAsyncFn(hook) ? await hook(this, value) : hook(this, value);
|
|
||||||
}
|
|
||||||
if (this.storage.enabled) {
|
|
||||||
value = deepMerge(value, get(this.storage.config, key, isArray(value) ? [] : {}));
|
|
||||||
}
|
|
||||||
this.set(key, value, storage && isNil(await this.get(key, null)), append);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected changeStorageValue<T>(key: string, value: T, change = false, append = false) {
|
|
||||||
if (change || !has(this.storage.config, key)) {
|
|
||||||
this.storage.set(key, value);
|
|
||||||
} else if (isObject(get(this.storage.config, key))) {
|
|
||||||
this.storage.set(
|
|
||||||
key,
|
|
||||||
deepMerge(value, get(this.storage.config, key), append ? 'merge' : 'replace'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.config = deepMerge(this.config, this.storage.config, append ? 'merge' : 'replace');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const a = new Configure();
|
|
||||||
a.initilize();
|
|
||||||
console.log(a.env);
|
|
|
@ -1,8 +0,0 @@
|
||||||
export enum EnvironmentType {
|
|
||||||
DEVELOPMENT = 'development',
|
|
||||||
DEV = 'dev',
|
|
||||||
PRODUCTION = 'production',
|
|
||||||
PROD = 'prod',
|
|
||||||
TEST = 'test',
|
|
||||||
PREVIEW = 'preview',
|
|
||||||
}
|
|
|
@ -1,140 +0,0 @@
|
||||||
import { readFileSync } from 'fs';
|
|
||||||
|
|
||||||
import { parse } from 'dotenv';
|
|
||||||
import findUp from 'find-up';
|
|
||||||
import { isNil, isFunction } from 'lodash';
|
|
||||||
|
|
||||||
import { EnvironmentType } from './constants';
|
|
||||||
|
|
||||||
export class Env {
|
|
||||||
/**
|
|
||||||
* 加载环境变量
|
|
||||||
*/
|
|
||||||
async load() {
|
|
||||||
/**
|
|
||||||
* 默认在开发环境运行
|
|
||||||
* eslint 报错就去turbo.jsoon文件中添加"tasks": {"build": {"dependsOn": ["$NODE_ENV"]
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (isNil(process.env.NODE_ENV)) process.env.NODE_ENV = EnvironmentType.DEVELOPMENT;
|
|
||||||
// 从当前运行应用的目录开始,向上查找.env文件,直到找到第一个文件为止
|
|
||||||
// 没有找到则返回undefined
|
|
||||||
const search = [findUp.sync(['.env'])];
|
|
||||||
// 从当前运行应用的目录开始,向上寻找.env.{环境变量文件},直到找到第一个文件为止,如.env.local
|
|
||||||
// 如果是development、dev、production、prod环境则同时查找两个
|
|
||||||
// 没有找到则返回undefined
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
||||||
if (this.isDev()) {
|
|
||||||
search.push(
|
|
||||||
findUp.sync([`.env.${EnvironmentType.DEVELOPMENT}`, `.env.${EnvironmentType.DEV}`]),
|
|
||||||
);
|
|
||||||
} else if (this.isProd()) {
|
|
||||||
search.push(
|
|
||||||
findUp.sync([`.env.${EnvironmentType.PRODUCTION}`, `.env.${EnvironmentType.PROD}`]),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
search.push(findUp.sync([`.env.${this.run()}`]));
|
|
||||||
}
|
|
||||||
// 过滤掉undefined,把找到的环境变量文件放入envFiles数组
|
|
||||||
const envFiles = search.filter((file) => file !== undefined);
|
|
||||||
// 转义每个环境变量文件中的内容为一个对象并让前者覆盖合并后者
|
|
||||||
// 如.env.{环境变量文件}会覆盖合并.env
|
|
||||||
// 然后,得到所有文件中配置的环境变量对象
|
|
||||||
const fileEnvs = envFiles
|
|
||||||
.map((filePath) => parse(readFileSync(filePath)))
|
|
||||||
.reduce(
|
|
||||||
(oc, nc) => ({
|
|
||||||
...oc,
|
|
||||||
...nc,
|
|
||||||
}),
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
// 文件环境变量与系统环境变量合并
|
|
||||||
const envs = { ...process.env, ...fileEnvs };
|
|
||||||
// 过滤出在envs中存在而在process.env中不存在的键
|
|
||||||
const keys = Object.keys(envs).filter((key) => !(key in process.env));
|
|
||||||
// 把.env*中存在而系统环境变量中不存在的键值对追加到process.env中
|
|
||||||
// 这样就可以得到最终环境变量 process.env
|
|
||||||
keys.forEach((key) => {
|
|
||||||
process.env[key] = envs[key];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当前运行环境,如果production, development等
|
|
||||||
*/
|
|
||||||
run() {
|
|
||||||
return process.env.NODE_ENV as EnvironmentType & RecordAny;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否在生产环境运行
|
|
||||||
*/
|
|
||||||
isProd() {
|
|
||||||
return this.run() === EnvironmentType.PRODUCTION || this.run() === EnvironmentType.PROD;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否在开发环境运行
|
|
||||||
*/
|
|
||||||
isDev() {
|
|
||||||
return this.run() === EnvironmentType.DEVELOPMENT || this.run() === EnvironmentType.DEV;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取全部环境变量
|
|
||||||
*/
|
|
||||||
get(): { [key: string]: string };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 直接获取环境变量
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
get<T extends BaseType = string>(key: string): T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取类型转义后的环境变量
|
|
||||||
* @param key
|
|
||||||
* @param parseTo 类型转义函数
|
|
||||||
*/
|
|
||||||
get<T extends BaseType = string>(key: string, parseTo: ParseType<T>): T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取环境变量,不存在则获取默认值
|
|
||||||
* @param key
|
|
||||||
* @param defaultValue 默认值
|
|
||||||
*/
|
|
||||||
get<T extends BaseType = string>(key: string, defaultValue: T): T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取类型转义后的环境变量,不存在则获取默认值
|
|
||||||
* @param key
|
|
||||||
* @param parseTo 类型转义函数
|
|
||||||
* @param defaultValue 默认值
|
|
||||||
*/
|
|
||||||
get<T extends BaseType = string>(key: string, parseTo: ParseType<T>, defaultValue: T): T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取环境变量
|
|
||||||
* @param key
|
|
||||||
* @param parseTo 类型转义函数
|
|
||||||
* @param defaultValue 默认值
|
|
||||||
*/
|
|
||||||
get<T extends BaseType = string>(key?: string, parseTo?: ParseType<T> | T, defaultValue?: T) {
|
|
||||||
if (!key) return process.env;
|
|
||||||
const value = process.env[key];
|
|
||||||
if (value !== undefined) {
|
|
||||||
if (parseTo && isFunction(parseTo)) {
|
|
||||||
return parseTo(value);
|
|
||||||
}
|
|
||||||
return value as T;
|
|
||||||
}
|
|
||||||
if (parseTo === undefined && defaultValue === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (parseTo && defaultValue === undefined) {
|
|
||||||
return isFunction(parseTo) ? undefined : parseTo;
|
|
||||||
}
|
|
||||||
return defaultValue! as T;
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue