feat: add query-login
This commit is contained in:
parent
cfd263a1e7
commit
ca269e5ae2
2
libs/query-config/.npmrc
Normal file
2
libs/query-config/.npmrc
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
||||||
|
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
26
libs/query-config/package.json
Normal file
26
libs/query-config/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
7
libs/query-config/readme.md
Normal file
7
libs/query-config/readme.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# query config for kevisual
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @kevisual/query-config
|
||||||
|
```
|
99
libs/query-config/src/query-config.ts
Normal file
99
libs/query-config/src/query-config.ts
Normal 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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
28
libs/query-config/tsconfig.json
Normal file
28
libs/query-config/tsconfig.json
Normal 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/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
12
libs/query-config/tsup.config.ts
Normal file
12
libs/query-config/tsup.config.ts
Normal 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
2
libs/query-login/.npmrc
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
||||||
|
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
11
libs/query-login/index.html
Normal file
11
libs/query-login/index.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Query Login</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<script src="src/test/login.ts" type="module"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
30
libs/query-login/package.json
Normal file
30
libs/query-login/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
159
libs/query-login/src/login-cache.ts
Normal file
159
libs/query-login/src/login-cache.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
243
libs/query-login/src/query-login.ts
Normal file
243
libs/query-login/src/query-login.ts
Normal 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;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 检查401错误,并刷新token, 如果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;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 检查401错误,并刷新token, 如果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;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
22
libs/query-login/src/test/login.ts
Normal file
22
libs/query-login/src/test/login.ts
Normal 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;
|
28
libs/query-login/tsconfig.json
Normal file
28
libs/query-login/tsconfig.json
Normal 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/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
13
libs/query-login/tsup.config.ts
Normal file
13
libs/query-login/tsup.config.ts
Normal 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',
|
||||||
|
});
|
13
package.json
13
package.json
@ -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",
|
||||||
|
@ -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
2342
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,4 @@
|
|||||||
packages:
|
packages:
|
||||||
- 'packages/*'
|
- 'packages/*'
|
||||||
- '!packages/webshell/webshell-node'
|
- '!packages/webshell/webshell-node'
|
||||||
|
- 'libs/*'
|
@ -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 />} />
|
||||||
|
|
||||||
|
238
src/pages/config/edit/List.tsx
Normal file
238
src/pages/config/edit/List.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
14
src/pages/config/index.tsx
Normal file
14
src/pages/config/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
53
src/pages/config/store/config.ts
Normal file
53
src/pages/config/store/config.ts
Normal 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('删除失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
@ -33,5 +33,10 @@
|
|||||||
"include": [
|
"include": [
|
||||||
"src",
|
"src",
|
||||||
"packages/**/*"
|
"packages/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"libs"
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user