feat: add query-login

This commit is contained in:
xion 2025-03-21 19:46:25 +08:00
parent cfd263a1e7
commit ca269e5ae2
24 changed files with 2548 additions and 822 deletions

2
libs/query-config/.npmrc Normal file
View File

@ -0,0 +1,2 @@
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
//registry.npmjs.org/:_authToken=${NPM_TOKEN}

View File

@ -0,0 +1,26 @@
{
"name": "@kevisual/query-config",
"version": "0.0.1",
"description": "",
"main": "dist/query-config.js",
"types": "dist/query-config.d.ts",
"scripts": {
"build": "tsup"
},
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me>",
"license": "MIT",
"type": "module",
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"@kevisual/query": "^0.0.12"
},
"devDependencies": {
"tsup": "^8.4.0"
},
"exports": {
".": "./dist/query-config.js"
}
}

View File

@ -0,0 +1,7 @@
# query config for kevisual
## 安装
```bash
npm install @kevisual/query-config
```

View File

@ -0,0 +1,99 @@
import { Query } from '@kevisual/query';
import type { Result } from '@kevisual/query/query';
type QueryConfigOpts = {
query?: Query;
};
export type Config<T = any> = {
id?: string;
title?: string;
key?: string;
description?: string;
data?: T;
createdAt?: string;
updatedAt?: string;
};
export type UploadConfig = {
key?: string;
version?: string;
};
export class QueryConfig {
query: Query;
constructor(opts?: QueryConfigOpts) {
this.query = opts?.query || new Query();
}
async post<T = Config>(data: any) {
return this.query.post<T>({ path: 'config', ...data });
}
async getConfig({ id, key }: { id?: string; key?: string }) {
return this.post({
key: 'get',
data: {
id,
key,
},
});
}
async updateConfig(data: Config) {
return this.post({
key: 'update',
data,
});
}
async deleteConfig(id: string) {
return this.post({
key: 'delete',
data: { id },
});
}
async listConfig() {
return this.post<{ list: Config[] }>({
key: 'list',
});
}
/**
*
* @returns
*/
async getUploadConfig() {
return this.post<Result<Config<UploadConfig>>>({
key: 'getUploadConfig',
});
}
/**
*
* @param data
* @returns
*/
async updateUploadConfig(data: Config) {
return this.post<Result<Config<UploadConfig>>>({
key: 'updateUploadConfig',
data,
});
}
}
/**
* , admin
*
*/
export class VipQueryConfig extends QueryConfig {
constructor(opts?: QueryConfigOpts) {
super(opts);
}
/**
* ,
*
*
*
* @returns
*/
async getVipConfig() {
return this.post<Result<Config<UploadConfig>>>({
key: 'shareConfig',
data: {
type: 'vip',
username: 'admin',
},
});
}
}

View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"module": "nodenext",
"target": "esnext",
"noImplicitAny": false,
"outDir": "./dist",
"sourceMap": false,
"allowJs": true,
"newLine": "LF",
"baseUrl": "./",
"typeRoots": [
"node_modules/@types",
],
"declaration": true,
"noEmit": false,
"allowImportingTsExtensions": true,
"emitDeclarationOnly": true,
"moduleResolution": "NodeNext",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"paths": {
"@/*": [
"src/*"
]
}
},
}

View File

@ -0,0 +1,12 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/query-config.ts'],
splitting: false,
sourcemap: false,
clean: true,
format: 'esm',
dts: true,
outDir: 'dist',
tsconfig: 'tsconfig.json',
});

2
libs/query-login/.npmrc Normal file
View File

@ -0,0 +1,2 @@
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
//registry.npmjs.org/:_authToken=${NPM_TOKEN}

View File

@ -0,0 +1,11 @@
<html>
<head>
<title>Query Login</title>
</head>
<body>
<script src="src/test/login.ts" type="module"></script>
</body>
</html>

View File

@ -0,0 +1,30 @@
{
"name": "@kevisual/query-login",
"version": "0.0.1",
"description": "",
"main": "dist/query-login.js",
"types": "dist/query-login.d.ts",
"scripts": {
"build": "tsup"
},
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me>",
"license": "MIT",
"type": "module",
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"@kevisual/query": "^0.0.12"
},
"devDependencies": {
"@types/node": "^22.13.11",
"tsup": "^8.4.0"
},
"exports": {
".": "./dist/query-login.js"
},
"dependencies": {
"@kevisual/cache": "^0.0.1"
}
}

View File

@ -0,0 +1,159 @@
import { MyCache } from '@kevisual/cache';
export type CacheLoginUser = {
user?: any;
id?: string;
accessToken?: string;
refreshToken?: string;
};
type CacheLogin = {
loginUsers: CacheLoginUser[];
} & CacheLoginUser;
export interface CacheStore<T = any> {
name: string;
cacheData: CacheLogin;
cache: T;
/**
* @update
*/
get(key: string): Promise<any>;
/**
* @update
*/
set(key: string, value: CacheLogin): Promise<CacheLogin>;
/**
* @update
*/
del(): Promise<void>;
/**
*
*/
setLoginUser(user: CacheLoginUser): Promise<void>;
/**
*
*/
getCurrentUser(): Promise<CacheLoginUser>;
/**
*
*/
getCurrentUserList(): Promise<CacheLoginUser[]>;
/**
* refreshToken
*/
getRefreshToken(): Promise<string>;
/**
* accessToken
*/
getAccessToken(): Promise<string>;
/**
*
*/
init(): Promise<void>;
/**
*
*/
clearCurrentUser(): Promise<void>;
/**
*
*/
clearAll(): Promise<void>;
}
export class LoginCacheStore implements CacheStore<MyCache<any>> {
cache: MyCache<any>;
name: string;
cacheData: CacheLogin;
constructor(name: string) {
this.cache = new MyCache(name);
this.cacheData = {
loginUsers: [],
user: undefined,
id: undefined,
accessToken: undefined,
refreshToken: undefined,
};
this.name = name;
}
/**
*
* @param key
* @param value
* @returns
*/
async set(key: string, value: CacheLogin) {
await this.cache.set(key, value);
return value;
}
/**
*
*/
async del() {
await this.cache.del();
}
get(key: string): Promise<CacheLogin> {
return this.cache.get(key);
}
async init() {
this.cacheData = (await this.get(this.name)) || {
loginUsers: [],
user: null,
id: null,
accessToken: null,
refreshToken: null,
};
}
/**
*
* @param user
*/
async setLoginUser(user: CacheLoginUser) {
const has = this.cacheData.loginUsers.find((u) => u.id === user.id);
if (has) {
this.cacheData.loginUsers = this.cacheData?.loginUsers?.filter((u) => u?.id && u.id !== user.id);
}
this.cacheData.loginUsers.push(user);
this.cacheData.user = user.user;
this.cacheData.id = user.id;
this.cacheData.accessToken = user.accessToken;
this.cacheData.refreshToken = user.refreshToken;
await this.set(this.name, this.cacheData);
}
getCurrentUser(): Promise<CacheLoginUser> {
const cacheData = this.cacheData;
return Promise.resolve(cacheData.user);
}
getCurrentUserList(): Promise<CacheLoginUser[]> {
return Promise.resolve(this.cacheData.loginUsers.filter((u) => u?.id));
}
getRefreshToken(): Promise<string> {
const cacheData = this.cacheData;
return Promise.resolve(cacheData.refreshToken || '');
}
getAccessToken(): Promise<string> {
const cacheData = this.cacheData;
return Promise.resolve(cacheData.accessToken || '');
}
async clearCurrentUser() {
const user = await this.getCurrentUser();
const has = this.cacheData.loginUsers.find((u) => u.id === user.id);
if (has) {
this.cacheData.loginUsers = this.cacheData?.loginUsers?.filter((u) => u?.id && u.id !== user.id);
}
this.cacheData.user = undefined;
this.cacheData.id = undefined;
this.cacheData.accessToken = undefined;
this.cacheData.refreshToken = undefined;
await this.set(this.name, this.cacheData);
}
async clearAll() {
this.cacheData.loginUsers = [];
this.cacheData.user = undefined;
this.cacheData.id = undefined;
this.cacheData.accessToken = undefined;
this.cacheData.refreshToken = undefined;
await this.set(this.name, this.cacheData);
}
}

View File

@ -0,0 +1,243 @@
import { Query } from '@kevisual/query';
import type { Result, DataOpts } from '@kevisual/query/query';
import { setBaseResponse } from '@kevisual/query/query';
import { LoginCacheStore, CacheStore } from './login-cache.ts';
type QueryLoginOpts = {
query?: Query;
isBrowser?: boolean;
onLoad?: () => void;
storage?: Storage;
};
export type QueryLoginData = {
username?: string;
password: string;
email?: string;
};
export type QueryLoginResult = {
accessToken: string;
refreshToken: string;
};
export class QueryLogin {
query: Query;
cache: CacheStore;
isBrowser: boolean;
load?: boolean;
storage: Storage;
onLoad?: () => void;
constructor(opts?: QueryLoginOpts) {
this.query = opts?.query || new Query();
this.cache = new LoginCacheStore('login');
this.isBrowser = opts?.isBrowser ?? true;
this.init();
this.onLoad = opts?.onLoad;
this.storage = opts?.storage || localStorage;
}
setQuery(query: Query) {
this.query = query;
}
async init() {
await this.cache.init();
this.load = true;
this.onLoad?.();
}
async post<T = any>(data: any, opts?: DataOpts) {
return this.query.post<T>({ path: 'user', ...data }, opts);
}
/**
* ,
* @param data
* @returns
*/
async login(data: QueryLoginData) {
const res = await this.post<QueryLoginResult>({ key: 'login', ...data });
if (res.code === 200) {
const { accessToken, refreshToken } = res?.data || {};
this.storage.setItem('token', accessToken || '');
await this.beforeSetLoginUser({ accessToken, refreshToken });
}
return res;
}
/**
*
* @param param0
*/
async beforeSetLoginUser({ accessToken, refreshToken, check401 }: { accessToken?: string; refreshToken?: string; check401?: boolean }) {
if (accessToken && refreshToken) {
const resUser = await this.getMe(accessToken, check401);
if (resUser.code === 200) {
const user = resUser.data;
if (user) {
this.cache.setLoginUser({
user,
id: user.id,
accessToken,
refreshToken,
});
} else {
console.error('登录失败');
}
}
}
}
async queryRefreshToken(refreshToken?: string) {
const _refreshToken = refreshToken || this.cache.getRefreshToken();
let data = { refreshToken: _refreshToken };
if (!_refreshToken) {
await this.cache.clearCurrentUser();
return {
code: 401,
message: '请先登录',
data: {} as any,
};
}
return this.post(
{ key: 'refreshToken', data },
{
afterResponse: async (response, ctx) => {
setBaseResponse(response);
return response as any;
},
},
);
}
/**
* 401token, refreshToken存在token, 401
* @param res
* @returns
*/
async check401ToRefreshToken(res: Result) {
const refreshToken = await this.cache.getRefreshToken();
if (refreshToken) {
const res = await this.queryRefreshToken(refreshToken);
return res;
}
return res;
}
/**
* 401token, refreshToken存在token, 401
* @param response
* @param ctx
* @param refetch
* @returns
*/
async afterCheck401ToRefreshToken(response: Result, ctx?: { req?: any; res?: any; fetch?: any }, refetch?: boolean) {
const that = this;
if (response?.code === 401) {
const hasRefreshToken = await that.cache.getRefreshToken();
if (hasRefreshToken) {
const res = await that.queryRefreshToken(hasRefreshToken);
if (res.code === 200) {
const { accessToken, refreshToken } = res?.data || {};
that.storage.setItem('token', accessToken || '');
await that.beforeSetLoginUser({ accessToken, refreshToken, check401: false });
if (refetch && ctx && ctx.req && ctx.req.url && ctx.fetch) {
await new Promise((resolve) => setTimeout(resolve, 1500));
const url = ctx.req?.url;
const body = ctx.req?.body;
const headers = ctx.req?.headers;
const res = await ctx.fetch(url, {
method: 'POST',
body: body,
headers: { ...headers, Authorization: `Bearer ${accessToken}` },
});
setBaseResponse(res);
return res;
}
} else {
that.storage.removeItem('token');
await that.cache.clearCurrentUser();
}
return res;
}
}
}
/**
*
* @param token
* @returns
*/
async getMe(token?: string, check401: boolean = true) {
const _token = token || this.storage.getItem('token');
const that = this;
return that.post(
{ key: 'me' },
{
beforeRequest: async (config) => {
if (config.headers) {
config.headers['Authorization'] = `Bearer ${_token}`;
}
return config;
},
afterResponse: async (response, ctx) => {
if (response?.code === 401 && check401) {
return await that.afterCheck401ToRefreshToken(response, ctx);
}
return response as any;
},
},
);
}
async postSwitchUser(username: string) {
return this.post({ key: 'switchCheck', data: { username } });
}
/**
*
* @param username
* @returns
*/
async switchUser(username: string) {
const localUserList = await this.cache.getCurrentUserList();
const user = localUserList.find((userItem) => userItem.user.username === username);
if (user) {
this.storage.setItem('token', user.accessToken || '');
await this.cache.setLoginUser(user);
return {
code: 200,
data: {
accessToken: user.accessToken,
refreshToken: user.refreshToken,
},
success: true,
message: '切换用户成功',
};
}
const res = await this.postSwitchUser(username);
if (res.code === 200) {
this.cache.setLoginUser(res.data);
await this.beforeSetLoginUser({ accessToken: res.data.accessToken, refreshToken: res.data.refreshToken });
}
return res;
}
async logout() {
this.storage.removeItem('token');
this.cache.del();
return this.post<Result>({ key: 'logout' });
}
async hasUser(username: string) {
const that = this;
return this.post<Result>(
{
path: 'org',
key: 'hasUser',
data: {
username,
},
},
{
afterResponse: async (response, ctx) => {
if (response?.code === 401) {
const res = await that.afterCheck401ToRefreshToken(response, ctx, true);
return res;
}
return response as any;
},
},
);
}
}

View File

@ -0,0 +1,22 @@
import { QueryLogin } from '../query-login';
import { Query } from '@kevisual/query';
const query = new Query({
url: 'https://kevisual.silkyai.cn/api/router',
});
query.before(async (options) => {
console.log('before', options);
const token = localStorage.getItem('token');
if (token) {
options.headers = {
...options.headers,
Authorization: `Bearer ${token}`,
};
}
return options;
});
const queryLogin = new QueryLogin({
query,
isBrowser: true,
});
// @ts-ignore
window.queryLogin = queryLogin;

View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"module": "nodenext",
"target": "esnext",
"noImplicitAny": false,
"outDir": "./dist",
"sourceMap": false,
"allowJs": true,
"newLine": "LF",
"baseUrl": "./",
"typeRoots": [
"node_modules/@types",
],
"declaration": true,
"noEmit": false,
"allowImportingTsExtensions": true,
"emitDeclarationOnly": true,
"moduleResolution": "NodeNext",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"paths": {
"@/*": [
"src/*"
]
}
},
}

View File

@ -0,0 +1,13 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/query-login.ts'],
splitting: false,
sourcemap: false,
clean: true,
format: 'esm',
dts: true,
outDir: 'dist',
tsconfig: 'tsconfig.json',
});

View File

@ -13,20 +13,21 @@
"pub": "envision deploy ./dist -k center -v 0.0.9 -u -o root" "pub": "envision deploy ./dist -k center -v 0.0.9 -u -o root"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.6.1", "@ant-design/icons": "^6.0.0",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@icon-park/react": "^1.4.2", "@icon-park/react": "^1.4.2",
"@kevisual/center-components": "workspace:*", "@kevisual/center-components": "workspace:*",
"@kevisual/codemirror": "workspace:*", "@kevisual/codemirror": "workspace:*",
"@kevisual/container": "1.0.0", "@kevisual/container": "1.0.0",
"@kevisual/query": "^0.0.9", "@kevisual/query": "^0.0.12",
"@kevisual/query-config": "workspace:*",
"@kevisual/resources": "workspace:*", "@kevisual/resources": "workspace:*",
"@kevisual/system-ui": "^0.0.3", "@kevisual/system-ui": "^0.0.3",
"@kevisual/ui": "^0.0.2", "@kevisual/ui": "^0.0.2",
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@mui/material": "^6.4.8", "@mui/material": "^6.4.8",
"@tailwindcss/vite": "^4.0.14", "@tailwindcss/vite": "^4.0.15",
"@uiw/react-textarea-code-editor": "^3.1.0", "@uiw/react-textarea-code-editor": "^3.1.0",
"@xyflow/react": "^12.4.4", "@xyflow/react": "^12.4.4",
"antd": "^5.24.4", "antd": "^5.24.4",
@ -39,6 +40,7 @@
"i18next": "^24.2.3", "i18next": "^24.2.3",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^3.0.2",
"immer": "^10.1.1", "immer": "^10.1.1",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"marked": "^15.0.7", "marked": "^15.0.7",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
@ -58,8 +60,9 @@
"@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^22.13.10", "@types/node": "^22.13.11",
"@types/path-browserify": "^1.0.3", "@types/path-browserify": "^1.0.3",
"@types/react": "^19.0.12", "@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
@ -77,7 +80,7 @@
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"react-is": "19.0.0", "react-is": "19.0.0",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.14", "tailwindcss": "^4.0.15",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"typescript-eslint": "^8.27.0", "typescript-eslint": "^8.27.0",

View File

@ -18,7 +18,6 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.18.6", "@codemirror/autocomplete": "^6.18.6",
"@codemirror/basic-setup": "^0.20.0",
"@codemirror/commands": "^6.8.0", "@codemirror/commands": "^6.8.0",
"@codemirror/lang-css": "^6.3.1", "@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9", "@codemirror/lang-html": "^6.4.9",

2342
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,4 @@
packages: packages:
- 'packages/*' - 'packages/*'
- '!packages/webshell/webshell-node' - '!packages/webshell/webshell-node'
- 'libs/*'

View File

@ -5,6 +5,7 @@ import { App as UserApp } from './pages/user';
import { App as UserAppApp } from './pages/app'; import { App as UserAppApp } from './pages/app';
import { App as FileApp } from './pages/file'; import { App as FileApp } from './pages/file';
import { App as OrgApp } from './pages/org'; import { App as OrgApp } from './pages/org';
import { App as ConfigApp } from './pages/config';
import { basename } from './modules/basename'; import { basename } from './modules/basename';
import { Redirect } from './modules/Redirect'; import { Redirect } from './modules/Redirect';
import { CustomThemeProvider } from '@kevisual/center-components/theme/index.tsx'; import { CustomThemeProvider } from '@kevisual/center-components/theme/index.tsx';
@ -74,6 +75,7 @@ export const App = () => {
<Route path='/user/*' element={<UserApp />} /> <Route path='/user/*' element={<UserApp />} />
<Route path='/user1/*' element={<UserApp />} /> <Route path='/user1/*' element={<UserApp />} />
<Route path='/org/*' element={<OrgApp />} /> <Route path='/org/*' element={<OrgApp />} />
<Route path='/config/*' element={<ConfigApp />} />
<Route path='/app/*' element={<UserAppApp />} /> <Route path='/app/*' element={<UserAppApp />} />
<Route path='/file/*' element={<FileApp />} /> <Route path='/file/*' element={<FileApp />} />

View File

@ -0,0 +1,238 @@
import { useEffect, useState } from 'react';
import { useConfigStore } from '../store/config';
import { CardBlank } from '@/components/card';
import { Button, ButtonGroup, Divider, Drawer, Tab, Tabs, Tooltip } from '@mui/material';
import { Edit, Plus, Save, Trash, X } from 'lucide-react';
import { useModal } from '@kevisual/center-components/modal/Confirm.tsx';
import { useForm, Controller } from 'react-hook-form';
import { TextField } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { IconButton } from '@kevisual/center-components/button/index.tsx';
import { useShallow } from 'zustand/shallow';
import { load, dump } from 'js-yaml';
import CodeEditor from '@uiw/react-textarea-code-editor';
import { toast } from 'react-toastify';
import { isEmpty, pick } from 'lodash-es';
type DataYamlEditProps = {
onSave: (data: any) => Promise<void>;
type?: 'yaml' | 'json';
};
export const DataYamlEdit = ({ onSave, type }: DataYamlEditProps) => {
const { formData } = useConfigStore(
useShallow((state) => {
return {
formData: state.formData,
};
}),
);
const [yaml, setYaml] = useState(formData.data);
useEffect(() => {
const _fromData = formData.data || {};
if (isEmpty(_fromData)) {
setYaml('');
return;
} else {
if (type === 'yaml') {
const data = dump(_fromData);
setYaml(data);
} else {
const data = JSON.stringify(_fromData, null, 2);
setYaml(data);
}
}
}, [formData]);
console.log(formData);
const handleSave = () => {
let data: any = {};
try {
if (type === 'yaml') {
data = load(yaml);
} else {
data = JSON.parse(yaml);
}
onSave({ data });
} catch (error: any) {
console.error(error);
const errorMessage = error.message.toString();
toast.error(errorMessage || '解析失败,请检查格式');
}
};
return (
<>
<div className='flex gap-2 items-center'>
<Tooltip title='保存'>
<IconButton sx={{ width: 24, height: 24, padding: '8px' }} onClick={handleSave}>
<Save />
</IconButton>
</Tooltip>
<div className='text-sm'>{type === 'yaml' ? 'Yaml' : 'Json'} </div>
</div>
<CodeEditor value={yaml} onChange={(e) => setYaml(e.target.value)} className='w-full h-full grow' language={type === 'yaml' ? 'yaml' : 'json'} />
</>
);
};
export const DrawerEdit = () => {
const { t } = useTranslation();
const { showEdit, setShowEdit, formData, updateData } = useConfigStore(
useShallow((state) => {
return {
showEdit: state.showEdit,
setShowEdit: state.setShowEdit,
formData: state.formData,
updateData: state.updateData,
};
}),
);
const [tab, setTab] = useState<'base' | 'yaml' | 'json'>('base');
const { control, handleSubmit, reset } = useForm({
defaultValues: {
title: formData.title || '',
description: formData.description || '',
key: formData.key || '',
},
});
useEffect(() => {
if (showEdit) {
const _formData = {
id: formData.id || '',
title: formData.title || '',
description: formData.description || '',
key: formData.key || '',
};
reset(_formData);
}
}, [showEdit, formData]);
const isEdit = !!formData?.id;
const onSave = async (values: any) => {
await updateData({ ...values, id: formData.id }, { refresh: true });
};
const onSubmit = (data) => {
console.log('Form Data:', data);
const pickValue = pick(data, ['title', 'key', 'description']);
onSave(pickValue);
};
return (
<Drawer
open={showEdit}
anchor='right'
slotProps={{
paper: {
sx: {
width: '50%',
height: '100%',
},
},
}}>
<div className='h-full bg-white rounded-lg p-2 w-full overflow-hidden'>
<div className='text-2xl font-bold px-2 py-4 flex gap-2 items-center'>
<IconButton onClick={() => setShowEdit(false)} size='small'>
<X />
</IconButton>
<div>{formData.title}</div>
</div>
<Divider />
<Tabs value={tab} onChange={(_, value) => setTab(value)}>
<Tab label='基本信息' value='base' />
<Tab label='Yaml Config' value='yaml' />
<Tab label='JSON Config' value='json' />
</Tabs>
{tab === 'base' && (
<form onSubmit={handleSubmit(onSubmit)} className='w-full p-2'>
<Controller
name='title'
control={control}
render={({ field }) => <TextField {...field} label='Title' variant='outlined' fullWidth margin='normal' />}
/>
<Controller
name='key'
control={control}
render={({ field }) => <TextField {...field} label='Key' variant='outlined' fullWidth margin='normal' />}
/>
<Controller
name='description'
control={control}
render={({ field }) => <TextField {...field} label='Description' variant='outlined' fullWidth margin='normal' multiline rows={4} />}
/>
<Button type='submit' variant='contained' color='primary'>
{t('Submit')}
</Button>
</form>
)}
{tab === 'yaml' && (
<div className='w-full flex flex-col gap-2 px-4 py-2' style={{ height: 'calc(100% - 120px)' }}>
<DataYamlEdit onSave={onSave} type='yaml' />
</div>
)}
{tab === 'json' && (
<div className='w-full flex flex-col gap-2 px-4 py-2' style={{ height: 'calc(100% - 120px)' }}>
<DataYamlEdit onSave={onSave} type='json' />
</div>
)}
</div>
</Drawer>
);
};
export const List = () => {
const { list, getConfig, setShowEdit, setFormData, deleteConfig } = useConfigStore();
const [modal, contextHolder] = useModal();
useEffect(() => {
getConfig();
}, []);
console.log(list);
return (
<div className='w-full h-full flex bg-gray-100'>
<div className='h-full bg-white'>
<div className='p-2'>
<IconButton
onClick={() => {
setShowEdit(true);
setFormData({});
}}>
<Plus />
</IconButton>
</div>
</div>
<div className=' grow p-4'>
<div className='w-full h-full bg-white rounded-lg p-2 scrollbar '>
<div className='flex flex-wrap gap-2'>
{list.map((item) => (
<div className='card w-[300px]' key={item.id}>
<div className='card-title flex font-bold justify-between'>{item.title}</div>
<div className='card-content'>{item.description}</div>
<div className='card-footer flex justify-end'>
<ButtonGroup variant='contained'>
<Button
onClick={() => {
setShowEdit(true);
setFormData(item);
}}>
<Edit />
</Button>
<Button
onClick={() => {
modal.confirm({
title: 'Delete',
content: 'Are you sure delete this data?',
onOk: () => {
deleteConfig(item.id);
},
});
}}>
<Trash />
</Button>
</ButtonGroup>
</div>
</div>
))}
<CardBlank />
</div>
</div>
</div>
<DrawerEdit />
{contextHolder}
</div>
);
};

View File

@ -0,0 +1,14 @@
import { Route, Routes } from 'react-router-dom';
import { LayoutMain } from '@/modules/layout';
import { List } from './edit/List.tsx';
import { Redirect } from '@/modules/Redirect';
export const App = () => {
return (
<Routes>
<Route element={<LayoutMain title='Config' />}>
<Route path='/' element={<Redirect to='/config/edit/list' />}></Route>
<Route path='edit/list' element={<List />} />
</Route>
</Routes>
);
};

View File

@ -0,0 +1,53 @@
import { create } from 'zustand';
import { query } from '@/modules/query';
import { toast } from 'react-toastify';
import { QueryConfig } from '@kevisual/query-config';
export const queryConfig = new QueryConfig({ query });
interface ConfigStore {
list: any[];
getConfig: () => Promise<void>;
updateData: (data: any, opts?: { refresh?: boolean }) => Promise<any>;
showEdit: boolean;
setShowEdit: (showEdit: boolean) => void;
formData: any;
setFormData: (formData: any) => void;
deleteConfig: (id: string) => Promise<void>;
}
export const useConfigStore = create<ConfigStore>((set, get) => ({
list: [],
getConfig: async () => {
const res = await queryConfig.listConfig();
if (res.code === 200) {
set({ list: res.data?.list || [] });
}
},
updateData: async (data: any, opts?: { refresh?: boolean }) => {
const res = await queryConfig.updateConfig(data);
if (res.code === 200) {
get().setFormData(res.data);
if (opts?.refresh ?? true) {
get().getConfig();
}
toast.success('保存成功');
} else {
toast.error('保存失败');
}
return res;
},
showEdit: false,
setShowEdit: (showEdit: boolean) => set({ showEdit }),
formData: {},
setFormData: (formData: any) => set({ formData }),
deleteConfig: async (id: string) => {
const res = await queryConfig.deleteConfig(id);
if (res.code === 200) {
get().getConfig();
toast.success('删除成功');
} else {
toast.error('删除失败');
}
},
}));

View File

@ -33,5 +33,10 @@
"include": [ "include": [
"src", "src",
"packages/**/*" "packages/**/*"
],
"exclude": [
"node_modules",
"dist",
"libs"
] ]
} }

View File

@ -1,22 +1,29 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"lib": ["ES2023"], "lib": [
"ES2023"
],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"isolatedModules": true, "isolatedModules": true,
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true
}, },
"include": ["vite.config.ts"] "include": [
"vite.config.ts"
],
"exclude": [
"node_modules",
"dist",
"libs"
]
} }