Compare commits

...

10 Commits

Author SHA1 Message Date
well e1dbfb010d 2.15 2025-02-14 14:17:25 +08:00
well 1a3da2efff 2.9 2025-02-09 10:39:10 +08:00
well dc1711f949 130.4 2025-01-30 16:42:30 +08:00
well 131db241a3 121 2025-01-24 14:26:30 +08:00
well ba0825a019 117 2025-01-16 19:34:36 +08:00
well d2489121d9 115 2025-01-15 10:39:14 +08:00
well 425f0d7635 1202 2024-12-02 13:41:28 +08:00
well 8d1fee793d 1104u 2024-11-04 16:15:49 +08:00
well 774b52fca9 upnav 2024-11-02 10:36:32 +08:00
well 952ff8f2ab updateturbo 2024-11-01 14:03:16 +08:00
673 changed files with 13610 additions and 42246 deletions

15
.vscode/launch.json vendored 100644
View File

@ -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"
}
]
}

41
.vscode/settings.json vendored
View File

@ -1,18 +1,13 @@
{ {
"editor.formatOnSave": false, "editor.formatOnSave": false,
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma",
"editor.formatOnSave": true
},
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit", "source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit" "source.fixAll.stylelint": "explicit"
}, },
"eslint.workingDirectories": [ "[prisma]": {
{ "editor.defaultFormatter": "Prisma.prisma",
"mode": "auto" "editor.formatOnSave": true
} },
],
"css.validate": false, "css.validate": false,
"less.validate": false, "less.validate": false,
"scss.validate": false, "scss.validate": false,
@ -20,15 +15,33 @@
"emmet.includeLanguages": { "emmet.includeLanguages": {
"postcss": "css" "postcss": "css"
}, },
"tailwindCSS.experimental.configFile": {
"apps/web/src/app/tailwind-config.ts": "apps/web/src/app/**",
"apps/web2/src/app/tailwind-config.ts": "apps/web2/src/app/**",
},
"stylelint.enable": true, "stylelint.enable": true,
"stylelint.snippet": ["css", "scss", "less", "postcss"], "stylelint.snippet": ["css", "scss", "less", "postcss"],
"stylelint.validate": ["css", "scss", "less", "postcss"], "stylelint.validate": ["css", "scss", "less", "postcss"],
"tailwindCSS.experimental.configFile": {
"apps/web2/src/app/tailwind-config.ts": "apps/web2/src/app/**",
},
"javascript.preferences.importModuleSpecifier": "project-relative", "javascript.preferences.importModuleSpecifier": "project-relative",
"typescript.suggest.jsdoc.generateReturns": false, "typescript.suggest.jsdoc.generateReturns": false,
"stylelint.packageManager": "pnpm", "stylelint.packageManager": "pnpm",
"npm.packageManager": "pnpm" "npm.packageManager": "pnpm",
"editor.inlineSuggest.enabled": true, // (Copilot)
"editor.suggestSelection": "first", //
"editor.quickSuggestions": {
//
"other": true,
"comments": false,
"strings": true
},
//
"typescript.suggest.enabled": true,
"javascript.suggest.enabled": true,
// Copilot
"github.copilot.enable": {
"*": true,
"plaintext": false,
"markdown": false,
"scminput": false
},
"typescript.tsdk": "node_modules/typescript/lib",
} }

View File

@ -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

View File

@ -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')],
};

26
apps/admin/.gitignore vendored
View File

@ -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?

View File

@ -1,2 +0,0 @@
/** @format */
module.exports = require.resolve('@3rapp/core/prettier')

View File

@ -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

View File

@ -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

View File

@ -1,3 +0,0 @@
module.exports = {
extends: [require.resolve('@3rapp/core/stylelint')],
};

View File

@ -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

View File

@ -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>

View File

@ -1,54 +0,0 @@
{
"name": "@3rapp/admin",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --no-clearScreen",
"build": "tsc && vite build",
"start": "pnpm preview",
"preview": "vite preview",
"lint": "pnpm lint:es && pnpm lint:style",
"lint:es": "eslint . --ext ts,tsx --fix --report-unused-disable-directives --max-warnings 0",
"lint:style": "stylelint \"**/*.css\" --fix --cache --cache-location node_modules/.cache/stylelint/"
},
"dependencies": {
"@3rapp/store": "workspace:*",
"@3rapp/utils": "workspace:*",
"@ant-design/cssinjs": "^1.20.0",
"antd": "^5.18.1",
"antd-style": "^3.6.2",
"axios": "^1.7.0",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.11",
"immer": "^10.1.1",
"lodash": "^4.17.21",
"lunar-typescript": "^1.7.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-use": "^17.5.0",
"utility-types": "^3.11.0",
"zustand": "^4.5.2"
},
"devDependencies": {
"@3rapp/core": "workspace:*",
"@3rapp/utils": "workspace:*",
"@types/lodash": "^4.17.5",
"@types/node": "^20.12.12",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"postcss-import": "^16.1.0",
"postcss-mixins": "^10.0.1",
"postcss-nested": "^6.0.1",
"postcss-nesting": "^12.1.4",
"prettier": "^3.2.5",
"stylelint": "^16.5.0",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"vite": "^5.2.11"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +0,0 @@
module.exports = {
plugins: {
'postcss-import': {},
'postcss-nesting': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {},
'postcss-mixins': {},
},
};

View File

@ -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

View File

@ -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',
);
};

View File

@ -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;
}

View File

@ -1,3 +0,0 @@
import { ConfigEnv, UserConfig } from 'vite';
export type Configure = (params: ConfigEnv, isBuild: boolean) => UserConfig;

View File

@ -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]',
);

View File

@ -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

View File

@ -1,8 +0,0 @@
export enum LangType {
EN_US = 'en_US',
ZH_CN = 'zh_CN',
}
/**
*
*/
export const langs: `${LangType}`[] = ['en_US', 'zh_CN'];

View File

@ -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,
},
};

View File

@ -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;

View File

@ -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',
},
);

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
`,
};
});

View File

@ -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,
};

View File

@ -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' }), []),
};
};

View File

@ -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;

View File

@ -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>;

View File

@ -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>;

View File

@ -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>,
);

View File

@ -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;
}

View File

@ -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';

View File

@ -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);
}
}

View File

@ -1,2 +0,0 @@
@layer components {
}

View File

@ -1,2 +0,0 @@
@layer utilities {
}

View File

@ -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;
}

View File

@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@ -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: [],
};

View File

@ -1,5 +0,0 @@
{
"extends": "./tsconfig.json",
"include": ["./src", "./test", "./typings", "./scripts", "**.cjs", "**.ts"],
"exclude": ["node_modules"]
}

View File

@ -1,12 +0,0 @@
{
"extends": "@3rapp/core/tsconfig/react.json",
"compilerOptions": {
"outDir": "./dist",
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -1,11 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts", "./scripts"]
}

View File

@ -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;
});

View File

@ -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

View File

@ -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

View File

@ -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')],
};

68
apps/api/.gitignore vendored
View File

@ -1,56 +1,28 @@
# compiled output # dev
/dist .yarn/
/node_modules !.yarn/releases
/build .vscode/*
!.vscode/launch.json
!.vscode/*.code-snippets
.idea/workspace.xml
.idea/usage.statistics.xml
.idea/shelf
# Logs # deps
logs node_modules/
# env
.env
.env.production
# logs
logs/
*.log *.log
npm-debug.log* npm-debug.log*
pnpm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
# OS # misc
.DS_Store .DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

View File

@ -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

View File

@ -1 +0,0 @@
module.exports = require.resolve('@3rapp/core/prettier');

View File

@ -0,0 +1,2 @@
import config from '@3rapp/core/prettierrc';
export default config;

View File

@ -1,5 +1,6 @@
public public
node_modules node_modules
ui
pnpm-lock.yaml pnpm-lock.yaml
package-lock.json package-lock.json
docker docker

View File

@ -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>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](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).

View File

@ -1,5 +0,0 @@
{
"app": {
"name": "NomcqhSli"
}
}

View File

@ -0,0 +1,2 @@
import eslint from '@3rapp/core/eslintrc';
export default eslint;

View File

@ -0,0 +1,7 @@
/** @type {import('ts-jest').JestConfigWithTsJest} **/
export default {
testEnvironment: 'node',
transform: {
'^.+.tsx?$': ['ts-jest', {}],
},
};

View File

@ -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"
}
}
]
}
}

View File

@ -1,111 +1,54 @@
{ {
"name": "@3rapp/api", "name": "@repo/api",
"version": "0.0.1", "type": "module",
"description": "", "exports": {
"author": "", "import": {
"private": true, "types": "./dist/es/index.d.ts",
"license": "UNLICENSED", "default": "./dist/es/index.js"
}
},
"main": "./dist/cjs/index.js",
"module": "./dist/es/index.mjs",
"types": "./dist/cjs/index.d.ts",
"files": [
"dist"
],
"scripts": { "scripts": {
"start1:dev": "nest start --watch", "build": "bunchee -w --tsconfig tsconfig.build.json",
"cli": "bun --bun src/console/bin.ts", "dev": "tsx watch src/index.ts",
"dev": "cross-env NODE_ENV=development pnpm cli start -w", "dbm": "prisma migrate dev",
"prod": "cross-env NODE_ENV=production pnpm cli start -w -p", "dbp": "prisma db push",
"prodjs": "cross-env NODE_ENV=production pnpm cli start -w -p --no-ts", "dbg": "prisma generate",
"reload": "cross-env NODE_ENV=production pnpm cli start -r", "test": "jest --runInBand --detectOpenHandles --forceExit --verbose"
"build": "cross-env NODE_ENV=production pnpm cli build",
"start": "cross-env NODE_ENV=development nest start",
"start:dev": "cross-env NODE_ENV=development nest start --watch",
"start:debug": "cross-env NODE_ENV=development nest start --debug --watch",
"start:prod": "cross-env NODE_ENV=production node dist/main"
}, },
"dependencies": { "dependencies": {
"@3rapp/utils": "workspace:*", "@3rapp/utils": "workspace:*",
"@faker-js/faker": "^8.4.1", "@hono/node-server": "^1.13.8",
"@fastify/static": "^7.0.4", "@hono/swagger-ui": "^0.5.0",
"@nestjs/common": "^10.3.10", "@hono/zod-openapi": "^0.18.3",
"@nestjs/core": "^10.3.10", "@prisma/client": " 6.3.0",
"@nestjs/jwt": "^10.2.0", "form-data": "^4.0.1",
"@nestjs/passport": "^10.0.3", "hono": "^4.6.20",
"@nestjs/platform-fastify": "^10.3.10",
"@nestjs/swagger": "^7.4.0",
"@nestjs/typeorm": "^10.0.2",
"bcrypt": "^5.1.1",
"chalk": "4",
"chokidar": "^3.6.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dayjs": "^1.11.11",
"dotenv": "^16.4.5",
"fastify": "^4.28.1",
"find-up": "5",
"fs-extra": "^11.2.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"meilisearch": "^0.41.0", "openai": "^4.83.0",
"mysql2": "^3.11.0", "prisma-paginate": "^5.2.1",
"ora": "5", "uuid": "^11.0.5"
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pm2": "^5.4.2",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"sanitize-html": "^2.13.0",
"typeorm": "^0.3.20",
"utility-types": "^3.11.0",
"uuid": "^10.0.0",
"validator": "^13.12.0",
"yaml": "^2.5.0",
"yargs": "^17.7.2"
}, },
"devDependencies": { "devDependencies": {
"@3rapp/core": "workspace:*", "@3rapp/core": "workspace:*",
"@nestjs/cli": "^10.4.2", "@types/jest": "^29.5.14",
"@nestjs/schematics": "^10.1.3", "@types/jsonwebtoken": "^9.0.7",
"@nestjs/testing": "^10.3.10", "@types/lodash": "^4.17.15",
"@types/bcrypt": "^5.0.2", "@types/node": "^20.11.17",
"@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.6",
"@types/lodash": "^4.17.7",
"@types/node": "^20.14.12",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"@types/sanitize-html": "^2.11.0",
"@types/supertest": "^6.0.2",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@types/validator": "^13.12.0", "bunchee": "^6.3.2",
"@types/yargs": "^17.0.32",
"bun": "^1.1.21",
"bun-types": "^1.1.21",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"nodemon": "^3.1.4", "prisma": " 6.3.0",
"prettier": "^3.3.3", "ts-jest": "^29.2.5",
"source-map-support": "^0.5.21", "tsx": "^4.7.1"
"supertest": "^7.0.0",
"ts-jest": "^29.2.3",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.5.4"
}, },
"jest": { "prisma": {
"moduleFileExtensions": [ "schema": "src/database/schema"
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
import OpenAI from 'openai';
const openai = new OpenAI({
baseURL: 'https://api.deepseek.com/v1',
apiKey: 'sk-626f7d17d5544477a1844eb174e54c4e',
});
async function main() {
const completion = await openai.chat.completions.create({
messages: [
{
role: 'system',
content: '網站設計有哪些可以參考的或者ai設計網站頁面佈局',
},
],
model: 'deepseek-chat',
});
console.log(completion.choices[0].message.content);
}
main();

View File

@ -0,0 +1,6 @@
import type { AppConfig } from '@/libs/types';
export const appConfig: AppConfig = {
baseUrl: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
apiUrl: process.env.NEXT_PUBLIC_APi_URL || `${process.env.NEXT_PUBLIC_APP_URL}/api`,
};

View File

@ -0,0 +1,6 @@
import type { AuthConfig } from '@/server/auth/type';
export const authConfig: AuthConfig = {
jwtSecret: process.env.AUTH_JWT_SECRET || 'hTVLuGqhuKZW9HUnKzs83yvVBitlwc5d0PNfJqDRsRs=',
jwtExpiration: '5d',
};

View File

@ -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) },
}),
};

View File

@ -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'),
};
});

View File

@ -1,5 +0,0 @@
import { ContentConfig } from '@/modules/content/types';
export const content = (): ContentConfig => ({
searchType: 'meilisearch',
});

View File

@ -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: [],
};
};

View File

@ -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';

View File

@ -1,8 +0,0 @@
import { MelliConfig } from '@/modules/meilisearch/types';
export const meili = (): MelliConfig => [
{
name: 'default',
host: 'http://192.168.31.43:7700/',
},
];

View File

@ -1,3 +0,0 @@
import { createUserConfig } from '@/modules/user/config';
export const user = createUserConfig(() => ({}));

View File

@ -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;
// },
// };

View File

@ -0,0 +1,21 @@
-- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'USER');
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"username" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"role" "UserRole" NOT NULL DEFAULT 'USER',
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");

View File

@ -0,0 +1,18 @@
-- CreateTable
CREATE TABLE "posts" (
"id" TEXT NOT NULL,
"thumb" TEXT NOT NULL,
"title" TEXT NOT NULL,
"summary" TEXT,
"body" TEXT NOT NULL,
"slug" TEXT,
"keywords" TEXT,
"description" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "posts_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "posts_slug_key" ON "posts"("slug");

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "posts" ADD COLUMN "authorId" TEXT;
-- AddForeignKey
ALTER TABLE "posts" ADD CONSTRAINT "posts_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE "File" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"fileName" TEXT NOT NULL,
"fileType" TEXT NOT NULL,
"fileSize" INTEGER NOT NULL,
"userId" TEXT NOT NULL,
"filePath" TEXT NOT NULL,
CONSTRAINT "File_pkey" PRIMARY KEY ("id")
);

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@ -0,0 +1,22 @@
model File {
/// 文件的唯一标识符
id String @id @default(uuid())
/// 文件创建的时间戳
createdAt DateTime @default(now())
/// 文件的名称
fileName String
/// 文件的类型例如pdf, docx
fileType String
/// 文件的大小(单位:字节)
fileSize Int
/// 创建文件的用户ID
userId String
/// 文件在存储系统中的路径
filePath String
}

View File

@ -0,0 +1,16 @@
model Post {
id String @id @default(uuid())
thumb String
title String
summary String?
body String
slug String? @unique
keywords String?
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
author User? @relation(fields: [authorId], references: [id])
authorId String?
@@map("posts")
}

View File

@ -0,0 +1,17 @@
enum UserRole {
ADMIN
USER
}
model User {
id String @id @default(uuid())
username String @unique
email String @unique
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
role UserRole @default(USER)
posts Post[]
@@map("users")
}

View File

@ -0,0 +1,38 @@
import db from '@/libs/db/prismaClient';
const item = async () => {
await db.post.create({
data: {
authorId: '2431e468-dcd0-424a-9270-d29d8df70318',
title: '/posts/create',
summary: '/posts/create',
keywords: '/posts/create',
description: '/posts/create',
slug: 'posts-create111',
body: '/posts/create/posts/create/posts/create/posts/create/posts/create/posts/create',
thumb: `/uploads/thumb/post-1.png`,
},
});
};
console.log(item());
// export const createPostData = async () => {
// // 为避免重复添加数据,在重新运行数据填充时,清空已有文章数据
// await db.post.$truncate();
// for (let index = 0; index < 90; index++) {
// await db.post.create({
// select: { id: true },
// data: {
// user: null,
// // 随机封面图
// thumb: `/uploads/thumb/post-${getRandomInt(1, 8)}.png`,
// // 生成1到3个段落的标题
// title: faker.lorem.paragraph({ min: 1, max: 2 }),
// // 生成3-6个段落的内容并把每个段落用换行符换行
// body: faker.lorem.paragraphs(getRandomInt(4, 9), '/n'),
// // 有49%的机率会生成一段摘要
// summary: faker.lorem.paragraph({ min: 1, max: 2 }),
// },
// });
// }
// };

View File

@ -0,0 +1,2 @@
export * from './libs/api';
export * from './server/main';

View File

@ -0,0 +1,8 @@
import type { AppType } from '@/server/main';
import type { ClientRequestOptions } from 'hono/client';
import { appConfig } from '@/config/app';
import { hc } from 'hono/client';
export const createClient = (options: ClientRequestOptions) =>
hc<AppType>(appConfig.baseUrl, options);

View File

@ -1,15 +1,15 @@
/* eslint-disable no-var */
/* eslint-disable vars-on-top */ /* eslint-disable vars-on-top */
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
declare global {
// eslint-disable-next-line no-var
var prismaGlobal: undefined | ReturnType<typeof prismaClientSingleton>;
}
const prismaClientSingleton = () => { const prismaClientSingleton = () => {
return new PrismaClient(); return new PrismaClient();
}; };
declare global {
var prismaGlobal: undefined | ReturnType<typeof prismaClientSingleton>;
}
const db = globalThis.prismaGlobal ?? prismaClientSingleton(); const db = globalThis.prismaGlobal ?? prismaClientSingleton();
export default db; export default db;

View File

@ -0,0 +1,232 @@
// Helpers.
const s = 1000;
const m = s * 60;
const h = m * 60;
const d = h * 24;
const w = d * 7;
const y = d * 365.25;
type Unit =
| 'Years'
| 'Year'
| 'Yrs'
| 'Yr'
| 'Y'
| 'Weeks'
| 'Week'
| 'W'
| 'Days'
| 'Day'
| 'D'
| 'Hours'
| 'Hour'
| 'Hrs'
| 'Hr'
| 'H'
| 'Minutes'
| 'Minute'
| 'Mins'
| 'Min'
| 'M'
| 'Seconds'
| 'Second'
| 'Secs'
| 'Sec'
| 's'
| 'Milliseconds'
| 'Millisecond'
| 'Msecs'
| 'Msec'
| 'Ms';
type UnitAnyCase = Unit | Uppercase<Unit> | Lowercase<Unit>;
export type StringValue = `${number}` | `${number}${UnitAnyCase}` | `${number} ${UnitAnyCase}`;
interface Options {
/**
* Set to `true` to use verbose formatting. Defaults to `false`.
*/
long?: boolean;
}
/**
* Parse or format the given value.
*
* @param value - The string or number to convert
* @param options - Options for the conversion
* @throws Error if `value` is not a non-empty string or a number
*/
function msFn(value: StringValue, options?: Options): number;
function msFn(value: number, options?: Options): string;
function msFn(value: StringValue | number, options?: Options): number | string {
try {
if (typeof value === 'string') {
return parse(value);
}
if (typeof value === 'number') {
return format(value, options);
}
throw new Error('Value provided to ms() must be a string or number.');
} catch (error) {
const message = isError(error)
? `${error.message}. value=${JSON.stringify(value)}`
: 'An unknown error has occurred.';
throw new Error(message);
}
}
/**
* Parse the given string and return milliseconds.
*
* @param str - A string to parse to milliseconds
* @returns The parsed value in milliseconds, or `NaN` if the string can't be
* parsed
*/
export function parse(str: string): number {
if (typeof str !== 'string' || str.length === 0 || str.length > 100) {
throw new Error(
'Value provided to ms.parse() must be a string with length between 1 and 99.',
);
}
const match =
/^(?<value>-?(?:\d+(?:\.\d+)?|\.\d+)) *(?<type>milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(
str,
);
// Named capture groups need to be manually typed today.
// https://github.com/microsoft/TypeScript/issues/32098
const groups = match?.groups as { value: string; type?: string } | undefined;
if (!groups) {
return Number.NaN;
}
const n = Number.parseFloat(groups.value);
const type = (groups.type || 'ms').toLowerCase() as Lowercase<Unit>;
switch (type) {
case 'years':
case 'year':
case 'yrs':
case 'yr':
case 'y':
return n * y;
case 'weeks':
case 'week':
case 'w':
return n * w;
case 'days':
case 'day':
case 'd':
return n * d;
case 'hours':
case 'hour':
case 'hrs':
case 'hr':
case 'h':
return n * h;
case 'minutes':
case 'minute':
case 'mins':
case 'min':
case 'm':
return n * m;
case 'seconds':
case 'second':
case 'secs':
case 'sec':
case 's':
return n * s;
case 'milliseconds':
case 'millisecond':
case 'msecs':
case 'msec':
case 'ms':
return n;
default:
// This should never occur.
throw new Error(`The unit ${type as string} was matched, but no matching case exists.`);
}
}
/**
* Parse the given StringValue and return milliseconds.
*
* @param value - A typesafe StringValue to parse to milliseconds
* @returns The parsed value in milliseconds, or `NaN` if the string can't be
* parsed
*/
export function parseStrict(value: StringValue): number {
return parse(value);
}
export default msFn;
/**
* Short format for `ms`.
*/
function fmtShort(ms: number): StringValue {
const msAbs = Math.abs(ms);
if (msAbs >= d) {
return `${Math.round(ms / d)}d`;
}
if (msAbs >= h) {
return `${Math.round(ms / h)}h`;
}
if (msAbs >= m) {
return `${Math.round(ms / m)}m`;
}
if (msAbs >= s) {
return `${Math.round(ms / s)}s`;
}
return `${ms}ms`;
}
/**
* Long format for `ms`.
*/
function fmtLong(ms: number): StringValue {
const msAbs = Math.abs(ms);
if (msAbs >= d) {
return plural(ms, msAbs, d, 'day');
}
if (msAbs >= h) {
return plural(ms, msAbs, h, 'hour');
}
if (msAbs >= m) {
return plural(ms, msAbs, m, 'minute');
}
if (msAbs >= s) {
return plural(ms, msAbs, s, 'second');
}
return `${ms} ms`;
}
/**
* Format the given integer as a string.
*
* @param ms - milliseconds
* @param options - Options for the conversion
* @returns The formatted string
*/
export function format(ms: number, options?: Options): string {
if (typeof ms !== 'number' || !Number.isFinite(ms)) {
throw new TypeError('Value provided to ms.format() must be of type number.');
}
return options?.long ? fmtLong(ms) : fmtShort(ms);
}
/**
* Pluralization helper.
*/
function plural(ms: number, msAbs: number, n: number, name: string): StringValue {
const isPlural = msAbs >= n * 1.5;
return `${Math.round(ms / n)} ${name}${isPlural ? 's' : ''}` as StringValue;
}
/**
* A type guard for errors.
*
* @param value - The value to test
* @returns A boolean `true` if the provided value is an Error-like object
*/
function isError(value: unknown): value is Error {
return typeof value === 'object' && value !== null && 'message' in value;
}

View File

@ -0,0 +1,50 @@
// eslint-disable-next-line unicorn/prefer-node-protocol
import { randomBytes, scryptSync, timingSafeEqual } from 'crypto';
/**
*
* @param password
* @returns : salt.hash
*/
export const hashPassword = (password: string) => {
try {
const salt = randomBytes(16).toString('hex');
const hash = scryptSync(password, salt, 64, {
N: 16384,
r: 8,
p: 1,
}).toString('hex');
return `${salt}.${hash}`;
} catch (error) {
if (error instanceof Error) {
throw new TypeError(error.message);
} else {
throw new TypeError('Unknown error occurred');
}
}
};
/**
*
* @param password
* @param hash
* @returns true false
*/
export const verifyPassword = (password: string, hash: string): boolean => {
try {
const [salt, storedHash] = hash.split('.');
if (!salt || !storedHash) return false;
const newHashBuffers = scryptSync(password, salt, 64, {
N: 16384,
r: 8,
p: 1,
});
return timingSafeEqual(newHashBuffers, Buffer.from(storedHash, 'hex'));
} catch (error) {
if (error instanceof Error) {
throw new TypeError(error.message);
} else {
throw new TypeError('Unknown error occurred');
}
}
};

View File

@ -0,0 +1,35 @@
import type { AuthItem } from '@/server/auth/type';
import { authConfig } from '@/config/auth';
import jwt from 'jsonwebtoken';
import { parseStrict } from './ms';
export const ACCESS_TOKEN_COOKIE_NAME = 'auth_token';
// 生成 token
export const generateAccessToken = (user: AuthItem) => {
const expiryMs = parseStrict(authConfig.jwtExpiration);
return jwt.sign(
{
id: user.id,
username: user.username,
role: user.role,
exp: Math.floor(Date.now() / 1000) + expiryMs,
},
authConfig.jwtSecret,
);
};
// 验证 token
export const getTokenExpirationTime = (token: string) => {
const decoded = jwt.decode(token);
if (decoded === null || typeof decoded === 'string') throw new Error('令牌解析错误');
const expiresIn = Number(decoded.exp) * 1000 - Date.now();
if (expiresIn < 0) throw new Error('令牌已过期');
return expiresIn;
};
export const getAccessTokenFromHeader = (req: any) => {
const authHeader = req.headers?.authorization;
return authHeader.startsWith('Bearer ') ? authHeader.substring(7) : null;
};

View File

@ -0,0 +1,8 @@
// 定义一个类型 Date2String用于将对象中 Date 类型的属性转换为 string 类型 遍歷參數T的每個屬性如果屬性是 Date 類型,則轉換為 string 類型,否則保持原樣。
export type Date2String<T> = {
[P in keyof T]: T[P] extends Date ? string : T[P];
};
export interface AppConfig {
apiUrl: string;
baseUrl: string;
}

View File

@ -1,4 +0,0 @@
import { createApp, startApp } from './modules/core/helpers/app';
import { createOptions } from './options';
startApp(createApp(createOptions));

View File

@ -0,0 +1,11 @@
import { createMiddleware } from 'hono/factory';
import jwt from 'jsonwebtoken';
export const getUserMiddleware = createMiddleware(async (c, next) => {
const Authorization = c.req.header('authorization');
console.log(Authorization);
const token = Authorization?.split(' ')[1] || '';
const users = jwt.decode(token) as any;
console.log(users);
c.set('user', users);
await next();
});

View File

@ -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,
},
],
};
}
}

View File

@ -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,changeconfig.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);

View File

@ -1,8 +0,0 @@
export enum EnvironmentType {
DEVELOPMENT = 'development',
DEV = 'dev',
PRODUCTION = 'production',
PROD = 'prod',
TEST = 'test',
PREVIEW = 'preview',
}

View File

@ -1,140 +0,0 @@
import { readFileSync } from 'fs';
import { parse } from 'dotenv';
import findUp from 'find-up';
import { isNil, isFunction } from 'lodash';
import { EnvironmentType } from './constants';
export class Env {
/**
*
*/
async load() {
/**
*
* eslint turbo.jsoon"tasks": {"build": {"dependsOn": ["$NODE_ENV"]
*/
if (isNil(process.env.NODE_ENV)) process.env.NODE_ENV = EnvironmentType.DEVELOPMENT;
// 从当前运行应用的目录开始,向上查找.env文件,直到找到第一个文件为止
// 没有找到则返回undefined
const search = [findUp.sync(['.env'])];
// 从当前运行应用的目录开始,向上寻找.env.{环境变量文件},直到找到第一个文件为止,如.env.local
// 如果是development、dev、production、prod环境则同时查找两个
// 没有找到则返回undefined
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
if (this.isDev()) {
search.push(
findUp.sync([`.env.${EnvironmentType.DEVELOPMENT}`, `.env.${EnvironmentType.DEV}`]),
);
} else if (this.isProd()) {
search.push(
findUp.sync([`.env.${EnvironmentType.PRODUCTION}`, `.env.${EnvironmentType.PROD}`]),
);
} else {
search.push(findUp.sync([`.env.${this.run()}`]));
}
// 过滤掉undefined,把找到的环境变量文件放入envFiles数组
const envFiles = search.filter((file) => file !== undefined);
// 转义每个环境变量文件中的内容为一个对象并让前者覆盖合并后者
// 如.env.{环境变量文件}会覆盖合并.env
// 然后,得到所有文件中配置的环境变量对象
const fileEnvs = envFiles
.map((filePath) => parse(readFileSync(filePath)))
.reduce(
(oc, nc) => ({
...oc,
...nc,
}),
{},
);
// 文件环境变量与系统环境变量合并
const envs = { ...process.env, ...fileEnvs };
// 过滤出在envs中存在而在process.env中不存在的键
const keys = Object.keys(envs).filter((key) => !(key in process.env));
// 把.env*中存在而系统环境变量中不存在的键值对追加到process.env中
// 这样就可以得到最终环境变量 process.env
keys.forEach((key) => {
process.env[key] = envs[key];
});
}
/**
* ,production, development
*/
run() {
return process.env.NODE_ENV as EnvironmentType & RecordAny;
}
/**
*
*/
isProd() {
return this.run() === EnvironmentType.PRODUCTION || this.run() === EnvironmentType.PROD;
}
/**
*
*/
isDev() {
return this.run() === EnvironmentType.DEVELOPMENT || this.run() === EnvironmentType.DEV;
}
/**
*
*/
get(): { [key: string]: string };
/**
*
* @param key
*/
get<T extends BaseType = string>(key: string): T;
/**
*
* @param key
* @param parseTo
*/
get<T extends BaseType = string>(key: string, parseTo: ParseType<T>): T;
/**
* ,
* @param key
* @param defaultValue
*/
get<T extends BaseType = string>(key: string, defaultValue: T): T;
/**
* ,
* @param key
* @param parseTo
* @param defaultValue
*/
get<T extends BaseType = string>(key: string, parseTo: ParseType<T>, defaultValue: T): T;
/**
*
* @param key
* @param parseTo
* @param defaultValue
*/
get<T extends BaseType = string>(key?: string, parseTo?: ParseType<T> | T, defaultValue?: T) {
if (!key) return process.env;
const value = process.env[key];
if (value !== undefined) {
if (parseTo && isFunction(parseTo)) {
return parseTo(value);
}
return value as T;
}
if (parseTo === undefined && defaultValue === undefined) {
return undefined;
}
if (parseTo && defaultValue === undefined) {
return isFunction(parseTo) ? undefined : parseTo;
}
return defaultValue! as T;
}
}

Some files were not shown because too many files have changed in this diff Show More