diff --git a/.gitignore b/.gitignore index ec318e3f..c0b25c17 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ config.yaml prisma/client/ .env +config.json apps/server/.env # IDE diff --git a/packages/backend/src/common/config/config.service.ts b/packages/backend/src/common/config/config.service.ts index e01bbd2b..1c568ac2 100644 --- a/packages/backend/src/common/config/config.service.ts +++ b/packages/backend/src/common/config/config.service.ts @@ -38,16 +38,45 @@ const CONFIG_SCHEMA: ISettingSchema[] = [ items: [ { key: 'frontend', - type: 'input', + type: 'input' as any, label: '前端端口', }, { key: 'backend', - type: 'input', + type: 'input' as any, label: '后端端口', }, ], }, + { + key: 'jwt', + label: 'JWT', + items: [ + { + key: 'algorithm', + type: 'select' as any, + label: '算法', + selectOptions: ['HS256', 'RS256'], + }, + { + key: 'secret', + type: 'input' as any, + label: '密钥', + }, + ], + }, + { + key: 'email', + label: '邮箱', + items: [ + { + key: 'use', + type: 'select' as any, + label: '启用', + selectOptions: ['disable', 'resend'], + }, + ], + }, ]; @Injectable() @@ -66,11 +95,15 @@ export class ConfigService { : this.defaultConfig; } - getInstallConfigItem() { - if (fs.existsSync(this.configFilePath)) { - throw new BizException(ErrorCodeEnum.ConfigExists); - } - return []; + getConfigSchema() { + // if (fs.existsSync(this.configFilePath)) { + // throw new BizException(ErrorCodeEnum.ConfigExists); + // } + return CONFIG_SCHEMA; + } + + getConfigSchemaValue() { + return {}; } get(key: K): ConfigType[K] { diff --git a/packages/backend/src/modules/dashboard/dashboard.controller.ts b/packages/backend/src/modules/dashboard/dashboard.controller.ts index d9b2dd7d..26a163ef 100644 --- a/packages/backend/src/modules/dashboard/dashboard.controller.ts +++ b/packages/backend/src/modules/dashboard/dashboard.controller.ts @@ -93,7 +93,9 @@ export class DashboardController { @Public() @Get('install') - install() {} + install() { + return this.configService.getConfigSchema(); + } @Get('config') getAllConfig() { diff --git a/packages/frontend/src/app/dashboard/setting/page.tsx b/packages/frontend/src/app/dashboard/setting/page.tsx index c6ffd0ba..99f7468e 100644 --- a/packages/frontend/src/app/dashboard/setting/page.tsx +++ b/packages/frontend/src/app/dashboard/setting/page.tsx @@ -1,55 +1,27 @@ -import { OptionListItem, OptionListRoot } from '@/components/radix-ui-lib'; +'use client'; + +import useSWR from 'swr'; + +import { Loading } from '@/components/loading'; +import { OptionListRoot, OptionNode } from '@/components/radix-ui-lib'; +import { useStore } from '@/store'; export default function AdminSettingPage() { + const { fetcher } = useStore(); + const { data } = useSWR('/dashboard/install', (url) => + fetcher(url) + .then((res) => res.json()) + .then((res) => { + console.log(res); + return res; + }), + ); + if (!data) return ; return ( - <> - - - + + {data.map((item: any) => ( + + ))} + ); } diff --git a/packages/frontend/src/components/radix-ui-lib.tsx b/packages/frontend/src/components/radix-ui-lib.tsx index 6bdbecfc..5f366a02 100644 --- a/packages/frontend/src/components/radix-ui-lib.tsx +++ b/packages/frontend/src/components/radix-ui-lib.tsx @@ -1,14 +1,21 @@ 'use client'; -import { useState } from 'react'; import clsx from 'clsx'; +import { ChangeEvent, useState } from 'react'; + import { MinusIcon, PlusIcon } from '@radix-ui/react-icons'; import { Button, Select, Switch, Table, TextField } from '@radix-ui/themes'; import useInstallStore from '@/store/install'; import styles from '@/styles/module/radix-ui-lib.module.scss'; -import { ISettingSchema } from 'shared'; +import { + ISettingResult, + ISettingSchema, + MultiInputSettingSchema, + SelectSettingSchema, + TypeSettingSchema, +} from 'shared'; export function OptionListRoot(props: { children: JSX.Element | JSX.Element[]; @@ -16,176 +23,276 @@ export function OptionListRoot(props: { return
{props.children}
; } -export function OptionListItem(props: { +/* 开关选项 */ +function SwitchItem(props: { schema: ISettingSchema; - masterKeyTree?: string[]; + value: boolean; + onCheckedChange: (boolean: boolean) => void; }) { - const installStore = useInstallStore(); - const [tableItems, setTableItems] = useState([]); + return ( + + ); +} - function getKeyTree() { - return [...(props.masterKeyTree ?? props.schema.key)]; - } +/* 输入选项 */ +function InputItem(props: { + schema: TypeSettingSchema; + onChange: (e: ChangeEvent) => void; +}) { + return ( + { + console.log(e.target.value); + props.onChange(e); + }} + /> + ); +} - function addSingleItem(keys?: string[]) { - if (!keys) { - const item = ( +/* 选择 */ +function SelectItem(props: { + schema: SelectSettingSchema; + onValueChange: (value: string) => void; +}) { + return ( + + + + {props.schema.selectOptions.map((option) => ( + + {option} + + ))} + + + ); +} + +/* List组件 */ +function ListInputHeaderArea(props: { + schema: TypeSettingSchema; + addValue: () => void; +}) { + return ( + + ); +} + +function ListInputChildrenArea(props: { schema: TypeSettingSchema }) { + return ( + + - - { - installStore.updateItemRamda( - [...getKeyTree(), parseInt(event.currentTarget.id)], - event.target.value, - ); - }} - > - + {/*{props.schema.keys.map((key, index) => (*/} + {/* */} + {/* {*/} + {/* // installStore.updateItemRamda(*/} + {/* // [...getKeyTree(), parseInt(event.currentTarget.id), key],*/} + {/* // event.target.value,*/} + {/* // );*/} + {/* // }}*/} + {/* />*/} + {/* */} + {/*))}*/} - ); - setTableItems([...tableItems, item]); - return; - } - - const item = ( - - {keys.map((key: string) => { - return ( - - { - installStore.updateItemRamda( - [...getKeyTree(), parseInt(event.currentTarget.id), key], - event.target.value, - ); + + + ); +} + +function MultiInputHeaderArea(props: { + schema: MultiInputSettingSchema; + addKeyValue: () => void; +}) { + return ( + + ); +} + +function MultiInputChildrenArea(props: { + values: { [key: string]: string }[]; + schema: MultiInputSettingSchema; + onKeyValueChange: (index: number, key: string, value: string) => void; + removeKeyValue: (index: number) => void; +}) { + return ( + + + + {props.schema.keys.map((key, index) => ( + + {key} + + ))} + Actions + + + + + {props.values?.map((row, indexRow) => ( + + {Object.entries(row).map(([k, v], indexCol) => ( + + + props.onKeyValueChange(indexRow, k, event.target.value) + } + /> + + ))} + + - ); - })} - - - - - ); - setTableItems([...tableItems, item]); + + ))} + + + ); +} + +/** + * 树形结构的配置选项 + * @param schema 节点结构 + * @param masterKeyTree 父树路径 + */ +export function OptionNode({ + schema, + masterKeyTree = [], +}: { + schema: ISettingSchema; + masterKeyTree?: string[]; +}) { + const { + settings, + getSettingItem, + updateSettingItem, + addEmptyValue, + addEmptyKeyValue, + updateKeyValue, + removeKeyValue, + } = useInstallStore(); + + /* 获取当前节点的完整路径 */ + function getKeyTree() { + return [...masterKeyTree, schema.key]; } + let headerArea, childrenArea; + switch (schema.type) { + case 'switch': + headerArea = ( + updateSettingItem(getKeyTree(), e)} + /> + ); + break; + case 'input': + headerArea = ( + updateSettingItem(getKeyTree(), e.target.value)} + /> + ); + break; + case 'select': + headerArea = ( + updateSettingItem(getKeyTree(), value)} + /> + ); + break; + case 'list': + headerArea = ( + addEmptyValue(masterKeyTree)} + /> + ); + childrenArea = ; + break; + case 'multi-input': + headerArea = ( + addEmptyKeyValue(getKeyTree(), schema.keys)} + /> + ); + childrenArea = ( + + updateKeyValue(getKeyTree(), index, key, value) + } + removeKeyValue={(index) => removeKeyValue(getKeyTree(), index)} + /> + ); + break; + case undefined: // items + if (schema.items) { + childrenArea = schema.items.map((item) => ( + + )); + } + default: + break; + } + console.log('[Setting]', settings); return ( -
+
+ {/* Main Area, Header and Input Area */}
-
{props.schema.label}
-
{props.schema.description}
-
-
- {props.schema.type === 'switch' ? ( - { - installStore.updateItemRamda(getKeyTree(), boolean); - }} - > - ) : props.schema.type === 'input' ? ( - { - installStore.updateItemRamda(getKeyTree(), event.target.value); - }} - > - ) : props.schema.type === 'select' ? ( - { - installStore.updateItemRamda(getKeyTree(), value); - }} - > - - - {props.schema.selectOptions.map((option) => ( - - {option} - - ))} - - - ) : props.schema.type === 'multi-input' || - props.schema.type === 'list' ? ( - - ) : ( - '' - )} +
{schema.label}
+
{schema.description}
+
{!!headerArea && headerArea}
+ {/* Children item including group, list, multi-input */}
- {props.schema.items ? ( - props.schema.items.map((item) => ( - - )) - ) : props.schema.type === 'multi-input' || - props.schema.type === 'list' ? ( - - {props.schema.type === 'multi-input' && tableItems.length !== 0 ? ( - - - {props.schema.keys.map((key) => ( - - {key} - - ))} - Actions - - - ) : ( - '' - )} - {tableItems} - - ) : ( - '' - )} + {!!childrenArea && childrenArea}
); diff --git a/packages/frontend/src/store/install.ts b/packages/frontend/src/store/install.ts index 82b23681..41a6bf92 100644 --- a/packages/frontend/src/store/install.ts +++ b/packages/frontend/src/store/install.ts @@ -1,32 +1,66 @@ -import { create } from 'zustand' -import * as ramda from 'ramda' - +import * as ramda from 'ramda'; +import { create } from 'zustand'; + +import { ISettingResult, ISettingResultValue } from 'shared'; + +type pathLens = (string | number)[]; + interface InstallStoreState { - items: { [key: string]: string | boolean | number | object | object[] | string[] | null }; - addItem: (key: string, item: string | boolean | number | object | object[] | string[] | number[] | null) => void; - updateItem: (key: string, updatedItem: Partial) => void; - getItem: (key: string) => string | boolean | number | object | object[] | string[] | number[] | null; - updateItemRamda: (path: (string | number)[], key: string | boolean | number) => void; - getAll: () => { [key: string]: string | boolean | number | object | object[] | string[] | number[] | null}; + settings: ISettingResult; + _getFullPath: (key: pathLens) => pathLens; + getSettingItem: (path: pathLens) => ISettingResultValue | undefined; + updateSettingItem: (path: pathLens, value: ISettingResultValue) => void; + addEmptyValue: (path: pathLens) => void; + addEmptyKeyValue: (path: pathLens, keys: string[]) => void; + updateKeyValue: ( + path: pathLens, + index: number, + key: string, + value: ISettingResultValue, + ) => void; + removeKeyValue: (path: pathLens, index: number) => void; } - - + const useInstallStore = create((set, get) => ({ - items: {}, - addItem: (key, item) => - set((state) => ({ items: { ...state.items, [key]: item } })), - updateItem: (key, updatedItem) => - set((state) => ({ - items: { - ...state.items, - [key]: updatedItem, - }, - })), - getItem: (key) => get().items[key] || null, - getAll: () => get().items, - updateItemRamda: (path, key) => - set(ramda.over(ramda.lensPath(path), () => key)), + settings: {}, + _getFullPath: (key) => ['settings', ...key], + getSettingItem: (path) => ramda.path(path, get().settings), + updateSettingItem: (path, value) => + set(ramda.over(ramda.lensPath(get()._getFullPath(path)), () => value)), + addEmptyValue: (path) => { + set( + ramda.over(ramda.lensPath(get()._getFullPath(path)), (old) => [ + ...old, + '', + ]), + ); + }, + addEmptyKeyValue: (path, keys) => { + set( + ramda.over(ramda.lensPath(get()._getFullPath(path)), (old) => { + const newPart = keys.reduce((obj, key) => ({ ...obj, [key]: '' }), {}); + return !!old ? [...old, newPart] : [newPart]; + }), + ); + }, + updateKeyValue: (path, index, key, value) => { + set( + ramda.over( + ramda.lensPath([...get()._getFullPath(path), index]), + (old) => ({ + ...old, + [key]: value, + }), + ), + ); + }, + removeKeyValue: (path, index) => { + set( + ramda.over(ramda.lensPath(get()._getFullPath(path)), (oldArray) => + ramda.remove(index, 1, oldArray), + ), + ); + }, })); - + export default useInstallStore; - \ No newline at end of file diff --git a/packages/shared/src/install.ts b/packages/shared/src/install.ts index 8103bdfb..c9316592 100644 --- a/packages/shared/src/install.ts +++ b/packages/shared/src/install.ts @@ -1,31 +1,31 @@ -interface BaseSettingOptions { +export interface BaseSettingOptions { label: string; description?: string; isOptional?: boolean; value?: string | boolean | number; } -interface TypeSettingSchema extends BaseSettingOptions { +export interface TypeSettingSchema extends BaseSettingOptions { key: string; type: 'switch' | 'input' | 'list'; items?: never; } -interface MultiInputSettingSchema extends BaseSettingOptions { +export interface MultiInputSettingSchema extends BaseSettingOptions { key: string; keys: string[]; type: 'multi-input'; items?: never; } -interface SelectSettingSchema extends BaseSettingOptions { +export interface SelectSettingSchema extends BaseSettingOptions { key: string; type: 'select'; items?: never; selectOptions: string[]; } -interface ItemsSettingSchema extends BaseSettingOptions { +export interface ItemsSettingSchema extends BaseSettingOptions { key: string; type?: never; items: ISettingSchema[]; @@ -38,7 +38,16 @@ export type ISettingSchema = | SelectSettingSchema | MultiInputSettingSchema; +export type ISettingResultValue = + | string + | boolean + | number + | string[] + | number[] + | Record[] + | ISettingResult; + /* 回传表单的数据 */ export interface ISettingResult { - [key: string]: string | boolean | number | ISettingResult; + [key: string]: ISettingResultValue; }