121
parent
ba0825a019
commit
131db241a3
|
@ -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,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.21.1",
|
|
||||||
"antd": "^5.21.6",
|
|
||||||
"antd-style": "^3.7.1",
|
|
||||||
"axios": "^1.7.7",
|
|
||||||
"classnames": "^2.5.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"dayjs": "^1.11.13",
|
|
||||||
"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.1",
|
|
||||||
"utility-types": "^3.11.0",
|
|
||||||
"zustand": "^5.0.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@3rapp/core": "workspace:*",
|
|
||||||
"@3rapp/utils": "workspace:*",
|
|
||||||
"@types/lodash": "^4.17.13",
|
|
||||||
"@types/node": "^22.8.6",
|
|
||||||
"@types/react": "^18.3.12",
|
|
||||||
"@types/react-dom": "^18.3.1",
|
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
|
||||||
"autoprefixer": "^10.4.20",
|
|
||||||
"eslint": "^9.14.0",
|
|
||||||
"postcss-import": "^16.1.0",
|
|
||||||
"postcss-mixins": "^11.0.3",
|
|
||||||
"postcss-nested": "^7.0.2",
|
|
||||||
"postcss-nesting": "^13.0.1",
|
|
||||||
"prettier": "^3.3.3",
|
|
||||||
"stylelint": "^16.10.0",
|
|
||||||
"tailwindcss": "^3.4.14",
|
|
||||||
"typescript": "^5.6.3",
|
|
||||||
"vite": "^5.4.10"
|
|
||||||
}
|
|
||||||
}
|
|
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 +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="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
|
Before Width: | Height: | Size: 4.0 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 +0,0 @@
|
||||||
# compiled output
|
|
||||||
/dist
|
|
||||||
/node_modules
|
|
||||||
/build
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.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');
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@3rapp/api",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"description": "",
|
|
||||||
"author": "",
|
|
||||||
"private": true,
|
|
||||||
"license": "UNLICENSED",
|
|
||||||
"scripts": {
|
|
||||||
"start1:dev": "nest start --watch",
|
|
||||||
"cli": "bun --bun src/console/bin.ts",
|
|
||||||
"dev": "cross-env NODE_ENV=development pnpm cli start -w",
|
|
||||||
"prod": "cross-env NODE_ENV=production pnpm cli start -w -p",
|
|
||||||
"prodjs": "cross-env NODE_ENV=production pnpm cli start -w -p --no-ts",
|
|
||||||
"reload": "cross-env NODE_ENV=production pnpm cli start -r",
|
|
||||||
"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": {
|
|
||||||
"@3rapp/utils": "workspace:*",
|
|
||||||
"@faker-js/faker": "^9.1.0",
|
|
||||||
"@fastify/static": "^8.0.2",
|
|
||||||
"@nestjs/common": "^10.4.6",
|
|
||||||
"@nestjs/core": "^10.4.6",
|
|
||||||
"@nestjs/jwt": "^10.2.0",
|
|
||||||
"@nestjs/passport": "^10.0.3",
|
|
||||||
"@nestjs/platform-fastify": "^10.4.6",
|
|
||||||
"@nestjs/swagger": "^8.0.1",
|
|
||||||
"@nestjs/typeorm": "^10.0.2",
|
|
||||||
"bcrypt": "^5.1.1",
|
|
||||||
"chalk": "4",
|
|
||||||
"chokidar": "^4.0.1",
|
|
||||||
"class-transformer": "^0.5.1",
|
|
||||||
"class-validator": "^0.14.1",
|
|
||||||
"dayjs": "^1.11.13",
|
|
||||||
"dotenv": "^16.4.5",
|
|
||||||
"fastify": "^5.1.0",
|
|
||||||
"find-up": "5",
|
|
||||||
"fs-extra": "^11.2.0",
|
|
||||||
"jsonwebtoken": "^9.0.2",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"meilisearch": "^0.45.0",
|
|
||||||
"mysql2": "^3.11.3",
|
|
||||||
"ora": "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.1",
|
|
||||||
"typeorm": "^0.3.20",
|
|
||||||
"utility-types": "^3.11.0",
|
|
||||||
"uuid": "^11.0.2",
|
|
||||||
"validator": "^13.12.0",
|
|
||||||
"yaml": "^2.6.0",
|
|
||||||
"yargs": "^17.7.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@3rapp/core": "workspace:*",
|
|
||||||
"@nestjs/cli": "^10.4.5",
|
|
||||||
"@nestjs/schematics": "^10.2.3",
|
|
||||||
"@nestjs/testing": "^10.4.6",
|
|
||||||
"@types/bcrypt": "^5.0.2",
|
|
||||||
"@types/fs-extra": "^11.0.4",
|
|
||||||
"@types/jest": "^29.5.14",
|
|
||||||
"@types/jsonwebtoken": "^9.0.7",
|
|
||||||
"@types/lodash": "^4.17.13",
|
|
||||||
"@types/node": "^22.8.6",
|
|
||||||
"@types/passport-jwt": "^4.0.1",
|
|
||||||
"@types/passport-local": "^1.0.38",
|
|
||||||
"@types/sanitize-html": "^2.13.0",
|
|
||||||
"@types/supertest": "^6.0.2",
|
|
||||||
"@types/uuid": "^10.0.0",
|
|
||||||
"@types/validator": "^13.12.2",
|
|
||||||
"@types/yargs": "^17.0.33",
|
|
||||||
"bun": "^1.1.33",
|
|
||||||
"bun-types": "^1.1.33",
|
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"eslint": "^9.14.0",
|
|
||||||
"jest": "^29.7.0",
|
|
||||||
"nodemon": "^3.1.7",
|
|
||||||
"prettier": "^3.3.3",
|
|
||||||
"source-map-support": "^0.5.21",
|
|
||||||
"supertest": "^7.0.0",
|
|
||||||
"ts-jest": "^29.2.5",
|
|
||||||
"ts-loader": "^9.5.1",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"tsconfig-paths": "^4.2.0",
|
|
||||||
"typescript": "^5.6.3"
|
|
||||||
},
|
|
||||||
"jest": {
|
|
||||||
"moduleFileExtensions": [
|
|
||||||
"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
|
@ -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;
|
|
||||||
// },
|
|
||||||
// };
|
|
|
@ -1,4 +0,0 @@
|
||||||
import { createApp, startApp } from './modules/core/helpers/app';
|
|
||||||
import { createOptions } from './options';
|
|
||||||
|
|
||||||
startApp(createApp(createOptions));
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
import findUp from 'find-up';
|
|
||||||
import { ensureFileSync, readFileSync, writeFileSync } from 'fs-extra';
|
|
||||||
import { isNil, set, omit, has } from 'lodash';
|
|
||||||
import YAML from 'yaml';
|
|
||||||
/** storage.ts
|
|
||||||
* 存储配置
|
|
||||||
*/
|
|
||||||
export class ConfigStorage {
|
|
||||||
/**
|
|
||||||
* 是否开启存储配置功能
|
|
||||||
*/
|
|
||||||
protected _enabled = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* yaml文件配置路径
|
|
||||||
*/
|
|
||||||
protected _path = findUp.sync(['config.yml']);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 存储在yaml中的配置对象
|
|
||||||
*/
|
|
||||||
protected _config: Record<string, any> = {};
|
|
||||||
|
|
||||||
get enabled() {
|
|
||||||
return this._enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
get path() {
|
|
||||||
return this._path;
|
|
||||||
}
|
|
||||||
|
|
||||||
get config() {
|
|
||||||
return this._config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param enabled 是否开启存储配置功能
|
|
||||||
* @param filePath 存盘配置存放的文件路径
|
|
||||||
*/
|
|
||||||
constructor(enabled?: boolean, filePath?: string) {
|
|
||||||
this._enabled = isNil(enabled) ? this._enabled : enabled;
|
|
||||||
if (this._enabled) {
|
|
||||||
if (!isNil(filePath)) this._path = filePath;
|
|
||||||
ensureFileSync(this._path);
|
|
||||||
const config = YAML.parse(readFileSync(this._path, 'utf8'));
|
|
||||||
this._config = isNil(config) ? {} : config;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置存储配置
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
set<T>(key: string, value: T) {
|
|
||||||
ensureFileSync(this.path);
|
|
||||||
set(this._config, key, value);
|
|
||||||
writeFileSync(this.path, JSON.stringify(this._config, null, 4));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除存储配置
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
remove(key: string) {
|
|
||||||
this._config = omit(this._config, [key]);
|
|
||||||
if (has(this._config, key)) omit(this._config, [key]);
|
|
||||||
writeFileSync(this.path, JSON.stringify(this._config, null, 4));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
|
||||||
|
|
||||||
import { CommandModule } from 'yargs';
|
|
||||||
|
|
||||||
import { Configure } from './configure';
|
|
||||||
|
|
||||||
export interface ConfigStorageOption {
|
|
||||||
/**
|
|
||||||
* 是否开启存储
|
|
||||||
*/
|
|
||||||
enabled?: boolean;
|
|
||||||
/**
|
|
||||||
* yaml文件路径,默认为dist目录外的config.yaml
|
|
||||||
*/
|
|
||||||
filePath?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 配置注册器函数
|
|
||||||
*/
|
|
||||||
export type ConfigureRegister<T extends Record<string, any>> = (
|
|
||||||
configure: Configure,
|
|
||||||
) => T | Promise<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 配置构造器
|
|
||||||
*/
|
|
||||||
export interface ConfigureFactory<
|
|
||||||
T extends Record<string, any>,
|
|
||||||
C extends Record<string, any> = T,
|
|
||||||
> {
|
|
||||||
/**
|
|
||||||
* 配置注册器
|
|
||||||
*/
|
|
||||||
register: ConfigureRegister<RePartial<T>>;
|
|
||||||
/**
|
|
||||||
* 默认配置注册器
|
|
||||||
*/
|
|
||||||
defaultRegister?: ConfigureRegister<T>;
|
|
||||||
/**
|
|
||||||
* 是否存储该配置
|
|
||||||
*/
|
|
||||||
storage?: boolean;
|
|
||||||
/**
|
|
||||||
* 回调函数
|
|
||||||
* @param configure 配置类服务实例
|
|
||||||
* @param value 配置注册器register执行后的返回值
|
|
||||||
*/
|
|
||||||
hook?: (configure: Configure, value: T) => C | Promise<C>;
|
|
||||||
/**
|
|
||||||
* 深度合并时是否对数组采用追加模式,默认 false
|
|
||||||
*/
|
|
||||||
append?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 多连接连接型配置
|
|
||||||
*/
|
|
||||||
export type ConnectionOption<T extends Record<string, any>> = { name?: string } & T;
|
|
||||||
/**
|
|
||||||
* 多连接连接型配置生成的结果
|
|
||||||
*/
|
|
||||||
export type ConnectionRst<T extends Record<string, any>> = Array<{ name: string } & T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* App对象类型
|
|
||||||
*/
|
|
||||||
export type App = {
|
|
||||||
// 应用容器实例
|
|
||||||
container?: NestFastifyApplication;
|
|
||||||
// 配置类实例
|
|
||||||
configure: Configure;
|
|
||||||
/**
|
|
||||||
* 命令列表
|
|
||||||
*/
|
|
||||||
commands?: CommandModule<RecordAny, RecordAny>[];
|
|
||||||
};
|
|
|
@ -1,26 +0,0 @@
|
||||||
/**
|
|
||||||
* 文章内容类型
|
|
||||||
*/
|
|
||||||
export enum PostBodyType {
|
|
||||||
HTML = 'html',
|
|
||||||
MD = 'markdown',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文章排序类型
|
|
||||||
*/
|
|
||||||
export enum PostOrderType {
|
|
||||||
CREATED = 'createdAt',
|
|
||||||
UPDATED = 'updatedAt',
|
|
||||||
PUBLISHED = 'publishedAt',
|
|
||||||
COMMENTCOUNT = 'commentCounts',
|
|
||||||
CUSTOM = 'custom',
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* all - 选择所有 only - 仅选择回收站 none - 不选择回收站
|
|
||||||
*/
|
|
||||||
export enum SelectTrashMode {
|
|
||||||
ALL = 'all',
|
|
||||||
ONLY = 'only',
|
|
||||||
NONE = 'none',
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
import { ModuleMetadata } from '@nestjs/common';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Configure } from '../config/configure';
|
|
||||||
import { DatabaseModule } from '../database/database.module';
|
|
||||||
|
|
||||||
import * as controllers from './controllers';
|
|
||||||
import * as entities from './entities';
|
|
||||||
import * as repositories from './repositories';
|
|
||||||
import * as services from './services';
|
|
||||||
import { MeiliSearchService } from './services/meili.search.service';
|
|
||||||
import { PostService } from './services/post.service';
|
|
||||||
import { SanitizeService } from './services/sanitize.service';
|
|
||||||
import { CommentSubscriber } from './subscribers/comment.subscriber';
|
|
||||||
import { PostSubscriber } from './subscribers/post.subscriber';
|
|
||||||
import { ContentConfig } from './types';
|
|
||||||
|
|
||||||
export const forRoot = async (configure: Configure) => {
|
|
||||||
const config: Required<ContentConfig> = await configure.get('content');
|
|
||||||
const providers: ModuleMetadata['providers'] = [
|
|
||||||
...Object.values(services),
|
|
||||||
SanitizeService,
|
|
||||||
PostSubscriber,
|
|
||||||
CommentSubscriber,
|
|
||||||
{
|
|
||||||
provide: 'CONTENT_CONFIG',
|
|
||||||
useValue: config,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: PostService,
|
|
||||||
inject: [
|
|
||||||
repositories.PostRepository,
|
|
||||||
repositories.CategoryRepository,
|
|
||||||
services.CategoryService,
|
|
||||||
repositories.TagRepository,
|
|
||||||
MeiliSearchService,
|
|
||||||
],
|
|
||||||
useFactory(
|
|
||||||
postRepository: repositories.PostRepository,
|
|
||||||
categoryRepository: repositories.CategoryRepository,
|
|
||||||
categoryService: services.CategoryService,
|
|
||||||
tagRepository: repositories.TagRepository,
|
|
||||||
searchService: MeiliSearchService,
|
|
||||||
) {
|
|
||||||
return new PostService(
|
|
||||||
postRepository,
|
|
||||||
categoryRepository,
|
|
||||||
categoryService,
|
|
||||||
tagRepository,
|
|
||||||
searchService,
|
|
||||||
config.searchType,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
providers.push(MeiliSearchService);
|
|
||||||
return {
|
|
||||||
module: class ContentModule {},
|
|
||||||
imports: [
|
|
||||||
TypeOrmModule.forFeature(Object.values(entities)),
|
|
||||||
DatabaseModule.forRepository(Object.values(repositories)),
|
|
||||||
],
|
|
||||||
controllers: Object.values(controllers),
|
|
||||||
providers,
|
|
||||||
exports: [
|
|
||||||
...Object.values(services),
|
|
||||||
PostService,
|
|
||||||
DatabaseModule.forRepository(Object.values(repositories)),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,79 +0,0 @@
|
||||||
import {
|
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
SerializeOptions,
|
|
||||||
Query,
|
|
||||||
Param,
|
|
||||||
ParseUUIDPipe,
|
|
||||||
Post,
|
|
||||||
Body,
|
|
||||||
Patch,
|
|
||||||
Delete,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
import { SelectTrashMode } from '../constants';
|
|
||||||
import { QueryCategoryDto, CreateCategoryDto, UpdateCategoryDto } from '../dtos';
|
|
||||||
import { CategoryService } from '../services';
|
|
||||||
|
|
||||||
@Controller('categories')
|
|
||||||
export class CategoryController {
|
|
||||||
constructor(protected service: CategoryService) {}
|
|
||||||
|
|
||||||
@Get('tree')
|
|
||||||
@SerializeOptions({ groups: ['category-tree'] })
|
|
||||||
async tree() {
|
|
||||||
return this.service.findTrees({ trashed: SelectTrashMode.NONE });
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@SerializeOptions({ groups: ['category-list'] })
|
|
||||||
async list(
|
|
||||||
@Query()
|
|
||||||
options: QueryCategoryDto,
|
|
||||||
) {
|
|
||||||
return this.service.paginate(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(':id')
|
|
||||||
@SerializeOptions({ groups: ['category-detail'] })
|
|
||||||
async detail(
|
|
||||||
@Param('id', new ParseUUIDPipe())
|
|
||||||
id: string,
|
|
||||||
) {
|
|
||||||
return this.service.detail(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
@SerializeOptions({ groups: ['category-detail', 'create'] })
|
|
||||||
async store(
|
|
||||||
@Body()
|
|
||||||
data: CreateCategoryDto,
|
|
||||||
) {
|
|
||||||
return this.service.create(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch()
|
|
||||||
@SerializeOptions({ groups: ['category-detail'] })
|
|
||||||
async update(
|
|
||||||
@Body()
|
|
||||||
data: UpdateCategoryDto,
|
|
||||||
) {
|
|
||||||
return this.service.update(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete()
|
|
||||||
@SerializeOptions({ groups: ['category-detail'] })
|
|
||||||
async delete(@Body() data: { ids: string[] }) {
|
|
||||||
const { ids } = data;
|
|
||||||
|
|
||||||
return this.service.delete(ids, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('restore')
|
|
||||||
@SerializeOptions({ groups: ['category-detail'] })
|
|
||||||
async restore(@Body() data: { ids: string[] }) {
|
|
||||||
const { ids } = data;
|
|
||||||
|
|
||||||
return this.service.restore(ids);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { Controller, Get, SerializeOptions, Query, Post, Body, Delete } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { DeleteDto } from '@/modules/restful/dtos/delete.dto';
|
|
||||||
|
|
||||||
import { QueryCommentTreeDto, QueryCommentDto, CreateCommentDto } from '../dtos';
|
|
||||||
import { CommentService } from '../services';
|
|
||||||
|
|
||||||
@Controller('comments')
|
|
||||||
export class CommentController {
|
|
||||||
constructor(protected service: CommentService) {}
|
|
||||||
|
|
||||||
@Get('tree')
|
|
||||||
@SerializeOptions({ groups: ['comment-tree'] })
|
|
||||||
async tree(
|
|
||||||
@Query()
|
|
||||||
query: QueryCommentTreeDto,
|
|
||||||
) {
|
|
||||||
return this.service.findTrees(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@SerializeOptions({ groups: ['comment-list'] })
|
|
||||||
async list(
|
|
||||||
@Query()
|
|
||||||
query: QueryCommentDto,
|
|
||||||
) {
|
|
||||||
return this.service.paginate(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
@SerializeOptions({ groups: ['comment-detail'] })
|
|
||||||
async store(
|
|
||||||
@Body()
|
|
||||||
data: CreateCommentDto,
|
|
||||||
) {
|
|
||||||
return this.service.create(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete()
|
|
||||||
@SerializeOptions({ groups: ['comment-detail'] })
|
|
||||||
async delete(@Body() ids: DeleteDto) {
|
|
||||||
return this.service.delete(ids.ids);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export * from './category.controller';
|
|
||||||
export * from './tag.controller';
|
|
||||||
export * from './post.controller';
|
|
||||||
export * from './comment.controller';
|
|
|
@ -1,69 +0,0 @@
|
||||||
import {
|
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
Param,
|
|
||||||
ParseUUIDPipe,
|
|
||||||
Patch,
|
|
||||||
Post,
|
|
||||||
Query,
|
|
||||||
SerializeOptions,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
import { DeleteWithTrashDto, RestoreDto } from '@/modules/restful/dtos';
|
|
||||||
|
|
||||||
import { QueryPostDto, UpdatePostDto } from '../dtos/post.dto';
|
|
||||||
import { PostService } from '../services/post.service';
|
|
||||||
|
|
||||||
@Controller('posts')
|
|
||||||
export class PostController {
|
|
||||||
constructor(protected postservice: PostService) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@SerializeOptions({ groups: ['post-list'] })
|
|
||||||
async list(
|
|
||||||
@Query()
|
|
||||||
options: QueryPostDto,
|
|
||||||
) {
|
|
||||||
return this.postservice.paginate(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(':id')
|
|
||||||
@SerializeOptions({ groups: ['post-detail'] })
|
|
||||||
async detail(
|
|
||||||
@Param('id', new ParseUUIDPipe())
|
|
||||||
id: string,
|
|
||||||
) {
|
|
||||||
return this.postservice.detail(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
@SerializeOptions({ groups: ['post-detail'] })
|
|
||||||
async store(
|
|
||||||
@Body()
|
|
||||||
data: any,
|
|
||||||
) {
|
|
||||||
return this.postservice.create(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch()
|
|
||||||
@SerializeOptions({ groups: ['post-detail'] })
|
|
||||||
async update(
|
|
||||||
@Body()
|
|
||||||
data: UpdatePostDto,
|
|
||||||
) {
|
|
||||||
return this.postservice.update(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('delete')
|
|
||||||
@SerializeOptions({ groups: ['post-list'] })
|
|
||||||
async delete(@Body() data: DeleteWithTrashDto) {
|
|
||||||
return this.postservice.delete(data.ids, data.transh);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('restore')
|
|
||||||
@SerializeOptions({ groups: ['post-list'] })
|
|
||||||
async restore(@Body() data: RestoreDto) {
|
|
||||||
return this.postservice.restore(data.ids);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
import {
|
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
SerializeOptions,
|
|
||||||
Query,
|
|
||||||
Param,
|
|
||||||
ParseUUIDPipe,
|
|
||||||
Post,
|
|
||||||
Body,
|
|
||||||
Patch,
|
|
||||||
Delete,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
import { QueryCategoryDto, CreateTagDto, UpdateTagDto } from '../dtos';
|
|
||||||
import { TagService } from '../services';
|
|
||||||
|
|
||||||
@Controller('tags')
|
|
||||||
export class TagController {
|
|
||||||
constructor(protected service: TagService) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@SerializeOptions({})
|
|
||||||
async list(
|
|
||||||
@Query()
|
|
||||||
options: QueryCategoryDto,
|
|
||||||
) {
|
|
||||||
return this.service.paginate(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(':id')
|
|
||||||
@SerializeOptions({})
|
|
||||||
async detail(
|
|
||||||
@Param('id', new ParseUUIDPipe())
|
|
||||||
id: string,
|
|
||||||
) {
|
|
||||||
return this.service.detail(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
@SerializeOptions({})
|
|
||||||
async store(
|
|
||||||
@Body()
|
|
||||||
data: CreateTagDto,
|
|
||||||
) {
|
|
||||||
return this.service.create(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch()
|
|
||||||
@SerializeOptions({})
|
|
||||||
async update(
|
|
||||||
@Body()
|
|
||||||
data: UpdateTagDto,
|
|
||||||
) {
|
|
||||||
return this.service.update(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete(':id')
|
|
||||||
@SerializeOptions({})
|
|
||||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
|
||||||
return this.service.delete(id);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
import { PartialType } from '@nestjs/swagger';
|
|
||||||
|
|
||||||
import { Transform } from 'class-transformer';
|
|
||||||
import {
|
|
||||||
Min,
|
|
||||||
IsNumber,
|
|
||||||
IsOptional,
|
|
||||||
MaxLength,
|
|
||||||
IsNotEmpty,
|
|
||||||
IsUUID,
|
|
||||||
ValidateIf,
|
|
||||||
IsDefined,
|
|
||||||
IsEnum,
|
|
||||||
} from 'class-validator';
|
|
||||||
import { toNumber } from 'lodash';
|
|
||||||
|
|
||||||
import { DtoValidation } from '@/modules/core/decorators/dto-validation.decorator';
|
|
||||||
import { IsDataExist } from '@/modules/database/constraints';
|
|
||||||
import { PaginateOptions } from '@/modules/database/types';
|
|
||||||
|
|
||||||
import { SelectTrashMode } from '../constants';
|
|
||||||
import { CategoryEntity } from '../entities';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 树形分类查询验证
|
|
||||||
*/
|
|
||||||
@DtoValidation({ type: 'query' })
|
|
||||||
export class QueryCategoryTreeDto {
|
|
||||||
@IsEnum(SelectTrashMode)
|
|
||||||
@IsOptional()
|
|
||||||
trashed?: SelectTrashMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@DtoValidation({ type: 'query' })
|
|
||||||
export class QueryCategoryDto extends QueryCategoryTreeDto implements PaginateOptions {
|
|
||||||
@Transform(({ value }) => toNumber(value))
|
|
||||||
@Min(1, { message: '当前页必须大于1' })
|
|
||||||
@IsNumber()
|
|
||||||
@IsOptional()
|
|
||||||
page = 1;
|
|
||||||
|
|
||||||
@Transform(({ value }) => toNumber(value))
|
|
||||||
@Min(1, { message: '每页显示数据必须大于1' })
|
|
||||||
@IsNumber()
|
|
||||||
@IsOptional()
|
|
||||||
limit = 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分类新增验证
|
|
||||||
*/
|
|
||||||
@DtoValidation({ groups: ['create'] })
|
|
||||||
export class CreateCategoryDto {
|
|
||||||
// @IsTreeUnique(CategoryEntity, { always: true, message: '分类名称已存在' })
|
|
||||||
// @IsTreeUniqueExist(CategoryEntity, { always: true, message: '父分类不存在1' })
|
|
||||||
@MaxLength(25, {
|
|
||||||
always: true,
|
|
||||||
message: '分类名称长度不得超过$constraint1',
|
|
||||||
})
|
|
||||||
@IsNotEmpty({ groups: ['create'], message: '分类名称不得为空' })
|
|
||||||
@IsOptional({ groups: ['update'] })
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@IsDataExist({ entity: CategoryEntity, map: 'id' }, { always: true, message: '父分类不存在' })
|
|
||||||
@IsUUID(undefined, { always: true, message: '父分类ID格式不正确' })
|
|
||||||
@ValidateIf((value) => value.parent !== null && value.parent)
|
|
||||||
@IsOptional({ always: true })
|
|
||||||
@Transform(({ value }) => (value === 'null' ? null : value))
|
|
||||||
parent?: string;
|
|
||||||
|
|
||||||
@Transform(({ value }) => toNumber(value))
|
|
||||||
@Min(0, { always: true, message: '排序值必须大于0' })
|
|
||||||
@IsNumber(undefined, { always: true })
|
|
||||||
@IsOptional({ always: true })
|
|
||||||
customOrder?: number = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分类更新验证
|
|
||||||
*/
|
|
||||||
@DtoValidation({ groups: ['update'] })
|
|
||||||
export class UpdateCategoryDto extends PartialType(CreateCategoryDto) {
|
|
||||||
@IsUUID(undefined, { groups: ['update'], message: 'ID格式错误' })
|
|
||||||
@IsDefined({ groups: ['update'], message: 'ID必须指定' })
|
|
||||||
id: string;
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
import { PickType } from '@nestjs/swagger';
|
|
||||||
|
|
||||||
import { Transform } from 'class-transformer';
|
|
||||||
import {
|
|
||||||
IsUUID,
|
|
||||||
IsOptional,
|
|
||||||
Min,
|
|
||||||
IsNumber,
|
|
||||||
MaxLength,
|
|
||||||
IsNotEmpty,
|
|
||||||
IsDefined,
|
|
||||||
ValidateIf,
|
|
||||||
} from 'class-validator';
|
|
||||||
import { toNumber } from 'lodash';
|
|
||||||
|
|
||||||
import { DtoValidation } from '@/modules/core/decorators/dto-validation.decorator';
|
|
||||||
|
|
||||||
import { IsDataExist } from '@/modules/database/constraints';
|
|
||||||
import { PaginateOptions } from '@/modules/database/types';
|
|
||||||
|
|
||||||
import { CommentEntity, PostEntity } from '../entities';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 评论分页查询验证
|
|
||||||
*/
|
|
||||||
@DtoValidation({ type: 'query' })
|
|
||||||
export class QueryCommentDto implements PaginateOptions {
|
|
||||||
@IsDataExist(PostEntity, { always: true, message: '文章不存在' })
|
|
||||||
@IsUUID(undefined, { message: 'ID格式错误' })
|
|
||||||
@IsOptional()
|
|
||||||
post?: string;
|
|
||||||
|
|
||||||
@Transform(({ value }) => toNumber(value))
|
|
||||||
@Min(1, { message: '当前页必须大于1' })
|
|
||||||
@IsNumber()
|
|
||||||
@IsOptional()
|
|
||||||
page = 1;
|
|
||||||
|
|
||||||
@Transform(({ value }) => toNumber(value))
|
|
||||||
@Min(1, { message: '每页显示数据必须大于1' })
|
|
||||||
@IsNumber()
|
|
||||||
@IsOptional()
|
|
||||||
limit = 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 评论树查询
|
|
||||||
*/
|
|
||||||
@DtoValidation({ type: 'query' })
|
|
||||||
export class QueryCommentTreeDto extends PickType(QueryCommentDto, ['post']) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 评论添加验证
|
|
||||||
*/
|
|
||||||
@DtoValidation()
|
|
||||||
export class CreateCommentDto {
|
|
||||||
@MaxLength(1000, { message: '评论内容不能超过$constraint1个字' })
|
|
||||||
@IsNotEmpty({ message: '评论内容不能为空' })
|
|
||||||
body: string;
|
|
||||||
|
|
||||||
@IsDataExist(PostEntity, { always: true, message: '文章不存在' })
|
|
||||||
@IsUUID(undefined, { message: 'ID格式错误' })
|
|
||||||
@IsDefined({ message: 'ID必须指定' })
|
|
||||||
post: string;
|
|
||||||
|
|
||||||
@IsDataExist(CommentEntity, { always: true, message: '评论ID错误' })
|
|
||||||
@IsUUID(undefined, { always: true, message: 'ID格式错误' })
|
|
||||||
@ValidateIf((value) => value.parent !== null && value.parent)
|
|
||||||
@IsOptional({ always: true })
|
|
||||||
@Transform(({ value }) => (value === 'null' ? null : value))
|
|
||||||
parent?: string;
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
export * from './category.dto';
|
|
||||||
export * from './comment.dto';
|
|
||||||
export * from './post.dto';
|
|
||||||
export * from './tag.dto';
|
|
||||||
// export * from ''
|
|
|
@ -1,151 +0,0 @@
|
||||||
import { toBoolean } from '@3rapp/utils';
|
|
||||||
import { PartialType } from '@nestjs/swagger';
|
|
||||||
import { Transform } from 'class-transformer';
|
|
||||||
import {
|
|
||||||
IsBoolean,
|
|
||||||
IsOptional,
|
|
||||||
IsEnum,
|
|
||||||
Min,
|
|
||||||
IsNumber,
|
|
||||||
MaxLength,
|
|
||||||
IsNotEmpty,
|
|
||||||
ValidateIf,
|
|
||||||
IsUUID,
|
|
||||||
IsDefined,
|
|
||||||
Max,
|
|
||||||
} from 'class-validator';
|
|
||||||
import { toNumber, isNil } from 'lodash';
|
|
||||||
|
|
||||||
import { DtoValidation } from '@/modules/core/decorators/dto-validation.decorator';
|
|
||||||
import { IsDataExist } from '@/modules/database/constraints/data.exist.constraint';
|
|
||||||
import { PaginateOptions } from '@/modules/database/types';
|
|
||||||
|
|
||||||
import { PostOrderType, SelectTrashMode } from '../constants';
|
|
||||||
import { CategoryEntity, TagEntity } from '../entities';
|
|
||||||
|
|
||||||
@DtoValidation({ type: 'query' })
|
|
||||||
export class QueryPostDto implements PaginateOptions {
|
|
||||||
@Transform(({ value }) => toBoolean(value))
|
|
||||||
@IsBoolean()
|
|
||||||
@IsOptional()
|
|
||||||
isPublished?: boolean;
|
|
||||||
|
|
||||||
@IsEnum(PostOrderType, {
|
|
||||||
message: `排序规则必须是${Object.values(PostOrderType).join(',')}其中一项`,
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
orderBy?: PostOrderType;
|
|
||||||
|
|
||||||
@Transform(({ value }) => {
|
|
||||||
return toNumber(value);
|
|
||||||
})
|
|
||||||
@Min(1, { message: '当前页必须大于1' })
|
|
||||||
@Max(100, { message: '当前页必须小于100' })
|
|
||||||
@IsNumber()
|
|
||||||
@IsOptional()
|
|
||||||
page: number;
|
|
||||||
|
|
||||||
// DTO 设置默认值是无效的,因为它是一个类,而不是一个对象。如果要设置默认值,可以在构造函数中设置默认值。
|
|
||||||
@Transform(({ value }) => toNumber(value))
|
|
||||||
@Min(1, { message: '每页显示数据必须大于1' })
|
|
||||||
@IsNumber()
|
|
||||||
@IsOptional()
|
|
||||||
limit: number;
|
|
||||||
|
|
||||||
@IsDataExist(CategoryEntity, { always: true, message: '分类不存在' })
|
|
||||||
@IsUUID(undefined, { message: 'ID格式错误' })
|
|
||||||
@IsOptional()
|
|
||||||
category?: string;
|
|
||||||
|
|
||||||
@IsDataExist(TagEntity, { always: true, message: '标签不存在' })
|
|
||||||
@IsUUID(undefined, { message: 'ID格式错误' })
|
|
||||||
@IsOptional()
|
|
||||||
tag?: string;
|
|
||||||
|
|
||||||
@MaxLength(100, {
|
|
||||||
always: true,
|
|
||||||
message: '搜索关键字最大长度为$constraint1',
|
|
||||||
})
|
|
||||||
@IsOptional({ always: true })
|
|
||||||
search?: string;
|
|
||||||
|
|
||||||
@IsEnum(SelectTrashMode)
|
|
||||||
@IsOptional()
|
|
||||||
transhed?: SelectTrashMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文章创建验证
|
|
||||||
*/
|
|
||||||
@DtoValidation({ type: 'body', groups: ['create'] })
|
|
||||||
export class CreatePostDto {
|
|
||||||
@IsDataExist(CategoryEntity, { always: true, message: '分类不存在' })
|
|
||||||
@IsUUID(undefined, {
|
|
||||||
always: true,
|
|
||||||
message: 'ID格式错误',
|
|
||||||
})
|
|
||||||
@IsOptional({ always: true })
|
|
||||||
category?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据标签ID查询
|
|
||||||
*/
|
|
||||||
@IsDataExist(TagEntity, { always: true, each: true, message: '标签不存在' })
|
|
||||||
@IsUUID(undefined, {
|
|
||||||
always: true,
|
|
||||||
each: true,
|
|
||||||
message: 'ID格式错误',
|
|
||||||
})
|
|
||||||
@IsOptional({ always: true })
|
|
||||||
tags?: string[];
|
|
||||||
|
|
||||||
@MaxLength(255, {
|
|
||||||
always: true,
|
|
||||||
message: '文章标题长度最大为$constraint1',
|
|
||||||
})
|
|
||||||
@IsNotEmpty({ groups: ['create'], message: '文章标题必须填写' })
|
|
||||||
@IsOptional({ groups: ['update'] })
|
|
||||||
title: string;
|
|
||||||
|
|
||||||
@MaxLength(4, { always: true, groups: ['create'], message: '文章标题长度最大为$constraint1' })
|
|
||||||
@IsNotEmpty({ groups: ['create'], message: '文章内容必须填写' })
|
|
||||||
@IsOptional({ groups: ['update'] })
|
|
||||||
body: string;
|
|
||||||
|
|
||||||
@MaxLength(500, {
|
|
||||||
always: true,
|
|
||||||
message: '文章描述长度最大为$constraint1',
|
|
||||||
})
|
|
||||||
@IsOptional({ always: true })
|
|
||||||
summary?: string;
|
|
||||||
|
|
||||||
@Transform(({ value }) => toBoolean(value))
|
|
||||||
@IsBoolean({ always: true })
|
|
||||||
@ValidateIf((value) => !isNil(value.publish))
|
|
||||||
@IsOptional({ always: true })
|
|
||||||
publish?: boolean;
|
|
||||||
|
|
||||||
@MaxLength(20, {
|
|
||||||
each: true,
|
|
||||||
always: true,
|
|
||||||
message: '每个关键字长度最大为$constraint1',
|
|
||||||
})
|
|
||||||
@IsOptional({ always: true })
|
|
||||||
keywords?: string[];
|
|
||||||
|
|
||||||
@Transform(({ value }) => toNumber(value))
|
|
||||||
@Min(0, { always: true, message: '排序值必须大于0' })
|
|
||||||
@IsNumber(undefined, { always: true })
|
|
||||||
@IsOptional({ always: true })
|
|
||||||
customOrder?: number = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文章更新验证
|
|
||||||
*/
|
|
||||||
@DtoValidation({ groups: ['update'] })
|
|
||||||
export class UpdatePostDto extends PartialType(CreatePostDto) {
|
|
||||||
@IsUUID(undefined, { groups: ['update'], message: '文章ID格式错误' })
|
|
||||||
@IsDefined({ groups: ['update'], message: '文章ID必须指定' })
|
|
||||||
id: string;
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
import { PartialType } from '@nestjs/swagger';
|
|
||||||
import { Transform } from 'class-transformer';
|
|
||||||
import {
|
|
||||||
Min,
|
|
||||||
IsNumber,
|
|
||||||
IsOptional,
|
|
||||||
MaxLength,
|
|
||||||
IsNotEmpty,
|
|
||||||
IsUUID,
|
|
||||||
IsDefined,
|
|
||||||
} from 'class-validator';
|
|
||||||
import { toNumber } from 'lodash';
|
|
||||||
|
|
||||||
import { DtoValidation } from '@/modules/core/decorators/dto-validation.decorator';
|
|
||||||
import { IsUnique } from '@/modules/database/constraints/unique.constraint';
|
|
||||||
import { PaginateOptions } from '@/modules/database/types';
|
|
||||||
|
|
||||||
import { TagEntity } from '../entities';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 标签分页查询验证
|
|
||||||
*/
|
|
||||||
@DtoValidation({ type: 'query' })
|
|
||||||
export class QueryTagDto implements PaginateOptions {
|
|
||||||
@Transform(({ value }) => toNumber(value))
|
|
||||||
@Min(1, { message: '当前页必须大于1' })
|
|
||||||
@IsNumber()
|
|
||||||
@IsOptional()
|
|
||||||
page = 1;
|
|
||||||
|
|
||||||
@Transform(({ value }) => toNumber(value))
|
|
||||||
@Min(1, { message: '每页显示数据必须大于1' })
|
|
||||||
@IsNumber()
|
|
||||||
@IsOptional()
|
|
||||||
limit = 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 标签创建验证
|
|
||||||
*/
|
|
||||||
@DtoValidation({ groups: ['create'] })
|
|
||||||
export class CreateTagDto {
|
|
||||||
@IsUnique(TagEntity, { groups: ['create'], message: '标签名称已存在' })
|
|
||||||
@MaxLength(255, {
|
|
||||||
always: true,
|
|
||||||
message: '标签名称长度最大为$constraint1',
|
|
||||||
})
|
|
||||||
@IsNotEmpty({ groups: ['create'], message: '标签名称必须填写' })
|
|
||||||
@IsOptional({ groups: ['update'] })
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@MaxLength(500, {
|
|
||||||
always: true,
|
|
||||||
message: '标签描述长度最大为$constraint1',
|
|
||||||
})
|
|
||||||
@IsOptional({ always: true })
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 标签更新验证
|
|
||||||
*/
|
|
||||||
@DtoValidation({ groups: ['update'] })
|
|
||||||
export class UpdateTagDto extends PartialType(CreateTagDto) {
|
|
||||||
@IsUUID(undefined, { groups: ['update'], message: 'ID格式错误' })
|
|
||||||
@IsDefined({ groups: ['update'], message: 'ID必须指定' })
|
|
||||||
id: string;
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
## 序列化 响应拦截器
|
|
||||||
@Exclude() @Expose({ groups: ['post-detail'] })
|
|
||||||
@SerializeOptions({ groups: ['post-detail'] })
|
|
||||||
@UseInterceptors(AppInterceptor)
|
|
|
@ -1,54 +0,0 @@
|
||||||
import { Exclude, Expose, Type } from 'class-transformer';
|
|
||||||
import {
|
|
||||||
Entity,
|
|
||||||
BaseEntity,
|
|
||||||
PrimaryColumn,
|
|
||||||
Column,
|
|
||||||
OneToMany,
|
|
||||||
Relation,
|
|
||||||
Tree,
|
|
||||||
TreeParent,
|
|
||||||
TreeChildren,
|
|
||||||
Index,
|
|
||||||
DeleteDateColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
|
|
||||||
import { PostEntity } from './post.entity';
|
|
||||||
|
|
||||||
@Exclude()
|
|
||||||
@Tree('materialized-path')
|
|
||||||
@Entity('content_categories')
|
|
||||||
export class CategoryEntity extends BaseEntity {
|
|
||||||
@Expose()
|
|
||||||
@PrimaryColumn({ type: 'varchar', generated: 'uuid', length: 36 })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@Column({ comment: '分类名称' })
|
|
||||||
@Index({ fulltext: true, unique: true })
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@Expose({ groups: ['category-tree', 'category-list', 'category-detail'] })
|
|
||||||
@Column({ comment: '分类排序', default: 0 })
|
|
||||||
customOrder: number;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@OneToMany(() => PostEntity, (post) => post.category, { cascade: true })
|
|
||||||
posts: Relation<PostEntity[]>;
|
|
||||||
|
|
||||||
@Expose({ groups: ['category-list'] })
|
|
||||||
depth = 0;
|
|
||||||
|
|
||||||
@Expose({ groups: ['category-list', 'category-detail'] })
|
|
||||||
@TreeParent({ onDelete: 'NO ACTION' })
|
|
||||||
parent: Relation<CategoryEntity> | null;
|
|
||||||
|
|
||||||
@Expose({ groups: ['category-tree'] })
|
|
||||||
@TreeChildren({ cascade: true })
|
|
||||||
children: Relation<CategoryEntity>[];
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@Type(() => Date)
|
|
||||||
@DeleteDateColumn({ comment: '删除时间', nullable: true })
|
|
||||||
deletedAt: Date;
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
import { Exclude, Expose } from 'class-transformer';
|
|
||||||
import {
|
|
||||||
Entity,
|
|
||||||
BaseEntity,
|
|
||||||
PrimaryColumn,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
Relation,
|
|
||||||
ManyToOne,
|
|
||||||
Tree,
|
|
||||||
TreeChildren,
|
|
||||||
TreeParent,
|
|
||||||
Index,
|
|
||||||
} from 'typeorm';
|
|
||||||
|
|
||||||
import { UserEntity } from '@/modules/user/entities/user.entity';
|
|
||||||
|
|
||||||
import { PostEntity } from './post.entity';
|
|
||||||
|
|
||||||
@Exclude()
|
|
||||||
@Tree('materialized-path')
|
|
||||||
@Entity('content_comments')
|
|
||||||
export class CommentEntity extends BaseEntity {
|
|
||||||
@Expose({ groups: ['comment-detail', 'comment-list'] })
|
|
||||||
@PrimaryColumn({ type: 'varchar', generated: 'uuid', length: 36 })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Expose({ groups: ['post-detail', 'post-list'] })
|
|
||||||
@Column({ comment: '评论内容', type: 'text' })
|
|
||||||
@Index({ fulltext: true })
|
|
||||||
body: string;
|
|
||||||
|
|
||||||
@Expose({ groups: ['comment-detail', 'comment-list'] })
|
|
||||||
@CreateDateColumn({
|
|
||||||
comment: '创建时间',
|
|
||||||
})
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@ManyToOne(() => PostEntity, (post) => post.comments, {
|
|
||||||
nullable: false,
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
})
|
|
||||||
post: Relation<PostEntity>;
|
|
||||||
|
|
||||||
@Expose({ groups: ['comment-list'] })
|
|
||||||
depth = 0;
|
|
||||||
|
|
||||||
@Expose({ groups: ['comment-detail', 'comment-list'] })
|
|
||||||
@TreeParent({ onDelete: 'CASCADE' })
|
|
||||||
parent: Relation<CommentEntity> | null;
|
|
||||||
|
|
||||||
@Expose({ groups: ['comment-tree'] })
|
|
||||||
@TreeChildren({ cascade: true })
|
|
||||||
children: Relation<CommentEntity>[];
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@ManyToOne((type) => UserEntity, (user) => user.comments, {
|
|
||||||
nullable: false,
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
})
|
|
||||||
author: Relation<UserEntity>;
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export * from './category.entity';
|
|
||||||
export * from './comment.entity';
|
|
||||||
export * from './post.entity';
|
|
||||||
export * from './tag.entity';
|
|
|
@ -1,128 +0,0 @@
|
||||||
import { Exclude, Expose, Type } from 'class-transformer';
|
|
||||||
import {
|
|
||||||
BaseEntity,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
DeleteDateColumn,
|
|
||||||
Entity,
|
|
||||||
Index,
|
|
||||||
JoinTable,
|
|
||||||
ManyToMany,
|
|
||||||
ManyToOne,
|
|
||||||
OneToMany,
|
|
||||||
PrimaryColumn,
|
|
||||||
Relation,
|
|
||||||
UpdateDateColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
|
|
||||||
import { UserEntity } from '@/modules/user/entities/user.entity';
|
|
||||||
|
|
||||||
import { PostBodyType } from '../constants';
|
|
||||||
|
|
||||||
import { CategoryEntity } from './category.entity';
|
|
||||||
import { CommentEntity } from './comment.entity';
|
|
||||||
import { TagEntity } from './tag.entity';
|
|
||||||
|
|
||||||
@Exclude()
|
|
||||||
@Entity('content_posts')
|
|
||||||
export class PostEntity extends BaseEntity {
|
|
||||||
@Expose()
|
|
||||||
@PrimaryColumn({ type: 'varchar', generated: 'uuid', length: 36 })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@Column({ comment: '文章标题' })
|
|
||||||
@Index({ fulltext: true })
|
|
||||||
title: string;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@Column({ comment: '文章内容', type: 'text' })
|
|
||||||
@Index({ fulltext: true })
|
|
||||||
body: string;
|
|
||||||
|
|
||||||
@Expose({ groups: ['post-detail'] })
|
|
||||||
@Column({ comment: '文章描述', nullable: true })
|
|
||||||
@Index({ fulltext: true })
|
|
||||||
summary?: string;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@Column({ comment: '关键字', type: 'simple-array', nullable: true })
|
|
||||||
keywords?: string[];
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@Column({
|
|
||||||
comment: '文章类型',
|
|
||||||
type: 'varchar',
|
|
||||||
// 如果是mysql或者postgresql你可以使用enum类型
|
|
||||||
// enum: PostBodyType,
|
|
||||||
default: PostBodyType.MD,
|
|
||||||
})
|
|
||||||
type: PostBodyType;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@Column({
|
|
||||||
comment: '发布时间',
|
|
||||||
type: 'varchar',
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
publishedAt?: Date | null;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@Column({ comment: '自定义文章排序', default: 0 })
|
|
||||||
customOrder: number;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@Type(() => Date)
|
|
||||||
@CreateDateColumn({
|
|
||||||
comment: '创建时间',
|
|
||||||
})
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@Type(() => Date)
|
|
||||||
@UpdateDateColumn({
|
|
||||||
comment: '更新时间',
|
|
||||||
})
|
|
||||||
updatedAt: Date;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@Type(() => Date)
|
|
||||||
@DeleteDateColumn({
|
|
||||||
comment: '删除时间',
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
deleteAt: Date;
|
|
||||||
|
|
||||||
@Expose({ groups: ['post-detail'] })
|
|
||||||
@ManyToOne(() => CategoryEntity, (category) => category.posts, {
|
|
||||||
nullable: true,
|
|
||||||
onDelete: 'SET NULL',
|
|
||||||
})
|
|
||||||
category: Relation<CategoryEntity>;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@JoinTable()
|
|
||||||
@ManyToMany(() => TagEntity, (tag) => tag.posts, {
|
|
||||||
cascade: true,
|
|
||||||
})
|
|
||||||
tags: Relation<TagEntity[]>;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@OneToMany(() => CommentEntity, (comment) => comment.post)
|
|
||||||
comments: Relation<CommentEntity[]>;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@Column({ comment: '文章评论量', default: 0, select: false })
|
|
||||||
commentCount: number;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
commentCounts: number;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@ManyToOne(() => UserEntity, (user) => user.posts, {
|
|
||||||
nullable: false,
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
})
|
|
||||||
author: Relation<UserEntity>;
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
import { Exclude, Expose } from 'class-transformer';
|
|
||||||
import { Entity, PrimaryColumn, Column, ManyToMany, Index } from 'typeorm';
|
|
||||||
|
|
||||||
import { PostEntity } from './post.entity';
|
|
||||||
|
|
||||||
@Exclude()
|
|
||||||
@Entity('content_tags')
|
|
||||||
export class TagEntity {
|
|
||||||
@Expose()
|
|
||||||
@PrimaryColumn({ type: 'varchar', generated: 'uuid', length: 36 })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Expose({ groups: ['tag-list', 'tag-detail', 'post-list', 'post-detail'] })
|
|
||||||
@Column({ comment: '标签名称' })
|
|
||||||
@Index({ fulltext: true, unique: true })
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@Column({ comment: '标签描述', nullable: true })
|
|
||||||
description?: string;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@ManyToMany(() => PostEntity, (post) => post.tags)
|
|
||||||
posts: PostEntity[];
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
postCount: number;
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
import { instanceToPlain } from 'class-transformer';
|
|
||||||
|
|
||||||
import { isNil, pick } from 'lodash';
|
|
||||||
|
|
||||||
import { PostEntity } from './entities';
|
|
||||||
import { CategoryRepository, CommentRepository } from './repositories';
|
|
||||||
|
|
||||||
export async function getSearchItem(
|
|
||||||
catRepo: CategoryRepository,
|
|
||||||
cmtRepo: CommentRepository,
|
|
||||||
post: PostEntity,
|
|
||||||
) {
|
|
||||||
const categories = isNil(post.category)
|
|
||||||
? []
|
|
||||||
: (await catRepo.flatAncestorsTree(post.category)).map((item) => ({
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
}));
|
|
||||||
const comments = (
|
|
||||||
await cmtRepo.find({
|
|
||||||
relations: ['post'],
|
|
||||||
where: { post: { id: post.id } },
|
|
||||||
})
|
|
||||||
).map((item) => ({ id: item.id, body: item.body }));
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
...pick(instanceToPlain(post), [
|
|
||||||
'id',
|
|
||||||
'title',
|
|
||||||
'body',
|
|
||||||
'summary',
|
|
||||||
'commentCount',
|
|
||||||
'commentCounts',
|
|
||||||
'deletedAt',
|
|
||||||
'publishedAt',
|
|
||||||
'createdAt',
|
|
||||||
'updatedAt',
|
|
||||||
]),
|
|
||||||
categories,
|
|
||||||
// tags: post.tags.map((item) => ({ id: item.id, name: item.name })),
|
|
||||||
comments,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getSearchData = async (
|
|
||||||
posts: PostEntity[],
|
|
||||||
catRepo: CategoryRepository,
|
|
||||||
cmtRepo: CommentRepository,
|
|
||||||
) =>
|
|
||||||
(await Promise.all(posts.map(async (post) => getSearchItem(catRepo, cmtRepo, post)))).reduce(
|
|
||||||
(o, n) => [...o, ...n],
|
|
||||||
[],
|
|
||||||
);
|
|
|
@ -1,202 +0,0 @@
|
||||||
import { isNil, pick, unset } from 'lodash';
|
|
||||||
import { FindOptionsUtils, FindTreeOptions, TreeRepository, TreeRepositoryUtils } from 'typeorm';
|
|
||||||
|
|
||||||
import { CustomRepository } from '@/modules/database/decorators/repository.decorator';
|
|
||||||
|
|
||||||
import { CategoryEntity } from '../entities';
|
|
||||||
|
|
||||||
interface CateFindTreeOptions extends FindTreeOptions {
|
|
||||||
onlyTrashed?: boolean;
|
|
||||||
withTrashed?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
@CustomRepository(CategoryEntity)
|
|
||||||
export class CategoryRepository extends TreeRepository<CategoryEntity> {
|
|
||||||
buildBaseQB() {
|
|
||||||
return this.createQueryBuilder('category').leftJoinAndSelect('category.parent', 'parent');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 树形结构查询
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
async findTrees(options?: CateFindTreeOptions) {
|
|
||||||
const roots = await this.findRoots(options);
|
|
||||||
await Promise.all(roots.map((root) => this.findDescendantsTree(root, options)));
|
|
||||||
return roots;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询顶级分类
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
findRoots(options?: CateFindTreeOptions) {
|
|
||||||
const escapeAlias = (alias: string) => this.manager.connection.driver.escape(alias);
|
|
||||||
const escapeColumn = (column: string) => this.manager.connection.driver.escape(column);
|
|
||||||
|
|
||||||
const joinColumn = this.metadata.treeParentRelation!.joinColumns[0];
|
|
||||||
const parentPropertyName = joinColumn.givenDatabaseName || joinColumn.databaseName;
|
|
||||||
const qb = this.buildBaseQB().orderBy('category.customOrder', 'ASC');
|
|
||||||
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
|
|
||||||
if (options?.withTrashed) {
|
|
||||||
qb.withDeleted();
|
|
||||||
options?.onlyTrashed && qb.where('category.deletedAt IS NOT NULL');
|
|
||||||
}
|
|
||||||
qb.where(`${escapeAlias('category')}.${escapeColumn(parentPropertyName)} IS NULL`);
|
|
||||||
return qb.getMany();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询后代分类
|
|
||||||
* @param entity
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
findDescendants(entity: CategoryEntity, options?: CateFindTreeOptions) {
|
|
||||||
const qb = this.createDescendantsQueryBuilder('category', 'treeClosure', entity);
|
|
||||||
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
|
|
||||||
if (options?.withTrashed) {
|
|
||||||
qb.withDeleted();
|
|
||||||
options?.onlyTrashed && qb.where('category.deletedAt IS NOT NULL');
|
|
||||||
}
|
|
||||||
qb.orderBy('category.customOrder', 'ASC');
|
|
||||||
return qb.getMany();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询祖先分类
|
|
||||||
* @param entity
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
findAncestors(entity: CategoryEntity, options?: CateFindTreeOptions) {
|
|
||||||
const qb = this.createAncestorsQueryBuilder('category', 'treeClosure', entity);
|
|
||||||
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
|
|
||||||
if (options?.withTrashed) {
|
|
||||||
qb.withDeleted();
|
|
||||||
options?.onlyTrashed && qb.where('category.deletedAt IS NOT NULL');
|
|
||||||
}
|
|
||||||
qb.orderBy('category.customOrder', 'ASC');
|
|
||||||
return qb.getMany();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询后代树
|
|
||||||
* @param entity
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
async findDescendantsTree(entity: CategoryEntity, options?: CateFindTreeOptions) {
|
|
||||||
const qb = this.createDescendantsQueryBuilder('category', 'treeClosure', entity)
|
|
||||||
.leftJoinAndSelect('category.parent', 'parent')
|
|
||||||
.orderBy('category.customOrder', 'ASC');
|
|
||||||
if (options?.withTrashed) {
|
|
||||||
qb.withDeleted();
|
|
||||||
options?.onlyTrashed && qb.where('category.deletedAt IS NOT NULL');
|
|
||||||
}
|
|
||||||
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, pick(options, ['relations', 'depth']));
|
|
||||||
const entities = await qb.getRawAndEntities();
|
|
||||||
const relationMaps = TreeRepositoryUtils.createRelationMaps(
|
|
||||||
this.manager,
|
|
||||||
this.metadata,
|
|
||||||
'category',
|
|
||||||
entities.raw,
|
|
||||||
);
|
|
||||||
TreeRepositoryUtils.buildChildrenEntityTree(
|
|
||||||
this.metadata,
|
|
||||||
entity,
|
|
||||||
entities.entities,
|
|
||||||
relationMaps,
|
|
||||||
{
|
|
||||||
depth: -1,
|
|
||||||
...pick(options, ['relations']),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询祖先树
|
|
||||||
* @param entity
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
async findAncestorsTree(entity: CategoryEntity, options?: CateFindTreeOptions) {
|
|
||||||
const qb = this.createAncestorsQueryBuilder('category', 'treeClosure', entity)
|
|
||||||
.leftJoinAndSelect('category.parent', 'parent')
|
|
||||||
.orderBy('category.customOrder', 'ASC');
|
|
||||||
if (options?.withTrashed) {
|
|
||||||
qb.withDeleted();
|
|
||||||
options?.onlyTrashed && qb.where('category.deletedAt IS NOT NULL');
|
|
||||||
}
|
|
||||||
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
|
|
||||||
|
|
||||||
const entities = await qb.getRawAndEntities();
|
|
||||||
const relationMaps = TreeRepositoryUtils.createRelationMaps(
|
|
||||||
this.manager,
|
|
||||||
this.metadata,
|
|
||||||
'category',
|
|
||||||
entities.raw,
|
|
||||||
);
|
|
||||||
TreeRepositoryUtils.buildParentEntityTree(
|
|
||||||
this.metadata,
|
|
||||||
entity,
|
|
||||||
entities.entities,
|
|
||||||
relationMaps,
|
|
||||||
);
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 统计后代元素数量
|
|
||||||
* @param entity
|
|
||||||
*/
|
|
||||||
async countDescendants(entity: CategoryEntity, options?: CateFindTreeOptions) {
|
|
||||||
const qb = this.createDescendantsQueryBuilder('category', 'treeClosure', entity);
|
|
||||||
if (options?.withTrashed) {
|
|
||||||
qb.withDeleted();
|
|
||||||
options?.onlyTrashed && qb.where('category.deletedAt IS NOT NULL');
|
|
||||||
}
|
|
||||||
return qb.getCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 统计祖先元素数量
|
|
||||||
* @param entity
|
|
||||||
*/
|
|
||||||
async countAncestors(entity: CategoryEntity, options?: CateFindTreeOptions) {
|
|
||||||
const qb = this.createAncestorsQueryBuilder('category', 'treeClosure', entity);
|
|
||||||
if (options?.withTrashed) {
|
|
||||||
qb.withDeleted();
|
|
||||||
options?.onlyTrashed && qb.where('category.deletedAt IS NOT NULL');
|
|
||||||
}
|
|
||||||
return qb.getCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 打平并展开树
|
|
||||||
* @param trees
|
|
||||||
* @param depth
|
|
||||||
* @param parent
|
|
||||||
*/
|
|
||||||
async toFlatTrees(trees: CategoryEntity[], depth = 0, parent: CategoryEntity | null = null) {
|
|
||||||
const data: Omit<CategoryEntity, 'children'>[] = [];
|
|
||||||
for (const item of trees) {
|
|
||||||
item.depth = depth;
|
|
||||||
item.parent = parent;
|
|
||||||
const { children } = item;
|
|
||||||
unset(item, 'children');
|
|
||||||
data.push(item);
|
|
||||||
data.push(...(await this.toFlatTrees(children, depth + 1, item)));
|
|
||||||
}
|
|
||||||
return data as CategoryEntity[];
|
|
||||||
}
|
|
||||||
|
|
||||||
async flatAncestorsTree(item: CategoryEntity) {
|
|
||||||
let data: Omit<CategoryEntity, 'children'>[] = [];
|
|
||||||
const category = await this.findAncestorsTree(item);
|
|
||||||
const { parent } = category;
|
|
||||||
unset(category, 'children');
|
|
||||||
unset(category, 'parent');
|
|
||||||
data.push(item);
|
|
||||||
if (!isNil(parent)) data = [...(await this.flatAncestorsTree(parent)), ...data];
|
|
||||||
return data as CategoryEntity[];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,129 +0,0 @@
|
||||||
import { pick, unset } from 'lodash';
|
|
||||||
import {
|
|
||||||
FindOptionsUtils,
|
|
||||||
FindTreeOptions,
|
|
||||||
SelectQueryBuilder,
|
|
||||||
TreeRepository,
|
|
||||||
TreeRepositoryUtils,
|
|
||||||
} from 'typeorm';
|
|
||||||
|
|
||||||
import { CustomRepository } from '@/modules/database/decorators/repository.decorator';
|
|
||||||
|
|
||||||
import { CommentEntity } from '../entities';
|
|
||||||
|
|
||||||
type FindCommentTreeOptions = FindTreeOptions & {
|
|
||||||
addQuery?: (query: SelectQueryBuilder<CommentEntity>) => SelectQueryBuilder<CommentEntity>;
|
|
||||||
};
|
|
||||||
@CustomRepository(CommentEntity)
|
|
||||||
export class CommentRepository extends TreeRepository<CommentEntity> {
|
|
||||||
/**
|
|
||||||
* 构建基础查询器
|
|
||||||
*/
|
|
||||||
buildBaseQB(qb: SelectQueryBuilder<CommentEntity>): SelectQueryBuilder<CommentEntity> {
|
|
||||||
return qb
|
|
||||||
.leftJoinAndSelect(`comment.parent`, 'parent')
|
|
||||||
.leftJoinAndSelect(`comment.post`, 'post')
|
|
||||||
.orderBy('comment.createdAt', 'DESC');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询树
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
async findTrees(options: FindCommentTreeOptions = {}) {
|
|
||||||
options.relations = ['parent', 'children'];
|
|
||||||
const roots = await this.findRoots(options);
|
|
||||||
await Promise.all(roots.map((root) => this.findDescendantsTree(root, options)));
|
|
||||||
return roots;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询顶级评论
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
findRoots(options: FindCommentTreeOptions = {}) {
|
|
||||||
const { addQuery, ...rest } = options;
|
|
||||||
const escapeAlias = (alias: string) => this.manager.connection.driver.escape(alias);
|
|
||||||
const escapeColumn = (column: string) => this.manager.connection.driver.escape(column);
|
|
||||||
|
|
||||||
const joinColumn = this.metadata.treeParentRelation!.joinColumns[0];
|
|
||||||
const parentPropertyName = joinColumn.givenDatabaseName || joinColumn.databaseName;
|
|
||||||
|
|
||||||
let qb = this.buildBaseQB(this.createQueryBuilder('comment'));
|
|
||||||
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, rest);
|
|
||||||
qb.where(`${escapeAlias('comment')}.${escapeColumn(parentPropertyName)} IS NULL`);
|
|
||||||
qb = addQuery ? addQuery(qb) : qb;
|
|
||||||
return qb.getMany();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建后代查询器
|
|
||||||
* @param closureTableAlias
|
|
||||||
* @param entity
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
createDtsQueryBuilder(
|
|
||||||
closureTableAlias: string,
|
|
||||||
entity: CommentEntity,
|
|
||||||
options: FindCommentTreeOptions = {},
|
|
||||||
): SelectQueryBuilder<CommentEntity> {
|
|
||||||
const { addQuery } = options;
|
|
||||||
const qb = this.buildBaseQB(
|
|
||||||
super.createDescendantsQueryBuilder('comment', closureTableAlias, entity),
|
|
||||||
);
|
|
||||||
return addQuery ? addQuery(qb) : qb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询后代树
|
|
||||||
* @param entity
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
async findDescendantsTree(
|
|
||||||
entity: CommentEntity,
|
|
||||||
options: FindCommentTreeOptions = {},
|
|
||||||
): Promise<CommentEntity> {
|
|
||||||
const qb: SelectQueryBuilder<CommentEntity> = this.createDtsQueryBuilder(
|
|
||||||
'treeClosure',
|
|
||||||
entity,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, pick(options, ['relations', 'depth']));
|
|
||||||
const entities = await qb.getRawAndEntities();
|
|
||||||
const relationMaps = TreeRepositoryUtils.createRelationMaps(
|
|
||||||
this.manager,
|
|
||||||
this.metadata,
|
|
||||||
'comment',
|
|
||||||
entities.raw,
|
|
||||||
);
|
|
||||||
TreeRepositoryUtils.buildChildrenEntityTree(
|
|
||||||
this.metadata,
|
|
||||||
entity,
|
|
||||||
entities.entities,
|
|
||||||
relationMaps,
|
|
||||||
{
|
|
||||||
depth: -1,
|
|
||||||
...pick(options, ['relations']),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 打平并展开树
|
|
||||||
* @param trees
|
|
||||||
* @param depth
|
|
||||||
*/
|
|
||||||
async toFlatTrees(trees: CommentEntity[], depth = 0) {
|
|
||||||
const data: Omit<CommentEntity, 'children'>[] = [];
|
|
||||||
for (const item of trees) {
|
|
||||||
item.depth = depth;
|
|
||||||
const { children } = item;
|
|
||||||
unset(item, 'children');
|
|
||||||
data.push(item);
|
|
||||||
data.push(...(await this.toFlatTrees(children, depth + 1)));
|
|
||||||
}
|
|
||||||
return data as CommentEntity[];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export * from './category.repository';
|
|
||||||
export * from './post.repository';
|
|
||||||
export * from './comment.repository';
|
|
||||||
export * from './tag.repository';
|
|
|
@ -1,28 +0,0 @@
|
||||||
import { BaseRepository } from '@/modules/database/base/repository';
|
|
||||||
import { CustomRepository } from '@/modules/database/decorators/repository.decorator';
|
|
||||||
|
|
||||||
import { CommentEntity } from '../entities';
|
|
||||||
import { PostEntity } from '../entities/post.entity';
|
|
||||||
|
|
||||||
@CustomRepository(PostEntity)
|
|
||||||
export class PostRepository extends BaseRepository<PostEntity> {
|
|
||||||
protected _qbName = 'post';
|
|
||||||
|
|
||||||
protected orderBy = 'createdAt';
|
|
||||||
|
|
||||||
// addselect 添加的列必须在entity中有定义 from 可以是实体也可以是表名 'content_comments'
|
|
||||||
// loadRelationCountAndMap 用于加载关系的数量
|
|
||||||
postBuildBaseQB() {
|
|
||||||
return this.createQueryBuilder(this.qbName)
|
|
||||||
.leftJoinAndSelect(`${this.qbName}.category`, 'category')
|
|
||||||
.leftJoinAndSelect(`${this.qbName}.tags`, 'tags')
|
|
||||||
.leftJoinAndSelect(`${this.qbName}.comments`, 'comments')
|
|
||||||
.addSelect((subQuery) => {
|
|
||||||
return subQuery
|
|
||||||
.select('COUNT(c.id)', 'count')
|
|
||||||
.from(CommentEntity, 'c')
|
|
||||||
.where(`c.post.id = ${this.qbName}.id`);
|
|
||||||
}, 'commentCounts')
|
|
||||||
.loadRelationCountAndMap(`${this.qbName}.commentCounts`, `${this.qbName}.comments`);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { BaseRepository } from '@/modules/database/base/repository';
|
|
||||||
import { CustomRepository } from '@/modules/database/decorators/repository.decorator';
|
|
||||||
|
|
||||||
import { TagEntity } from '../entities';
|
|
||||||
|
|
||||||
@CustomRepository(TagEntity)
|
|
||||||
export class TagRepository extends BaseRepository<TagEntity> {
|
|
||||||
protected _qbName = 'tag';
|
|
||||||
|
|
||||||
protected orderBy = 'name';
|
|
||||||
|
|
||||||
tagbuildBaseQB() {
|
|
||||||
return this.buildBaseQB()
|
|
||||||
.leftJoinAndSelect(`${this.qbName}.posts`, 'posts')
|
|
||||||
.addSelect(
|
|
||||||
(subQuery) =>
|
|
||||||
subQuery
|
|
||||||
.select('COUNT(p.id)', 'count')
|
|
||||||
.from('content_posts', 'p')
|
|
||||||
.where(`p.id = ${this.qbName}.id`),
|
|
||||||
'postCount',
|
|
||||||
)
|
|
||||||
.orderBy('postCount', 'DESC')
|
|
||||||
.loadRelationCountAndMap(`${this.qbName}.postCount`, `${this.qbName}.posts`);
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue