Compare commits
8 Commits
v2.0
...
003c43140b
Author | SHA1 | Date | |
---|---|---|---|
003c43140b | |||
6850347b89 | |||
fb5a59d81b | |||
8147f1652e | |||
d5be9dc08e | |||
2f9576f53d | |||
ba7395ac18 | |||
d49728a796 |
@ -16,7 +16,7 @@
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest"]
|
||||
"args": ["@playwright/mcp@0.0.29"]
|
||||
},
|
||||
"mcp-server-time": {
|
||||
"command": "uvx",
|
||||
@ -26,21 +26,14 @@
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-shrimp-task-manager"],
|
||||
"env": {
|
||||
"DATA_DIR": "D:/workspace/tools/mcp-shrimp-task-manager/data",
|
||||
"DATA_DIR": "D:/workspace/mcp-shrimp-task-manager/data",
|
||||
"TEMPLATES_USE": "en",
|
||||
"ENABLE_GUI": "true"
|
||||
"ENABLE_GUI": "false"
|
||||
}
|
||||
},
|
||||
"mcp-deepwiki": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-deepwiki@latest"]
|
||||
},
|
||||
"memory": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-memory"],
|
||||
"env": {
|
||||
"MEMORY_FILE_PATH": "D:/workspace/tools/server-memory/memory.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
**# RIPER-5 + 多维度思维 + 代理执行协议 (v4.9.1 - MCP工具驱动版)**
|
||||
|
||||
**元指令:** 此协议旨在最大化你的战略规划与执行效率。你的核心任务是**指挥和利用MCP工具集**来驱动项目进展。严格遵守核心原则,利用 `mcp-shrimp-task-manager` 进行项目规划与追踪,使用 `deepwiki-mcp` 进行深度研究。主动管理 `/project_document` 作为知识库。**每轮主要响应后,调用 `mcp.feedback_enhanced` 进行交互或通知。**
|
||||
@ -164,4 +167,5 @@ alwaysApply: false
|
||||
|
||||
* **极致效率:** AI应最大限度地减少手动干预,让MCP工具处理所有可以自动化的工作。
|
||||
* **战略聚焦:** 将AI的“思考”集中在无法被工具替代的领域:战略决策、创新构想、复杂问题诊断 (`mcp.sequential_thinking`) 和最终质量把关。
|
||||
|
||||
* **无缝集成:** 期望AI能流畅地在不同MCP工具之间传递信息,形成一个高度整合的自动化工作流。
|
||||
|
2
.env.dev
@ -15,8 +15,6 @@ VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
||||
|
||||
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
|
||||
VITE_APP_ENCRYPT=Y
|
||||
# AES 加密头标识
|
||||
VITE_HEADER_FLAG=encrypt-key
|
||||
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
|
||||
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
|
||||
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
|
||||
|
@ -12,8 +12,6 @@ VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
||||
|
||||
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
|
||||
VITE_APP_ENCRYPT=Y
|
||||
# AES 加密头标识
|
||||
VITE_HEADER_FLAG=encrypt-key
|
||||
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
|
||||
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
|
||||
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
|
||||
|
@ -12,8 +12,6 @@ VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
||||
|
||||
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
|
||||
VITE_APP_ENCRYPT=Y
|
||||
# AES 加密头标识
|
||||
VITE_HEADER_FLAG=encrypt-key
|
||||
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
|
||||
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
|
||||
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
|
||||
|
3
.vscode/settings.json
vendored
@ -31,8 +31,7 @@
|
||||
"vue.server.hybridMode": true,
|
||||
"files.exclude": { "/docs": true },
|
||||
"search.exclude": {
|
||||
"/docs": true,
|
||||
"**/dist/**": true
|
||||
"/docs": true
|
||||
},
|
||||
"cSpell.words": ["Axios", "tinymce"]
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { ProxyOptions } from 'vite';
|
||||
import type { HttpProxy, ProxyOptions } from 'vite';
|
||||
import { bgRed, bgYellow, green, lightBlue } from 'kolorist';
|
||||
import { consola } from 'consola';
|
||||
import { createServiceConfig } from '../../src/utils/service';
|
||||
@ -34,7 +34,7 @@ function createProxyItem(item: App.Service.ServiceConfigItem, enableLog: boolean
|
||||
target: item.baseURL,
|
||||
changeOrigin: true,
|
||||
ws: item.ws,
|
||||
configure: (_proxy, options) => {
|
||||
configure: (_proxy: HttpProxy.Server, options: ProxyOptions) => {
|
||||
_proxy.on('proxyReq', (_proxyReq, req, _res) => {
|
||||
if (!enableLog) return;
|
||||
|
||||
|
@ -10,7 +10,6 @@ import org.apache.velocity.VelocityContext;
|
||||
import org.dromara.common.core.utils.DateUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.mybatis.enums.DataBaseType;
|
||||
import org.dromara.common.mybatis.helper.DataBaseHelper;
|
||||
import org.dromara.generator.constant.GenConstants;
|
||||
import org.dromara.generator.domain.GenTable;
|
||||
@ -59,7 +58,7 @@ public class VelocityUtils {
|
||||
velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】");
|
||||
velocityContext.put("ClassName", genTable.getClassName());
|
||||
velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName()));
|
||||
velocityContext.put("moduleName", StrUtil.toSymbolCase(genTable.getModuleName(), '-'));
|
||||
velocityContext.put("moduleName", genTable.getModuleName());
|
||||
velocityContext.put("BusinessName", StringUtils.capitalize(genTable.getBusinessName()));
|
||||
velocityContext.put("businessName", genTable.getBusinessName());
|
||||
velocityContext.put("business_name", StrUtil.toUnderlineCase(genTable.getBusinessName()));
|
||||
@ -125,12 +124,11 @@ public class VelocityUtils {
|
||||
templates.add("vm/java/serviceImpl.java.vm");
|
||||
templates.add("vm/java/controller.java.vm");
|
||||
templates.add("vm/xml/mapper.xml.vm");
|
||||
DataBaseType dataBaseType = DataBaseHelper.getDataBaseType();
|
||||
if (dataBaseType.isOracle()) {
|
||||
if (DataBaseHelper.isOracle()) {
|
||||
templates.add("vm/sql/oracle/sql.vm");
|
||||
} else if (dataBaseType.isPostgreSql()) {
|
||||
} else if (DataBaseHelper.isPostgerSql()) {
|
||||
templates.add("vm/sql/postgres/sql.vm");
|
||||
} else if (dataBaseType.isSqlServer()) {
|
||||
} else if (DataBaseHelper.isSqlServer()) {
|
||||
templates.add("vm/sql/sqlserver/sql.vm");
|
||||
} else {
|
||||
templates.add("vm/sql/sql.vm");
|
||||
@ -165,7 +163,7 @@ public class VelocityUtils {
|
||||
String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/");
|
||||
String mybatisPath = MYBATIS_PATH + "/" + moduleName;
|
||||
String soybeanPath = "soy";
|
||||
String soybeanModuleName = StrUtil.toSymbolCase(moduleName, '-');
|
||||
|
||||
if (template.contains("domain.java.vm")) {
|
||||
fileName = StringUtils.format("{}/domain/{}.java", javaPath, className);
|
||||
}
|
||||
@ -188,17 +186,17 @@ public class VelocityUtils {
|
||||
} else if (template.contains("sql.vm")) {
|
||||
fileName = businessName + "Menu.sql";
|
||||
} else if (template.contains("index.vue.vm")) {
|
||||
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
|
||||
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
|
||||
} else if (template.contains("index-tree.vue.vm")) {
|
||||
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
|
||||
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
|
||||
} else if (template.contains("api.d.ts.vm")) {
|
||||
fileName = StringUtils.format("{}/typings/api/{}.{}.api.d.ts", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
|
||||
fileName = StringUtils.format("{}/typings/api/{}.{}.api.d.ts", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
|
||||
} else if (template.contains("api.ts.vm")) {
|
||||
fileName = StringUtils.format("{}/service/api/{}/{}.ts", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
|
||||
fileName = StringUtils.format("{}/service/api/{}/{}.ts", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
|
||||
} else if (template.contains("search.vue.vm")) {
|
||||
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-search.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
|
||||
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-search.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
|
||||
} else if (template.contains("operate-drawer.vue.vm")) {
|
||||
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-operate-drawer.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
|
||||
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-operate-drawer.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
60
package.json
@ -41,18 +41,21 @@
|
||||
"scripts": {
|
||||
"build": "vite build --mode prod",
|
||||
"build:dev": "vite build --mode dev",
|
||||
"build:tauri": "pnpm tauri build",
|
||||
"build:test": "vite build --mode test",
|
||||
"cleanup": "sa cleanup",
|
||||
"commit": "sa git-commit",
|
||||
"commit:zh": "sa git-commit -l=zh-cn",
|
||||
"dev": "vite --mode dev",
|
||||
"dev:prod": "vite --mode prod",
|
||||
"dev:tauri": "pnpm tauri dev",
|
||||
"dev:test": "vite --mode test",
|
||||
"gen-route": "sa gen-route",
|
||||
"lint": "eslint . --fix",
|
||||
"prepare": "simple-git-hooks",
|
||||
"preview": "vite preview",
|
||||
"release": "sa release",
|
||||
"tauri-icon": "pnpm tauri icon ./public/logo.png",
|
||||
"typecheck": "vue-tsc --noEmit --skipLibCheck",
|
||||
"update-pkg": "sa update-pkg"
|
||||
},
|
||||
@ -65,12 +68,13 @@
|
||||
"@sa/materials": "workspace:*",
|
||||
"@sa/tinymce": "workspace:*",
|
||||
"@sa/utils": "workspace:*",
|
||||
"@tauri-apps/api": "2.5.0",
|
||||
"@types/streamsaver": "^2.0.5",
|
||||
"@vueuse/core": "13.8.0",
|
||||
"@vueuse/core": "13.5.0",
|
||||
"clipboard": "2.0.11",
|
||||
"dayjs": "1.11.14",
|
||||
"dayjs": "1.11.13",
|
||||
"defu": "6.1.4",
|
||||
"echarts": "6.0.0",
|
||||
"echarts": "5.6.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"json5": "2.2.3",
|
||||
@ -80,47 +84,47 @@
|
||||
"pinia": "3.0.3",
|
||||
"streamsaver": "^2.0.6",
|
||||
"tailwind-merge": "3.3.1",
|
||||
"vue": "3.5.20",
|
||||
"vue": "3.5.17",
|
||||
"vue-advanced-cropper": "^2.8.9",
|
||||
"vue-draggable-plus": "0.6.0",
|
||||
"vue-i18n": "11.1.11",
|
||||
"vue-router": "4.5.1",
|
||||
"xlsx": "0.18.5"
|
||||
"vue-i18n": "11.1.9",
|
||||
"vue-router": "4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@elegant-router/vue": "0.3.8",
|
||||
"@iconify/json": "2.2.378",
|
||||
"@iconify/json": "2.2.357",
|
||||
"@sa/scripts": "workspace:*",
|
||||
"@sa/uno-preset": "workspace:*",
|
||||
"@soybeanjs/eslint-config": "1.7.1",
|
||||
"@types/node": "24.3.0",
|
||||
"@tauri-apps/cli": "2.5.0",
|
||||
"@types/node": "24.0.13",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@unocss/eslint-config": "66.4.2",
|
||||
"@unocss/preset-icons": "66.4.2",
|
||||
"@unocss/preset-uno": "66.4.2",
|
||||
"@unocss/transformer-directives": "66.4.2",
|
||||
"@unocss/transformer-variant-group": "66.4.2",
|
||||
"@unocss/vite": "66.4.2",
|
||||
"@vitejs/plugin-vue": "6.0.1",
|
||||
"@vitejs/plugin-vue-jsx": "5.1.0",
|
||||
"@unocss/eslint-config": "66.3.3",
|
||||
"@unocss/preset-icons": "66.3.3",
|
||||
"@unocss/preset-uno": "66.3.3",
|
||||
"@unocss/transformer-directives": "66.3.3",
|
||||
"@unocss/transformer-variant-group": "66.3.3",
|
||||
"@unocss/vite": "66.3.3",
|
||||
"@vitejs/plugin-vue": "6.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "5.0.1",
|
||||
"consola": "3.4.2",
|
||||
"eslint": "9.34.0",
|
||||
"eslint-plugin-vue": "10.4.0",
|
||||
"eslint": "9.31.0",
|
||||
"eslint-plugin-vue": "10.3.0",
|
||||
"kolorist": "1.8.0",
|
||||
"sass": "1.91.0",
|
||||
"simple-git-hooks": "2.13.1",
|
||||
"tsx": "4.20.5",
|
||||
"typescript": "5.9.2",
|
||||
"unplugin-icons": "22.2.0",
|
||||
"unplugin-vue-components": "29.0.0",
|
||||
"vite": "7.1.3",
|
||||
"sass": "1.89.2",
|
||||
"simple-git-hooks": "2.13.0",
|
||||
"tsx": "4.20.3",
|
||||
"typescript": "5.8.3",
|
||||
"unplugin-icons": "22.1.0",
|
||||
"unplugin-vue-components": "28.8.0",
|
||||
"vite": "7.0.4",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vite-plugin-static-copy": "^3.1.0",
|
||||
"vite-plugin-svg-icons": "2.0.1",
|
||||
"vite-plugin-vue-devtools": "8.0.1",
|
||||
"vite-plugin-vue-devtools": "7.7.7",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"vue-tsc": "3.0.6"
|
||||
"vue-tsc": "3.0.1"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"commit-msg": "pnpm sa git-commit-verify",
|
||||
|
@ -11,7 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@sa/utils": "workspace:*",
|
||||
"axios": "1.11.0",
|
||||
"axios": "1.10.0",
|
||||
"axios-retry": "4.5.0",
|
||||
"qs": "6.14.0"
|
||||
},
|
||||
|
@ -13,12 +13,11 @@ import type {
|
||||
ResponseType
|
||||
} from './type';
|
||||
|
||||
function createCommonRequest<
|
||||
ResponseData,
|
||||
ApiData = ResponseData,
|
||||
State extends Record<string, unknown> = Record<string, unknown>
|
||||
>(axiosConfig?: CreateAxiosDefaults, options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
|
||||
const opts = createDefaultOptions<ResponseData, ApiData, State>(options);
|
||||
function createCommonRequest<ResponseData = any>(
|
||||
axiosConfig?: CreateAxiosDefaults,
|
||||
options?: Partial<RequestOption<ResponseData>>
|
||||
) {
|
||||
const opts = createDefaultOptions<ResponseData>(options);
|
||||
|
||||
const axiosConf = createAxiosConfig(axiosConfig);
|
||||
const instance = axios.create(axiosConf);
|
||||
@ -81,6 +80,14 @@ function createCommonRequest<
|
||||
}
|
||||
);
|
||||
|
||||
function cancelRequest(requestId: string) {
|
||||
const abortController = abortControllerMap.get(requestId);
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
abortControllerMap.delete(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelAllRequest() {
|
||||
abortControllerMap.forEach(abortController => {
|
||||
abortController.abort();
|
||||
@ -91,6 +98,7 @@ function createCommonRequest<
|
||||
return {
|
||||
instance,
|
||||
opts,
|
||||
cancelRequest,
|
||||
cancelAllRequest
|
||||
};
|
||||
}
|
||||
@ -101,27 +109,27 @@ function createCommonRequest<
|
||||
* @param axiosConfig axios config
|
||||
* @param options request options
|
||||
*/
|
||||
export function createRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||
export function createRequest<ResponseData = any, State = Record<string, unknown>>(
|
||||
axiosConfig?: CreateAxiosDefaults,
|
||||
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||
options?: Partial<RequestOption<ResponseData>>
|
||||
) {
|
||||
const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
|
||||
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
|
||||
|
||||
const request: RequestInstance<ApiData, State> = async function request<
|
||||
T extends ApiData = ApiData,
|
||||
R extends ResponseType = 'json'
|
||||
>(config: CustomAxiosRequestConfig) {
|
||||
const request: RequestInstance<State> = async function request<T = any, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig
|
||||
) {
|
||||
const response: AxiosResponse<ResponseData> = await instance(config);
|
||||
|
||||
const responseType = response.config?.responseType || 'json';
|
||||
|
||||
if (responseType === 'json') {
|
||||
return opts.transform(response);
|
||||
return opts.transformBackendResponse(response);
|
||||
}
|
||||
|
||||
return response.data as MappedType<R, T>;
|
||||
} as RequestInstance<ApiData, State>;
|
||||
} as RequestInstance<State>;
|
||||
|
||||
request.cancelRequest = cancelRequest;
|
||||
request.cancelAllRequest = cancelAllRequest;
|
||||
request.state = {} as State;
|
||||
|
||||
@ -136,14 +144,14 @@ export function createRequest<ResponseData, ApiData, State extends Record<string
|
||||
* @param axiosConfig axios config
|
||||
* @param options request options
|
||||
*/
|
||||
export function createFlatRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||
export function createFlatRequest<ResponseData = any, State = Record<string, unknown>>(
|
||||
axiosConfig?: CreateAxiosDefaults,
|
||||
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||
options?: Partial<RequestOption<ResponseData>>
|
||||
) {
|
||||
const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
|
||||
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
|
||||
|
||||
const flatRequest: FlatRequestInstance<ResponseData, ApiData, State> = async function flatRequest<
|
||||
T extends ApiData = ApiData,
|
||||
const flatRequest: FlatRequestInstance<State, ResponseData> = async function flatRequest<
|
||||
T = any,
|
||||
R extends ResponseType = 'json'
|
||||
>(config: CustomAxiosRequestConfig) {
|
||||
try {
|
||||
@ -152,21 +160,20 @@ export function createFlatRequest<ResponseData, ApiData, State extends Record<st
|
||||
const responseType = response.config?.responseType || 'json';
|
||||
|
||||
if (responseType === 'json') {
|
||||
const data = await opts.transform(response);
|
||||
const data = opts.transformBackendResponse(response);
|
||||
|
||||
return { data, error: null, response };
|
||||
}
|
||||
|
||||
return { data: response.data as MappedType<R, T>, error: null, response };
|
||||
return { data: response.data as MappedType<R, T>, error: null };
|
||||
} catch (error) {
|
||||
return { data: null, error, response: (error as AxiosError<ResponseData>).response };
|
||||
}
|
||||
} as FlatRequestInstance<ResponseData, ApiData, State>;
|
||||
} as FlatRequestInstance<State, ResponseData>;
|
||||
|
||||
flatRequest.cancelRequest = cancelRequest;
|
||||
flatRequest.cancelAllRequest = cancelAllRequest;
|
||||
flatRequest.state = {
|
||||
...opts.defaultState
|
||||
} as State;
|
||||
flatRequest.state = {} as State;
|
||||
|
||||
return flatRequest;
|
||||
}
|
||||
|
@ -4,27 +4,15 @@ import { stringify } from 'qs';
|
||||
import { isHttpSuccess } from './shared';
|
||||
import type { RequestOption } from './type';
|
||||
|
||||
export function createDefaultOptions<
|
||||
ResponseData,
|
||||
ApiData = ResponseData,
|
||||
State extends Record<string, unknown> = Record<string, unknown>
|
||||
>(options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
|
||||
const opts: RequestOption<ResponseData, ApiData, State> = {
|
||||
defaultState: {} as State,
|
||||
transform: async response => response.data as unknown as ApiData,
|
||||
transformBackendResponse: async response => response.data as unknown as ApiData,
|
||||
export function createDefaultOptions<ResponseData = any>(options?: Partial<RequestOption<ResponseData>>) {
|
||||
const opts: RequestOption<ResponseData> = {
|
||||
onRequest: async config => config,
|
||||
isBackendSuccess: _response => true,
|
||||
onBackendFail: async () => {},
|
||||
transformBackendResponse: async response => response.data,
|
||||
onError: async () => {}
|
||||
};
|
||||
|
||||
if (options?.transform) {
|
||||
opts.transform = options.transform;
|
||||
} else {
|
||||
opts.transform = options?.transformBackendResponse || opts.transform;
|
||||
}
|
||||
|
||||
Object.assign(opts, options);
|
||||
|
||||
return opts;
|
||||
|
@ -8,30 +8,7 @@ export type ContentType =
|
||||
| 'application/x-www-form-urlencoded'
|
||||
| 'application/octet-stream';
|
||||
|
||||
export type ResponseTransform<Input = any, Output = any> = (input: Input) => Output | Promise<Output>;
|
||||
|
||||
export interface RequestOption<
|
||||
ResponseData,
|
||||
ApiData = ResponseData,
|
||||
State extends Record<string, unknown> = Record<string, unknown>
|
||||
> {
|
||||
/**
|
||||
* The default state
|
||||
*/
|
||||
defaultState?: State;
|
||||
/**
|
||||
* transform the response data to the api data
|
||||
*
|
||||
* @param response Axios response
|
||||
*/
|
||||
transform: ResponseTransform<AxiosResponse<ResponseData>, ApiData>;
|
||||
/**
|
||||
* transform the response data to the api data
|
||||
*
|
||||
* @deprecated use `transform` instead, will be removed in the next major version v3
|
||||
* @param response Axios response
|
||||
*/
|
||||
transformBackendResponse: ResponseTransform<AxiosResponse<ResponseData>, ApiData>;
|
||||
export interface RequestOption<ResponseData = any> {
|
||||
/**
|
||||
* The hook before request
|
||||
*
|
||||
@ -58,6 +35,12 @@ export interface RequestOption<
|
||||
response: AxiosResponse<ResponseData>,
|
||||
instance: AxiosInstance
|
||||
) => Promise<AxiosResponse | null> | Promise<void>;
|
||||
/**
|
||||
* transform backend response when the responseType is json
|
||||
*
|
||||
* @param response Axios response
|
||||
*/
|
||||
transformBackendResponse(response: AxiosResponse<ResponseData>): any | Promise<any>;
|
||||
/**
|
||||
* The hook to handle error
|
||||
*
|
||||
@ -85,7 +68,15 @@ export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<Axi
|
||||
responseType?: R;
|
||||
};
|
||||
|
||||
export interface RequestInstanceCommon<State extends Record<string, unknown>> {
|
||||
export interface RequestInstanceCommon<T> {
|
||||
/**
|
||||
* cancel the request by request id
|
||||
*
|
||||
* if the request provide abort controller sign from config, it will not collect in the abort controller map
|
||||
*
|
||||
* @param requestId
|
||||
*/
|
||||
cancelRequest: (requestId: string) => void;
|
||||
/**
|
||||
* cancel all request
|
||||
*
|
||||
@ -93,35 +84,32 @@ export interface RequestInstanceCommon<State extends Record<string, unknown>> {
|
||||
*/
|
||||
cancelAllRequest: () => void;
|
||||
/** you can set custom state in the request instance */
|
||||
state: State;
|
||||
state: T;
|
||||
}
|
||||
|
||||
/** The request instance */
|
||||
export interface RequestInstance<ApiData, State extends Record<string, unknown>> extends RequestInstanceCommon<State> {
|
||||
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig<R>
|
||||
): Promise<MappedType<R, T>>;
|
||||
export interface RequestInstance<S = Record<string, unknown>> extends RequestInstanceCommon<S> {
|
||||
<T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
|
||||
}
|
||||
|
||||
export type FlatResponseSuccessData<ResponseData, ApiData> = {
|
||||
data: ApiData;
|
||||
export type FlatResponseSuccessData<T = any, ResponseData = any> = {
|
||||
data: T;
|
||||
error: null;
|
||||
response: AxiosResponse<ResponseData>;
|
||||
};
|
||||
|
||||
export type FlatResponseFailData<ResponseData> = {
|
||||
export type FlatResponseFailData<ResponseData = any> = {
|
||||
data: null;
|
||||
error: AxiosError<ResponseData>;
|
||||
response: AxiosResponse<ResponseData>;
|
||||
};
|
||||
|
||||
export type FlatResponseData<ResponseData, ApiData> =
|
||||
| FlatResponseSuccessData<ResponseData, ApiData>
|
||||
export type FlatResponseData<T = any, ResponseData = any> =
|
||||
| FlatResponseSuccessData<T, ResponseData>
|
||||
| FlatResponseFailData<ResponseData>;
|
||||
|
||||
export interface FlatRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
|
||||
extends RequestInstanceCommon<State> {
|
||||
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||
export interface FlatRequestInstance<S = Record<string, unknown>, ResponseData = any> extends RequestInstanceCommon<S> {
|
||||
<T = any, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig<R>
|
||||
): Promise<FlatResponseData<ResponseData, MappedType<R, T>>>;
|
||||
): Promise<FlatResponseData<MappedType<R, T>, ResponseData>>;
|
||||
}
|
||||
|
@ -3,7 +3,9 @@ import useLoading from './use-loading';
|
||||
import useCountDown from './use-count-down';
|
||||
import useContext from './use-context';
|
||||
import useSvgIconRender from './use-svg-icon-render';
|
||||
import useTable from './use-table';
|
||||
import useHookTable from './use-table';
|
||||
|
||||
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useTable };
|
||||
export type * from './use-table';
|
||||
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable };
|
||||
|
||||
export * from './use-signal';
|
||||
export * from './use-table';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { inject, provide } from 'vue';
|
||||
import type { InjectionKey } from 'vue';
|
||||
|
||||
/**
|
||||
* Use context
|
||||
@ -11,7 +12,7 @@ import { inject, provide } from 'vue';
|
||||
* import { ref } from 'vue';
|
||||
* import { useContext } from '@sa/hooks';
|
||||
*
|
||||
* export const [provideDemoContext, useDemoContext] = useContext('demo', () => {
|
||||
* export const { setupStore, useStore } = useContext('demo', () => {
|
||||
* const count = ref(0);
|
||||
*
|
||||
* function increment() {
|
||||
@ -34,10 +35,10 @@ import { inject, provide } from 'vue';
|
||||
* <div>A</div>
|
||||
* </template>
|
||||
* <script setup lang="ts">
|
||||
* import { provideDemoContext } from './context';
|
||||
* import { setupStore } from './context';
|
||||
*
|
||||
* provideDemoContext();
|
||||
* // const { increment } = provideDemoContext(); // also can control the store in the parent component
|
||||
* setupStore();
|
||||
* // const { increment } = setupStore(); // also can control the store in the parent component
|
||||
* </script>
|
||||
* ``` // B.vue
|
||||
* ```vue
|
||||
@ -45,9 +46,9 @@ import { inject, provide } from 'vue';
|
||||
* <div>B</div>
|
||||
* </template>
|
||||
* <script setup lang="ts">
|
||||
* import { useDemoContext } from './context';
|
||||
* import { useStore } from './context';
|
||||
*
|
||||
* const { count, increment } = useDemoContext();
|
||||
* const { count, increment } = useStore();
|
||||
* </script>
|
||||
* ```;
|
||||
*
|
||||
@ -56,41 +57,40 @@ import { inject, provide } from 'vue';
|
||||
* @param contextName Context name
|
||||
* @param fn Context function
|
||||
*/
|
||||
export default function useContext<Arguments extends Array<any>, T>(
|
||||
contextName: string,
|
||||
composable: (...args: Arguments) => T
|
||||
) {
|
||||
const key = Symbol(contextName);
|
||||
export default function useContext<T extends (...args: any[]) => any>(contextName: string, fn: T) {
|
||||
type Context = ReturnType<T>;
|
||||
|
||||
/**
|
||||
* Injects the context value.
|
||||
*
|
||||
* @param consumerName - The name of the component that is consuming the context. If provided, the component must be
|
||||
* used within the context provider.
|
||||
* @param defaultValue - The default value to return if the context is not provided.
|
||||
* @returns The context value.
|
||||
*/
|
||||
const useInject = <N extends string | null | undefined = undefined>(
|
||||
consumerName?: N,
|
||||
defaultValue?: T
|
||||
): N extends null | undefined ? T | null : T => {
|
||||
const value = inject(key, defaultValue);
|
||||
const { useProvide, useInject: useStore } = createContext<Context>(contextName);
|
||||
|
||||
if (consumerName && !value) {
|
||||
throw new Error(`\`${consumerName}\` must be used within \`${contextName}\``);
|
||||
}
|
||||
function setupStore(...args: Parameters<T>) {
|
||||
const context: Context = fn(...args);
|
||||
return useProvide(context);
|
||||
}
|
||||
|
||||
// @ts-expect-error - we want to return null if the value is undefined or null
|
||||
return value || null;
|
||||
return {
|
||||
/** Setup store in the parent component */
|
||||
setupStore,
|
||||
/** Use store in the child component */
|
||||
useStore
|
||||
};
|
||||
}
|
||||
|
||||
/** Create context */
|
||||
function createContext<T>(contextName: string) {
|
||||
const injectKey: InjectionKey<T> = Symbol(contextName);
|
||||
|
||||
function useProvide(context: T) {
|
||||
provide(injectKey, context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function useInject() {
|
||||
return inject(injectKey) as T;
|
||||
}
|
||||
|
||||
return {
|
||||
useProvide,
|
||||
useInject
|
||||
};
|
||||
|
||||
const useProvide = (...args: Arguments) => {
|
||||
const value = composable(...args);
|
||||
|
||||
provide(key, value);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
return [useProvide, useInject] as const;
|
||||
}
|
||||
|
@ -6,31 +6,31 @@ import type {
|
||||
CreateAxiosDefaults,
|
||||
CustomAxiosRequestConfig,
|
||||
MappedType,
|
||||
RequestInstanceCommon,
|
||||
RequestOption,
|
||||
ResponseType
|
||||
} from '@sa/axios';
|
||||
import useLoading from './use-loading';
|
||||
|
||||
export type HookRequestInstanceResponseSuccessData<ApiData> = {
|
||||
data: Ref<ApiData>;
|
||||
export type HookRequestInstanceResponseSuccessData<T = any> = {
|
||||
data: Ref<T>;
|
||||
error: Ref<null>;
|
||||
};
|
||||
|
||||
export type HookRequestInstanceResponseFailData<ResponseData> = {
|
||||
export type HookRequestInstanceResponseFailData<ResponseData = any> = {
|
||||
data: Ref<null>;
|
||||
error: Ref<AxiosError<ResponseData>>;
|
||||
};
|
||||
|
||||
export type HookRequestInstanceResponseData<ResponseData, ApiData> = {
|
||||
export type HookRequestInstanceResponseData<T = any, ResponseData = any> = {
|
||||
loading: Ref<boolean>;
|
||||
} & (HookRequestInstanceResponseSuccessData<ApiData> | HookRequestInstanceResponseFailData<ResponseData>);
|
||||
} & (HookRequestInstanceResponseSuccessData<T> | HookRequestInstanceResponseFailData<ResponseData>);
|
||||
|
||||
export interface HookRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
|
||||
extends RequestInstanceCommon<State> {
|
||||
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||
export interface HookRequestInstance<ResponseData = any> {
|
||||
<T = any, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig
|
||||
): HookRequestInstanceResponseData<ResponseData, MappedType<R, T>>;
|
||||
): HookRequestInstanceResponseData<MappedType<R, T>, ResponseData>;
|
||||
cancelRequest: (requestId: string) => void;
|
||||
cancelAllRequest: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,26 +39,25 @@ export interface HookRequestInstance<ResponseData, ApiData, State extends Record
|
||||
* @param axiosConfig
|
||||
* @param options
|
||||
*/
|
||||
export default function createHookRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||
export default function createHookRequest<ResponseData = any>(
|
||||
axiosConfig?: CreateAxiosDefaults,
|
||||
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||
options?: Partial<RequestOption<ResponseData>>
|
||||
) {
|
||||
const request = createFlatRequest<ResponseData, ApiData, State>(axiosConfig, options);
|
||||
const request = createFlatRequest<ResponseData>(axiosConfig, options);
|
||||
|
||||
const hookRequest: HookRequestInstance<ResponseData, ApiData, State> = function hookRequest<
|
||||
T extends ApiData = ApiData,
|
||||
R extends ResponseType = 'json'
|
||||
>(config: CustomAxiosRequestConfig) {
|
||||
const hookRequest: HookRequestInstance<ResponseData> = function hookRequest<T = any, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig
|
||||
) {
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
|
||||
const data = ref(null) as Ref<MappedType<R, T>>;
|
||||
const error = ref(null) as Ref<AxiosError<ResponseData> | null>;
|
||||
const data = ref<MappedType<R, T> | null>(null) as Ref<MappedType<R, T>>;
|
||||
const error = ref<AxiosError<ResponseData> | null>(null) as Ref<AxiosError<ResponseData> | null>;
|
||||
|
||||
startLoading();
|
||||
|
||||
request(config).then(res => {
|
||||
if (res.data) {
|
||||
data.value = res.data as MappedType<R, T>;
|
||||
data.value = res.data;
|
||||
} else {
|
||||
error.value = res.error;
|
||||
}
|
||||
@ -71,8 +70,9 @@ export default function createHookRequest<ResponseData, ApiData, State extends R
|
||||
data,
|
||||
error
|
||||
};
|
||||
} as HookRequestInstance<ResponseData, ApiData, State>;
|
||||
} as HookRequestInstance<ResponseData>;
|
||||
|
||||
hookRequest.cancelRequest = request.cancelRequest;
|
||||
hookRequest.cancelAllRequest = request.cancelAllRequest;
|
||||
|
||||
return hookRequest;
|
||||
|
144
packages/hooks/src/use-signal.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import { computed, ref, shallowRef, triggerRef } from 'vue';
|
||||
import type {
|
||||
ComputedGetter,
|
||||
DebuggerOptions,
|
||||
Ref,
|
||||
ShallowRef,
|
||||
WritableComputedOptions,
|
||||
WritableComputedRef
|
||||
} from 'vue';
|
||||
|
||||
type Updater<T> = (value: T) => T;
|
||||
type Mutator<T> = (value: T) => void;
|
||||
|
||||
/**
|
||||
* Signal is a reactive value that can be set, updated or mutated
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const count = useSignal(0);
|
||||
*
|
||||
* // `watchEffect`
|
||||
* watchEffect(() => {
|
||||
* console.log(count());
|
||||
* });
|
||||
*
|
||||
* // watch
|
||||
* watch(count, value => {
|
||||
* console.log(value);
|
||||
* });
|
||||
*
|
||||
* // useComputed
|
||||
* const double = useComputed(() => count() * 2);
|
||||
* const writeableDouble = useComputed({
|
||||
* get: () => count() * 2,
|
||||
* set: value => count.set(value / 2)
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export interface Signal<T> {
|
||||
(): Readonly<T>;
|
||||
/**
|
||||
* Set the value of the signal
|
||||
*
|
||||
* It recommend use `set` for primitive values
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
set(value: T): void;
|
||||
/**
|
||||
* Update the value of the signal using an updater function
|
||||
*
|
||||
* It recommend use `update` for non-primitive values, only the first level of the object will be reactive.
|
||||
*
|
||||
* @param updater
|
||||
*/
|
||||
update(updater: Updater<T>): void;
|
||||
/**
|
||||
* Mutate the value of the signal using a mutator function
|
||||
*
|
||||
* this action will call `triggerRef`, so the value will be tracked on `watchEffect`.
|
||||
*
|
||||
* It recommend use `mutate` for non-primitive values, all levels of the object will be reactive.
|
||||
*
|
||||
* @param mutator
|
||||
*/
|
||||
mutate(mutator: Mutator<T>): void;
|
||||
/**
|
||||
* Get the reference of the signal
|
||||
*
|
||||
* Sometimes it can be useful to make `v-model` work with the signal
|
||||
*
|
||||
* ```vue
|
||||
* <template>
|
||||
* <input v-model="model.count" />
|
||||
* </template>;
|
||||
*
|
||||
* <script setup lang="ts">
|
||||
* const state = useSignal({ count: 0 }, { useRef: true });
|
||||
*
|
||||
* const model = state.getRef();
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
getRef(): Readonly<ShallowRef<Readonly<T>>>;
|
||||
}
|
||||
|
||||
export interface ReadonlySignal<T> {
|
||||
(): Readonly<T>;
|
||||
}
|
||||
|
||||
export interface SignalOptions {
|
||||
/**
|
||||
* Whether to use `ref` to store the value
|
||||
*
|
||||
* @default false use `sharedRef` to store the value
|
||||
*/
|
||||
useRef?: boolean;
|
||||
}
|
||||
|
||||
export function useSignal<T>(initialValue: T, options?: SignalOptions): Signal<T> {
|
||||
const { useRef } = options || {};
|
||||
|
||||
const state = useRef ? (ref(initialValue) as Ref<T>) : shallowRef(initialValue);
|
||||
|
||||
return createSignal(state);
|
||||
}
|
||||
|
||||
export function useComputed<T>(getter: ComputedGetter<T>, debugOptions?: DebuggerOptions): ReadonlySignal<T>;
|
||||
export function useComputed<T>(options: WritableComputedOptions<T>, debugOptions?: DebuggerOptions): Signal<T>;
|
||||
export function useComputed<T>(
|
||||
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
|
||||
debugOptions?: DebuggerOptions
|
||||
) {
|
||||
const isGetter = typeof getterOrOptions === 'function';
|
||||
|
||||
const computedValue = computed(getterOrOptions as any, debugOptions);
|
||||
|
||||
if (isGetter) {
|
||||
return () => computedValue.value as ReadonlySignal<T>;
|
||||
}
|
||||
|
||||
return createSignal(computedValue);
|
||||
}
|
||||
|
||||
function createSignal<T>(state: ShallowRef<T> | WritableComputedRef<T>): Signal<T> {
|
||||
const signal = () => state.value;
|
||||
|
||||
signal.set = (value: T) => {
|
||||
state.value = value;
|
||||
};
|
||||
|
||||
signal.update = (updater: Updater<T>) => {
|
||||
state.value = updater(state.value);
|
||||
};
|
||||
|
||||
signal.mutate = (mutator: Mutator<T>) => {
|
||||
mutator(state.value);
|
||||
triggerRef(state);
|
||||
};
|
||||
|
||||
signal.getRef = () => state as Readonly<ShallowRef<Readonly<T>>>;
|
||||
|
||||
return signal;
|
||||
}
|
@ -1,20 +1,12 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import type { Ref, VNodeChild } from 'vue';
|
||||
import { jsonClone } from '@sa/utils';
|
||||
import useBoolean from './use-boolean';
|
||||
import useLoading from './use-loading';
|
||||
|
||||
export interface PaginationData<T> {
|
||||
data: T[];
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
export type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
type GetApiData<ApiData, Pagination extends boolean> = Pagination extends true ? PaginationData<ApiData> : ApiData[];
|
||||
|
||||
type Transform<ResponseData, ApiData, Pagination extends boolean> = (
|
||||
response: ResponseData
|
||||
) => GetApiData<ApiData, Pagination>;
|
||||
export type ApiFn = (args: any) => Promise<unknown>;
|
||||
|
||||
export type TableColumnCheckTitle = string | ((...args: any) => VNodeChild);
|
||||
|
||||
@ -22,64 +14,74 @@ export type TableColumnCheck = {
|
||||
key: string;
|
||||
title: TableColumnCheckTitle;
|
||||
checked: boolean;
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
export interface UseTableOptions<ResponseData, ApiData, Column, Pagination extends boolean> {
|
||||
/**
|
||||
* api function to get table data
|
||||
*/
|
||||
api: () => Promise<ResponseData>;
|
||||
/**
|
||||
* whether to enable pagination
|
||||
*/
|
||||
pagination?: Pagination;
|
||||
/**
|
||||
* transform api response to table data
|
||||
*/
|
||||
transform: Transform<ResponseData, ApiData, Pagination>;
|
||||
/**
|
||||
* columns factory
|
||||
*/
|
||||
columns: () => Column[];
|
||||
export type TableDataWithIndex<T> = T & { index: number };
|
||||
|
||||
export type TransformedData<T> = {
|
||||
data: TableDataWithIndex<T>[];
|
||||
total?: number;
|
||||
};
|
||||
|
||||
export type Transformer<T, Response> = (response: Response) => TransformedData<T>;
|
||||
|
||||
export type TableConfig<A extends ApiFn, T, C> = {
|
||||
/** api function to get table data */
|
||||
apiFn: A;
|
||||
/** api params */
|
||||
apiParams?: Parameters<A>[0];
|
||||
/** transform api response to table data */
|
||||
transformer: Transformer<T, Awaited<ReturnType<A>>>;
|
||||
/** columns factory */
|
||||
columns: () => C[];
|
||||
/**
|
||||
* get column checks
|
||||
*
|
||||
* @param columns
|
||||
*/
|
||||
getColumnChecks: (columns: Column[]) => TableColumnCheck[];
|
||||
getColumnChecks: (columns: C[]) => TableColumnCheck[];
|
||||
/**
|
||||
* get columns
|
||||
*
|
||||
* @param columns
|
||||
*/
|
||||
getColumns: (columns: Column[], checks: TableColumnCheck[]) => Column[];
|
||||
getColumns: (columns: C[], checks: TableColumnCheck[]) => C[];
|
||||
/**
|
||||
* callback when response fetched
|
||||
*
|
||||
* @param transformed transformed data
|
||||
*/
|
||||
onFetched?: (data: GetApiData<ApiData, Pagination>) => void | Promise<void>;
|
||||
onFetched?: (transformed: TransformedData<T>) => MaybePromise<void>;
|
||||
/**
|
||||
* whether to get data immediately
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
immediate?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export default function useTable<ResponseData, ApiData, Column, Pagination extends boolean>(
|
||||
options: UseTableOptions<ResponseData, ApiData, Column, Pagination>
|
||||
) {
|
||||
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
const { bool: empty, setBool: setEmpty } = useBoolean();
|
||||
|
||||
const { api, pagination, transform, columns, getColumnChecks, getColumns, onFetched, immediate = true } = options;
|
||||
const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
|
||||
|
||||
const data = ref([]) as Ref<ApiData[]>;
|
||||
const searchParams: NonNullable<Parameters<A>[0]> = reactive(jsonClone({ ...apiParams }));
|
||||
|
||||
const columnChecks = ref(getColumnChecks(columns())) as Ref<TableColumnCheck[]>;
|
||||
const allColumns = ref(config.columns()) as Ref<C[]>;
|
||||
|
||||
const $columns = computed(() => getColumns(columns(), columnChecks.value));
|
||||
const data: Ref<TableDataWithIndex<T>[]> = ref([]);
|
||||
|
||||
const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
|
||||
|
||||
const columns = computed(() => getColumns(allColumns.value, columnChecks.value));
|
||||
|
||||
function reloadColumns() {
|
||||
allColumns.value = config.columns();
|
||||
|
||||
const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
|
||||
|
||||
const defaultChecks = getColumnChecks(columns());
|
||||
const defaultChecks = getColumnChecks(allColumns.value);
|
||||
|
||||
columnChecks.value = defaultChecks.map(col => ({
|
||||
...col,
|
||||
@ -88,21 +90,48 @@ export default function useTable<ResponseData, ApiData, Column, Pagination exten
|
||||
}
|
||||
|
||||
async function getData() {
|
||||
try {
|
||||
startLoading();
|
||||
startLoading();
|
||||
|
||||
const response = await api();
|
||||
const formattedParams = formatSearchParams(searchParams);
|
||||
|
||||
const transformed = transform(response);
|
||||
const response = await apiFn(formattedParams);
|
||||
|
||||
data.value = getTableData(transformed, pagination);
|
||||
const transformed = transformer(response as Awaited<ReturnType<A>>);
|
||||
|
||||
setEmpty(data.value.length === 0);
|
||||
data.value = transformed.data;
|
||||
|
||||
await onFetched?.(transformed);
|
||||
} finally {
|
||||
endLoading();
|
||||
}
|
||||
setEmpty(transformed.data.length === 0);
|
||||
|
||||
await config.onFetched?.(transformed);
|
||||
|
||||
endLoading();
|
||||
}
|
||||
|
||||
function formatSearchParams(params: Record<string, unknown>) {
|
||||
const formattedParams: Record<string, unknown> = {};
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
formattedParams[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return formattedParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* update search params
|
||||
*
|
||||
* @param params
|
||||
*/
|
||||
function updateSearchParams(params: Partial<Parameters<A>[0]>) {
|
||||
Object.assign(searchParams, params);
|
||||
}
|
||||
|
||||
/** reset search params */
|
||||
function resetSearchParams() {
|
||||
Object.assign(searchParams, jsonClone(apiParams));
|
||||
getData();
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
@ -113,20 +142,12 @@ export default function useTable<ResponseData, ApiData, Column, Pagination exten
|
||||
loading,
|
||||
empty,
|
||||
data,
|
||||
columns: $columns,
|
||||
columns,
|
||||
columnChecks,
|
||||
reloadColumns,
|
||||
getData
|
||||
getData,
|
||||
searchParams,
|
||||
updateSearchParams,
|
||||
resetSearchParams
|
||||
};
|
||||
}
|
||||
|
||||
function getTableData<ApiData, Pagination extends boolean>(
|
||||
data: GetApiData<ApiData, Pagination>,
|
||||
pagination?: Pagination
|
||||
) {
|
||||
if (pagination) {
|
||||
return (data as PaginationData<ApiData>).data;
|
||||
}
|
||||
|
||||
return data as ApiData[];
|
||||
}
|
||||
|
@ -127,6 +127,7 @@ function handleClickMask() {
|
||||
:class="[
|
||||
style['layout-header'],
|
||||
commonClass,
|
||||
headerClass,
|
||||
headerLeftGapClass,
|
||||
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
|
||||
]"
|
||||
|
@ -6,6 +6,12 @@ interface AdminLayoutHeaderConfig {
|
||||
* @default true
|
||||
*/
|
||||
headerVisible?: boolean;
|
||||
/**
|
||||
* Header class
|
||||
*
|
||||
* @default ''
|
||||
*/
|
||||
headerClass?: string;
|
||||
/**
|
||||
* Header height
|
||||
*
|
||||
|
15
packages/ofetch/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@sa/fetch",
|
||||
"version": "1.3.15",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ofetch": "1.4.1"
|
||||
}
|
||||
}
|
10
packages/ofetch/src/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { ofetch } from 'ofetch';
|
||||
import type { FetchOptions } from 'ofetch';
|
||||
|
||||
export function createRequest(options: FetchOptions) {
|
||||
const request = ofetch.create(options);
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
export default createRequest;
|
20
packages/ofetch/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"jsx": "preserve",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
@ -14,15 +14,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@soybeanjs/changelog": "0.3.24",
|
||||
"bumpp": "10.2.3",
|
||||
"c12": "3.2.0",
|
||||
"bumpp": "10.2.0",
|
||||
"c12": "3.0.4",
|
||||
"cac": "6.7.14",
|
||||
"consola": "3.4.2",
|
||||
"enquirer": "2.4.1",
|
||||
"execa": "9.6.0",
|
||||
"kolorist": "1.8.0",
|
||||
"npm-check-updates": "18.0.3",
|
||||
"picomatch": "4.0.3",
|
||||
"npm-check-updates": "18.0.1",
|
||||
"rimraf": "6.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -32,8 +32,7 @@ export function createStorage<T extends object>(type: StorageType, storagePrefix
|
||||
storageData = JSON.parse(json);
|
||||
} catch {}
|
||||
|
||||
// storageData may be `false` if it is boolean type
|
||||
if (storageData !== null) {
|
||||
if (storageData) {
|
||||
return storageData as T[K];
|
||||
}
|
||||
}
|
||||
|
2661
pnpm-lock.yaml
generated
BIN
public/logo.png
Normal file
After Width: | Height: | Size: 58 KiB |
3
src-tauri/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
4580
src-tauri/Cargo.lock
generated
Normal file
26
src-tauri/Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
default-run = "app"
|
||||
edition = "2021"
|
||||
rust-version = "1.60"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "2", features = [] }
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
|
||||
# DO NOT REMOVE!!
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
3
src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
7
src-tauri/capabilities/migrated.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"identifier": "migrated",
|
||||
"description": "permissions that were migrated from v1",
|
||||
"local": true,
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default"]
|
||||
}
|
1676
src-tauri/gen/schemas/acl-manifests.json
Normal file
9
src-tauri/gen/schemas/capabilities.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"migrated": {
|
||||
"identifier": "migrated",
|
||||
"description": "permissions that were migrated from v1",
|
||||
"local": true,
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default"]
|
||||
}
|
||||
}
|
1778
src-tauri/gen/schemas/desktop-schema.json
Normal file
1778
src-tauri/gen/schemas/macOS-schema.json
Normal file
1778
src-tauri/gen/schemas/windows-schema.json
Normal file
BIN
src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
src-tauri/icons/64x64.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 52 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
src-tauri/icons/icon.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
After Width: | Height: | Size: 14 KiB |
8
src-tauri/src/main.rs
Normal file
@ -0,0 +1,8 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
57
src-tauri/tauri.conf.json
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"build": {
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:9527"
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"copyright": "RuoYi-Plus-Soybean",
|
||||
"targets": "all",
|
||||
"externalBin": [],
|
||||
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
},
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"entitlements": null,
|
||||
"exceptionDomain": "",
|
||||
"frameworks": [],
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"productName": "RuoYi-Plus-Soybean",
|
||||
"mainBinaryName": "RuoYi-Plus-Soybean",
|
||||
"version": "1.0.0",
|
||||
"identifier": "org.dromara.admin",
|
||||
"plugins": {},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"fullscreen": false,
|
||||
"height": 768,
|
||||
"resizable": true,
|
||||
"title": "RuoYi-Plus-Soybean",
|
||||
"width": 1366,
|
||||
"useHttpsScheme": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
}
|
||||
}
|
19
src/App.vue
@ -4,6 +4,7 @@ import { NConfigProvider, darkTheme } from 'naive-ui';
|
||||
import type { WatermarkProps } from 'naive-ui';
|
||||
import { useAppStore } from './store/modules/app';
|
||||
import { useThemeStore } from './store/modules/theme';
|
||||
import { useAuthStore } from './store/modules/auth';
|
||||
import { naiveDateLocales, naiveLocales } from './locales/naive';
|
||||
|
||||
defineOptions({
|
||||
@ -12,6 +13,7 @@ defineOptions({
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const { userInfo } = useAuthStore();
|
||||
|
||||
const naiveDarkTheme = computed(() => (themeStore.darkMode ? darkTheme : undefined));
|
||||
|
||||
@ -24,19 +26,24 @@ const naiveDateLocale = computed(() => {
|
||||
});
|
||||
|
||||
const watermarkProps = computed<WatermarkProps>(() => {
|
||||
const appTitle = import.meta.env.VITE_APP_TITLE || 'RuoYi-Vue-Plus';
|
||||
const content =
|
||||
themeStore.watermark.enableUserName && userInfo.user?.userName
|
||||
? `${userInfo.user?.nickName}@${appTitle} ${userInfo.user?.userName}`
|
||||
: appTitle;
|
||||
return {
|
||||
content: themeStore.watermarkContent,
|
||||
content,
|
||||
cross: true,
|
||||
fullscreen: true,
|
||||
fontSize: 14,
|
||||
fontColor: themeStore.darkMode ? 'rgba(200, 200, 200, 0.03)' : 'rgba(200, 200, 200, 0.2)',
|
||||
lineHeight: 14,
|
||||
width: 384,
|
||||
height: 384,
|
||||
width: 200,
|
||||
height: 300,
|
||||
xOffset: 12,
|
||||
yOffset: 60,
|
||||
rotate: -13,
|
||||
zIndex: 9999,
|
||||
fontColor: themeStore.darkMode ? 'rgba(200, 200, 200, 0.03)' : 'rgba(200, 200, 200, 0.2)'
|
||||
rotate: -18,
|
||||
zIndex: 9999
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
@ -22,12 +22,7 @@ const columns = defineModel<NaiveUI.TableColumnCheck[]>('columns', {
|
||||
</NButton>
|
||||
</template>
|
||||
<VueDraggable v-model="columns" :animation="150" filter=".none_draggable">
|
||||
<div
|
||||
v-for="item in columns"
|
||||
:key="item.key"
|
||||
class="h-36px flex-y-center rd-4px hover:(bg-primary bg-opacity-20)"
|
||||
:class="{ hidden: !item.visible }"
|
||||
>
|
||||
<div v-for="item in columns" :key="item.key" class="h-36px flex-y-center rd-4px hover:(bg-primary bg-opacity-20)">
|
||||
<icon-mdi-drag class="mr-8px h-full cursor-move text-icon" />
|
||||
<NCheckbox v-model:checked="item.checked" class="none_draggable flex-1">
|
||||
<template v-if="typeof item.title === 'function'">
|
||||
|
@ -1,42 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, useSlots } from 'vue';
|
||||
import type { PopoverPlacement } from 'naive-ui';
|
||||
|
||||
defineOptions({ name: 'IconTooltip' });
|
||||
|
||||
interface Props {
|
||||
icon?: string;
|
||||
localIcon?: string;
|
||||
desc?: string;
|
||||
placement?: PopoverPlacement;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
icon: 'mdi-help-circle',
|
||||
localIcon: '',
|
||||
desc: '',
|
||||
placement: 'top'
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
const hasCustomTrigger = computed(() => Boolean(slots.trigger));
|
||||
|
||||
if (!hasCustomTrigger.value && !props.icon && !props.localIcon) {
|
||||
throw new Error('icon or localIcon is required when no custom trigger slot is provided');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NTooltip :placement="placement">
|
||||
<template #trigger>
|
||||
<slot name="trigger">
|
||||
<div class="cursor-pointer">
|
||||
<SvgIcon :icon="icon" :local-icon="localIcon" />
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
<slot>
|
||||
<span>{{ desc }}</span>
|
||||
</slot>
|
||||
</NTooltip>
|
||||
</template>
|
@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, useAttrs } from 'vue';
|
||||
import type { TagProps } from 'naive-ui';
|
||||
import { jsonClone } from '@sa/utils';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
import { isNotNull } from '@/utils/common';
|
||||
import { $t } from '@/locales';
|
||||
@ -29,7 +28,7 @@ const { transformDictData } = useDict(props.dictCode, props.immediate);
|
||||
|
||||
const dictTagData = computed<Api.System.DictData[]>(() => {
|
||||
if (props.dictData) {
|
||||
const dictData = jsonClone(props.dictData);
|
||||
const dictData = props.dictData;
|
||||
if (dictData.dictLabel?.startsWith(`dict.${dictData.dictType}.`)) {
|
||||
dictData.dictLabel = $t(dictData.dictLabel as App.I18n.I18nKey);
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
import { computed } from 'vue';
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import json from 'highlight.js/lib/languages/json';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
hljs.registerLanguage('json', json);
|
||||
|
||||
@ -11,19 +10,15 @@ defineOptions({
|
||||
});
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
code?: string;
|
||||
showLineNumbers?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
class: '',
|
||||
code: '',
|
||||
showLineNumbers: false
|
||||
});
|
||||
|
||||
const DEFAULT_CLASS = 'max-h-500px';
|
||||
|
||||
/** 格式化JSON数据 */
|
||||
const jsonData = computed<string>(() => {
|
||||
if (!props.code) return '';
|
||||
@ -38,9 +33,9 @@ const jsonData = computed<string>(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NScrollbar :class="twMerge(DEFAULT_CLASS, props.class)">
|
||||
<NCode :code="jsonData" :hljs="hljs" language="json" :show-line-numbers="showLineNumbers" :word-wrap="true" />
|
||||
</NScrollbar>
|
||||
<div class="json-preview">
|
||||
<NCode :code="jsonData" :hljs="hljs" language="json" :show-line-numbers="showLineNumbers" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@ -49,4 +44,18 @@ html[class='dark'] {
|
||||
background-color: #7c7777;
|
||||
}
|
||||
}
|
||||
.json-preview {
|
||||
width: 100%;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
@include scrollbar();
|
||||
.empty-data {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,524 +1,61 @@
|
||||
<!-- Copyright By https://github.com/Daymychen/art-design-pro/blob/main/src/components/core/views/login/LoginLeftView.vue -->
|
||||
<script lang="ts" setup>
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { computed } from 'vue';
|
||||
import { getPaletteColorByNumber } from '@sa/color';
|
||||
|
||||
defineOptions({ name: 'WaveBg' });
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
function toggleThemeScheme() {
|
||||
if (themeStore.darkMode) {
|
||||
themeStore.setThemeScheme('light');
|
||||
return;
|
||||
}
|
||||
themeStore.setThemeScheme('dark');
|
||||
interface Props {
|
||||
/** Theme color */
|
||||
themeColor: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const lightColor = computed(() => getPaletteColorByNumber(props.themeColor, 200));
|
||||
const darkColor = computed(() => getPaletteColorByNumber(props.themeColor, 500));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wave-bg">
|
||||
<!-- 几何装饰元素 -->
|
||||
<div class="geometric-decorations">
|
||||
<!-- 基础几何形状 -->
|
||||
<div class="geo-element circle-outline animate-fade-in-up animate-delay-0s"></div>
|
||||
<div class="geo-element square-rotated animate-fade-in-left animate-delay-0s"></div>
|
||||
<div class="geo-element circle-small animate-fade-in-up animate-delay-0.3s"></div>
|
||||
|
||||
<div class="geo-element square-bottom-right animate-fade-in-right animate-delay-0s"></div>
|
||||
|
||||
<!-- 背景泡泡 -->
|
||||
<div class="geo-element bg-bubble animate-scale-in animate-delay-0.5s"></div>
|
||||
|
||||
<!-- 太阳/月亮 -->
|
||||
<div
|
||||
class="geo-element circle-top-right animate-fade-in-down animate-delay-0.5s"
|
||||
@click="toggleThemeScheme"
|
||||
></div>
|
||||
|
||||
<!-- 装饰点 -->
|
||||
<div class="geo-element dot dot-top-left animate-bounce-in animate-delay-0s"></div>
|
||||
<div class="geo-element dot dot-top-right animate-bounce-in animate-delay-0s"></div>
|
||||
<div class="geo-element dot dot-center-right animate-bounce-in animate-delay-0s"></div>
|
||||
|
||||
<!-- 叠加方块组 -->
|
||||
<div class="squares-group">
|
||||
<i class="geo-element square square-blue animate-fade-in-left-rotated-blue animate-delay-0.2s"></i>
|
||||
<i class="geo-element square square-pink animate-fade-in-left-rotated-pink animate-delay-0.4s"></i>
|
||||
<i class="geo-element square square-purple animate-fade-in-left-no-rotation animate-delay-0.6s"></i>
|
||||
</div>
|
||||
<div class="absolute-lt z-1 size-full overflow-hidden">
|
||||
<div class="absolute -right-300px -top-900px lt-sm:(-right-100px -top-1170px)">
|
||||
<svg height="1337" width="1337">
|
||||
<defs>
|
||||
<path
|
||||
id="path-1"
|
||||
opacity="1"
|
||||
fill-rule="evenodd"
|
||||
d="M1337,668.5 C1337,1037.455193874239 1037.455193874239,1337 668.5,1337 C523.6725684305388,1337 337,1236 370.50000000000006,1094 C434.03835568300906,824.6732385973953 6.906089672974592e-14,892.6277623047779 0,668.5000000000001 C0,299.5448061257611 299.5448061257609,1.1368683772161603e-13 668.4999999999999,0 C1037.455193874239,0 1337,299.544806125761 1337,668.5Z"
|
||||
/>
|
||||
<linearGradient id="linearGradient-2" x1="0.79" y1="0.62" x2="0.21" y2="0.86">
|
||||
<stop offset="0" :stop-color="lightColor" stop-opacity="1" />
|
||||
<stop offset="1" :stop-color="darkColor" stop-opacity="1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g opacity="1">
|
||||
<use xlink:href="#path-1" fill="url(#linearGradient-2)" fill-opacity="1" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="absolute -bottom-400px -left-200px lt-sm:(-bottom-760px -left-100px)">
|
||||
<svg height="896" width="967.8852157128662">
|
||||
<defs>
|
||||
<path
|
||||
id="path-2"
|
||||
opacity="1"
|
||||
fill-rule="evenodd"
|
||||
d="M896,448 C1142.6325445712241,465.5747656464056 695.2579309733121,896 448,896 C200.74206902668806,896 5.684341886080802e-14,695.2579309733121 0,448.0000000000001 C0,200.74206902668806 200.74206902668791,5.684341886080802e-14 447.99999999999994,0 C695.2579309733121,0 475,418 896,448Z"
|
||||
/>
|
||||
<linearGradient id="linearGradient-3" x1="0.5" y1="0" x2="0.5" y2="1">
|
||||
<stop offset="0" :stop-color="darkColor" stop-opacity="1" />
|
||||
<stop offset="1" :stop-color="lightColor" stop-opacity="1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g opacity="1">
|
||||
<use xlink:href="#path-2" fill="url(#linearGradient-3)" fill-opacity="1" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 颜色变量定义
|
||||
$primary-light-7: rgb(var(--primary-50-color));
|
||||
$primary-light-8: rgb(var(--primary-100-color));
|
||||
$primary-light-9: rgb(var(--primary-200-color));
|
||||
$primary-base: rgb(var(--primary-color));
|
||||
$main-bg: rgb(var(--primary-50-color));
|
||||
|
||||
// 混合颜色函数
|
||||
$bg-mix-light-9: color-mix(in srgb, $primary-light-9 100%, $main-bg);
|
||||
$bg-mix-light-8: color-mix(in srgb, $primary-light-8 80%, $main-bg);
|
||||
$bg-mix-light-7: color-mix(in srgb, $primary-light-7 80%, $main-bg);
|
||||
|
||||
.wave-bg {
|
||||
.geometric-decorations {
|
||||
.geo-element {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
animation-fill-mode: forwards;
|
||||
animation-duration: 0.8s;
|
||||
animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
// 动画 mixin
|
||||
@mixin fadeAnimation($direction: '', $rotation: 0deg) {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
@if $direction == 'up' {
|
||||
transform: translateY(30px) rotate($rotation);
|
||||
} @else if $direction == 'down' {
|
||||
transform: translateY(-30px) rotate($rotation);
|
||||
} @else if $direction == 'left' {
|
||||
transform: translateX(-30px) rotate($rotation);
|
||||
} @else if $direction == 'right' {
|
||||
transform: translateX(30px) rotate($rotation);
|
||||
}
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
|
||||
@if $direction == 'up' or $direction == 'down' {
|
||||
transform: translateY(0) rotate($rotation);
|
||||
} @else {
|
||||
transform: translateX(0) rotate($rotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 动画定义
|
||||
@keyframes fadeInUp {
|
||||
@include fadeAnimation('up');
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
@include fadeAnimation('down');
|
||||
}
|
||||
|
||||
@keyframes fadeInLeft {
|
||||
@include fadeAnimation('left');
|
||||
}
|
||||
|
||||
@keyframes fadeInLeftRotated {
|
||||
@include fadeAnimation('left', -25deg);
|
||||
}
|
||||
|
||||
@keyframes fadeInRight {
|
||||
@include fadeAnimation('right');
|
||||
}
|
||||
|
||||
@keyframes fadeInRightRotated {
|
||||
@include fadeAnimation('right', 45deg);
|
||||
}
|
||||
|
||||
@keyframes fadeInLeftRotatedBlue {
|
||||
@include fadeAnimation('left', -10deg);
|
||||
}
|
||||
|
||||
@keyframes fadeInLeftRotatedPink {
|
||||
@include fadeAnimation('left', 10deg);
|
||||
}
|
||||
|
||||
@keyframes fadeInLeftNoRotation {
|
||||
@include fadeAnimation('left');
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.3);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes lineGrow {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 动画类
|
||||
.animate-fade-in-up {
|
||||
animation-name: fadeInUp;
|
||||
}
|
||||
|
||||
.animate-fade-in-down {
|
||||
animation-name: fadeInDown;
|
||||
}
|
||||
|
||||
.animate-fade-in-left {
|
||||
animation-name: fadeInLeft;
|
||||
}
|
||||
|
||||
.animate-fade-in-right {
|
||||
animation-name: fadeInRight;
|
||||
}
|
||||
|
||||
.animate-scale-in {
|
||||
animation-name: scaleIn;
|
||||
animation-duration: 1.2s;
|
||||
}
|
||||
|
||||
.animate-bounce-in {
|
||||
animation-name: bounceIn;
|
||||
animation-duration: 0.6s;
|
||||
}
|
||||
|
||||
.animate-fade-in-left-rotated-blue {
|
||||
animation-name: fadeInLeftRotatedBlue;
|
||||
}
|
||||
|
||||
.animate-fade-in-left-rotated-pink {
|
||||
animation-name: fadeInLeftRotatedPink;
|
||||
}
|
||||
|
||||
.animate-fade-in-left-no-rotation {
|
||||
animation-name: fadeInLeftNoRotation;
|
||||
}
|
||||
|
||||
// 基础几何形状
|
||||
.circle-outline {
|
||||
top: 10%;
|
||||
left: 25%;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border: 2px solid $primary-light-8;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.square-rotated {
|
||||
top: 50%;
|
||||
left: 16%;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background-color: $bg-mix-light-8;
|
||||
|
||||
&.animate-fade-in-left {
|
||||
animation-name: fadeInLeftRotated;
|
||||
}
|
||||
}
|
||||
|
||||
.circle-small {
|
||||
bottom: 26%;
|
||||
left: 30%;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: $primary-light-8;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
// 太阳/月亮效果
|
||||
.circle-top-right {
|
||||
top: 3%;
|
||||
right: 3%;
|
||||
z-index: 100;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
background: $bg-mix-light-7;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: '';
|
||||
background: linear-gradient(to right, #fcbb04, #fffc00);
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transition: all 0.5s;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 36px #fffc00;
|
||||
|
||||
&::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.square-bottom-right {
|
||||
right: 10%;
|
||||
bottom: 10%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: $primary-light-8;
|
||||
|
||||
&.animate-fade-in-right {
|
||||
animation-name: fadeInRightRotated;
|
||||
}
|
||||
}
|
||||
|
||||
// 背景泡泡
|
||||
.bg-bubble {
|
||||
top: -120px;
|
||||
right: -120px;
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
background-color: $bg-mix-light-8;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
// 装饰点
|
||||
.dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-color: $primary-light-7;
|
||||
border-radius: 50%;
|
||||
|
||||
&.dot-top-left {
|
||||
top: 140px;
|
||||
left: 100px;
|
||||
}
|
||||
|
||||
&.dot-top-right {
|
||||
top: 140px;
|
||||
right: 120px;
|
||||
}
|
||||
|
||||
&.dot-center-right {
|
||||
top: 46%;
|
||||
right: 22%;
|
||||
background-color: $primary-light-8;
|
||||
}
|
||||
}
|
||||
|
||||
// 叠加方块组
|
||||
.squares-group {
|
||||
position: absolute;
|
||||
bottom: 18px;
|
||||
left: 20px;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
pointer-events: none;
|
||||
|
||||
.square {
|
||||
position: absolute;
|
||||
display: block;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgb(64 87 167 / 12%);
|
||||
|
||||
&.square-blue {
|
||||
top: 12px;
|
||||
left: 30px;
|
||||
z-index: 2;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: rgb(from $primary-base r g b / 30%);
|
||||
}
|
||||
|
||||
&.square-pink {
|
||||
top: 30px;
|
||||
left: 48px;
|
||||
z-index: 1;
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
background-color: rgb(from $primary-base r g b / 15%);
|
||||
}
|
||||
|
||||
&.square-purple {
|
||||
top: 66px;
|
||||
left: 86px;
|
||||
z-index: 3;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: rgb(from $primary-base r g b / 45%);
|
||||
}
|
||||
}
|
||||
|
||||
// 装饰线条
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 86px;
|
||||
left: 72px;
|
||||
width: 80px;
|
||||
height: 1px;
|
||||
content: '';
|
||||
background: linear-gradient(90deg, var(--el-color-primary-light-6), transparent);
|
||||
opacity: 0;
|
||||
transform: rotate(50deg);
|
||||
animation: lineGrow 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1600px) {
|
||||
width: 60vw;
|
||||
|
||||
.text-wrap {
|
||||
bottom: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1280px) {
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
// 隐藏背景和其他内容,只保留 logo
|
||||
background: transparent;
|
||||
|
||||
.left-img,
|
||||
.text-wrap,
|
||||
.geometric-decorations {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: fixed;
|
||||
top: 15px;
|
||||
left: 25px;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 暗色主题
|
||||
.dark .wave-bg {
|
||||
background-color: color-mix(in srgb, $primary-light-9 60%, #070707);
|
||||
|
||||
@media only screen and (max-width: 1280px) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.geometric-decorations {
|
||||
// 月亮效果
|
||||
.circle-top-right {
|
||||
background-color: $bg-mix-light-8;
|
||||
box-shadow: 0 0 25px #333 inset;
|
||||
transition: all 0.3s ease-in-out 0.1s;
|
||||
rotate: -48deg;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 15px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
content: '';
|
||||
background-color: $bg-mix-light-9;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
box-shadow: 0 40px 25px #ddd inset;
|
||||
|
||||
&::before {
|
||||
left: 18px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bg-bubble {
|
||||
background-color: $bg-mix-light-9;
|
||||
}
|
||||
|
||||
// 其他元素颜色调整
|
||||
.square-rotated {
|
||||
background-color: $bg-mix-light-9;
|
||||
}
|
||||
|
||||
.circle-small,
|
||||
.dot {
|
||||
background-color: $primary-light-8;
|
||||
}
|
||||
|
||||
.square-bottom-right {
|
||||
background-color: $primary-light-9;
|
||||
}
|
||||
|
||||
.dot.dot-top-right {
|
||||
background-color: $primary-light-8;
|
||||
}
|
||||
}
|
||||
|
||||
// 方块组暗色调整
|
||||
.squares-group {
|
||||
.square {
|
||||
box-shadow: none;
|
||||
|
||||
&.square-blue {
|
||||
background-color: rgb(from $primary-base r g b / 18%);
|
||||
}
|
||||
|
||||
&.square-pink {
|
||||
background-color: rgb(from $primary-base r g b / 10%);
|
||||
}
|
||||
|
||||
&.square-purple {
|
||||
background-color: rgb(from $primary-base r g b / 20%);
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
background: linear-gradient(90deg, $primary-light-8, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
@ -5,9 +5,9 @@ export const GLOBAL_HEADER_MENU_ID = '__GLOBAL_HEADER_MENU__';
|
||||
export const GLOBAL_SIDER_MENU_ID = '__GLOBAL_SIDER_MENU__';
|
||||
|
||||
export const themeSchemaRecord: Record<UnionKey.ThemeScheme, App.I18n.I18nKey> = {
|
||||
light: 'theme.appearance.themeSchema.light',
|
||||
dark: 'theme.appearance.themeSchema.dark',
|
||||
auto: 'theme.appearance.themeSchema.auto'
|
||||
light: 'theme.themeSchema.light',
|
||||
dark: 'theme.themeSchema.dark',
|
||||
auto: 'theme.themeSchema.auto'
|
||||
};
|
||||
|
||||
export const themeSchemaOptions = transformRecordToOption(themeSchemaRecord);
|
||||
@ -21,61 +21,49 @@ export const loginModuleRecord: Record<UnionKey.LoginModule, App.I18n.I18nKey> =
|
||||
};
|
||||
|
||||
export const themeLayoutModeRecord: Record<UnionKey.ThemeLayoutMode, App.I18n.I18nKey> = {
|
||||
vertical: 'theme.layout.layoutMode.vertical',
|
||||
'vertical-mix': 'theme.layout.layoutMode.vertical-mix',
|
||||
'vertical-hybrid-header-first': 'theme.layout.layoutMode.vertical-hybrid-header-first',
|
||||
horizontal: 'theme.layout.layoutMode.horizontal',
|
||||
'top-hybrid-sidebar-first': 'theme.layout.layoutMode.top-hybrid-sidebar-first',
|
||||
'top-hybrid-header-first': 'theme.layout.layoutMode.top-hybrid-header-first'
|
||||
vertical: 'theme.layoutMode.vertical',
|
||||
'vertical-mix': 'theme.layoutMode.vertical-mix',
|
||||
horizontal: 'theme.layoutMode.horizontal',
|
||||
'horizontal-mix': 'theme.layoutMode.horizontal-mix'
|
||||
};
|
||||
|
||||
export const themeLayoutModeOptions = transformRecordToOption(themeLayoutModeRecord);
|
||||
|
||||
export const themeScrollModeRecord: Record<UnionKey.ThemeScrollMode, App.I18n.I18nKey> = {
|
||||
wrapper: 'theme.layout.content.scrollMode.wrapper',
|
||||
content: 'theme.layout.content.scrollMode.content'
|
||||
wrapper: 'theme.scrollMode.wrapper',
|
||||
content: 'theme.scrollMode.content'
|
||||
};
|
||||
|
||||
export const themeScrollModeOptions = transformRecordToOption(themeScrollModeRecord);
|
||||
|
||||
export const themeTabModeRecord: Record<UnionKey.ThemeTabMode, App.I18n.I18nKey> = {
|
||||
chrome: 'theme.layout.tab.mode.chrome',
|
||||
button: 'theme.layout.tab.mode.button'
|
||||
chrome: 'theme.tab.mode.chrome',
|
||||
button: 'theme.tab.mode.button'
|
||||
};
|
||||
|
||||
export const themeTabModeOptions = transformRecordToOption(themeTabModeRecord);
|
||||
|
||||
export const themePageAnimationModeRecord: Record<UnionKey.ThemePageAnimateMode, App.I18n.I18nKey> = {
|
||||
'fade-slide': 'theme.layout.content.page.mode.fade-slide',
|
||||
fade: 'theme.layout.content.page.mode.fade',
|
||||
'fade-bottom': 'theme.layout.content.page.mode.fade-bottom',
|
||||
'fade-scale': 'theme.layout.content.page.mode.fade-scale',
|
||||
'zoom-fade': 'theme.layout.content.page.mode.zoom-fade',
|
||||
'zoom-out': 'theme.layout.content.page.mode.zoom-out',
|
||||
none: 'theme.layout.content.page.mode.none'
|
||||
'fade-slide': 'theme.page.mode.fade-slide',
|
||||
fade: 'theme.page.mode.fade',
|
||||
'fade-bottom': 'theme.page.mode.fade-bottom',
|
||||
'fade-scale': 'theme.page.mode.fade-scale',
|
||||
'zoom-fade': 'theme.page.mode.zoom-fade',
|
||||
'zoom-out': 'theme.page.mode.zoom-out',
|
||||
none: 'theme.page.mode.none'
|
||||
};
|
||||
|
||||
export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord);
|
||||
|
||||
export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I18n.I18nKey> = {
|
||||
refresh: 'theme.layout.resetCacheStrategy.refresh',
|
||||
close: 'theme.layout.resetCacheStrategy.close'
|
||||
close: 'theme.resetCacheStrategy.close',
|
||||
refresh: 'theme.resetCacheStrategy.refresh'
|
||||
};
|
||||
|
||||
export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord);
|
||||
|
||||
export const DARK_CLASS = 'dark';
|
||||
|
||||
export const watermarkTimeFormatOptions = [
|
||||
{ label: 'YYYY-MM-DD HH:mm', value: 'YYYY-MM-DD HH:mm' },
|
||||
{ label: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss' },
|
||||
{ label: 'YYYY/MM/DD HH:mm', value: 'YYYY/MM/DD HH:mm' },
|
||||
{ label: 'YYYY/MM/DD HH:mm:ss', value: 'YYYY/MM/DD HH:mm:ss' },
|
||||
{ label: 'HH:mm', value: 'HH:mm' },
|
||||
{ label: 'HH:mm:ss', value: 'HH:mm:ss' },
|
||||
{ label: 'MM-DD HH:mm', value: 'MM-DD HH:mm' }
|
||||
];
|
||||
|
||||
export const themeTableSizeRecord: Record<UnionKey.ThemeTableSize, App.I18n.I18nKey> = {
|
||||
small: 'theme.table.size.small',
|
||||
medium: 'theme.table.size.medium',
|
||||
|
@ -4,6 +4,5 @@ export enum SetupStoreId {
|
||||
Auth = 'auth-store',
|
||||
Route = 'route-store',
|
||||
Tab = 'tab-store',
|
||||
Notice = 'notice-store',
|
||||
Dict = 'dict-store'
|
||||
Notice = 'notice-store'
|
||||
}
|
||||
|
@ -113,10 +113,6 @@ export function useDownload() {
|
||||
|
||||
const response = await fetch(fullUrl, requestOptions);
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(errorCodeRecord.default);
|
||||
}
|
||||
|
||||
await handleResponse(response);
|
||||
|
||||
const rawHeader = response.headers.get('Download-Filename');
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { computed, effectScope, nextTick, onScopeDispose, shallowRef, watch } from 'vue';
|
||||
import { computed, effectScope, nextTick, onScopeDispose, ref, watch } from 'vue';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts';
|
||||
@ -86,11 +86,11 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
||||
const themeStore = useThemeStore();
|
||||
const darkMode = computed(() => themeStore.darkMode);
|
||||
|
||||
const domRef = shallowRef<HTMLElement | null>(null);
|
||||
const domRef = ref<HTMLElement | null>(null);
|
||||
const initialSize = { width: 0, height: 0 };
|
||||
const { width, height } = useElementSize(domRef, initialSize);
|
||||
|
||||
const chart = shallowRef<echarts.ECharts | null>(null);
|
||||
let chart: echarts.ECharts | null = null;
|
||||
const chartOptions: T = optionsFactory();
|
||||
|
||||
const {
|
||||
@ -111,9 +111,18 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
||||
onDestroy
|
||||
} = hooks;
|
||||
|
||||
/**
|
||||
* whether can render chart
|
||||
*
|
||||
* when domRef is ready and initialSize is valid
|
||||
*/
|
||||
function canRender() {
|
||||
return domRef.value && initialSize.width > 0 && initialSize.height > 0;
|
||||
}
|
||||
|
||||
/** is chart rendered */
|
||||
function isRendered() {
|
||||
return Boolean(domRef.value && chart.value);
|
||||
return Boolean(domRef.value && chart);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -122,59 +131,59 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
||||
* @param callback callback function
|
||||
*/
|
||||
async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
|
||||
if (!isRendered()) return;
|
||||
|
||||
const updatedOpts = callback(chartOptions, optionsFactory);
|
||||
|
||||
Object.assign(chartOptions, updatedOpts);
|
||||
|
||||
await nextTick();
|
||||
|
||||
if (!isRendered()) return;
|
||||
|
||||
if (isRendered()) {
|
||||
chart.value?.clear();
|
||||
chart?.clear();
|
||||
}
|
||||
|
||||
chart.value?.setOption({ ...updatedOpts, backgroundColor: 'transparent' });
|
||||
chart?.setOption({ ...updatedOpts, backgroundColor: 'transparent' });
|
||||
|
||||
await onUpdated?.(chart.value!);
|
||||
await onUpdated?.(chart!);
|
||||
}
|
||||
|
||||
function setOptions(options: T) {
|
||||
chart.value?.setOption(options);
|
||||
chart?.setOption(options);
|
||||
}
|
||||
|
||||
/** render chart */
|
||||
async function render() {
|
||||
if (isRendered()) return;
|
||||
if (!isRendered()) {
|
||||
const chartTheme = darkMode.value ? 'dark' : 'light';
|
||||
|
||||
const chartTheme = darkMode.value ? 'dark' : 'light';
|
||||
await nextTick();
|
||||
|
||||
chart.value = echarts.init(domRef.value, chartTheme);
|
||||
chart = echarts.init(domRef.value, chartTheme);
|
||||
|
||||
chart.value?.setOption({ ...chartOptions, backgroundColor: 'transparent' });
|
||||
chart.setOption({ ...chartOptions, backgroundColor: 'transparent' });
|
||||
|
||||
await onRender?.(chart.value!);
|
||||
await onRender?.(chart);
|
||||
}
|
||||
}
|
||||
|
||||
/** resize chart */
|
||||
function resize() {
|
||||
chart.value?.resize();
|
||||
chart?.resize();
|
||||
}
|
||||
|
||||
/** destroy chart */
|
||||
async function destroy() {
|
||||
if (!chart.value) return;
|
||||
if (!chart) return;
|
||||
|
||||
await onDestroy?.(chart.value);
|
||||
chart.value?.dispose();
|
||||
chart.value = null;
|
||||
await onDestroy?.(chart);
|
||||
chart?.dispose();
|
||||
chart = null;
|
||||
}
|
||||
|
||||
/** change chart theme */
|
||||
async function changeTheme() {
|
||||
await destroy();
|
||||
await render();
|
||||
await onUpdated?.(chart.value!);
|
||||
await onUpdated?.(chart!);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -187,29 +196,30 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
||||
initialSize.width = w;
|
||||
initialSize.height = h;
|
||||
|
||||
// size is abnormal, destroy chart
|
||||
if (!canRender()) {
|
||||
await destroy();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// resize chart
|
||||
if (isRendered()) {
|
||||
resize();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// render chart
|
||||
await render();
|
||||
|
||||
if (chart.value) {
|
||||
await onUpdated?.(chart.value);
|
||||
if (chart) {
|
||||
await onUpdated?.(chart);
|
||||
}
|
||||
}
|
||||
|
||||
scope.run(() => {
|
||||
watch(
|
||||
[width, height],
|
||||
([newWidth, newHeight]) => {
|
||||
renderChartBySize(newWidth, newHeight);
|
||||
},
|
||||
{ flush: 'post' }
|
||||
);
|
||||
watch([width, height], ([newWidth, newHeight]) => {
|
||||
renderChartBySize(newWidth, newHeight);
|
||||
});
|
||||
|
||||
watch(darkMode, () => {
|
||||
changeTheme();
|
||||
@ -223,7 +233,6 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
||||
|
||||
return {
|
||||
domRef,
|
||||
chart,
|
||||
updateOptions,
|
||||
setOptions
|
||||
};
|
||||
|
@ -1,312 +1,195 @@
|
||||
import { computed, effectScope, onScopeDispose, reactive, ref, shallowRef, watch } from 'vue';
|
||||
import { computed, effectScope, onScopeDispose, reactive, ref, watch } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import type { PaginationProps } from 'naive-ui';
|
||||
import { useBoolean, useTable } from '@sa/hooks';
|
||||
import type { PaginationData, TableColumnCheck, UseTableOptions } from '@sa/hooks';
|
||||
import type { FlatResponseData } from '@sa/axios';
|
||||
import { jsonClone } from '@sa/utils';
|
||||
import { useBoolean, useHookTable } from '@sa/hooks';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
export type UseNaiveTableOptions<ResponseData, ApiData, Pagination extends boolean> = Omit<
|
||||
UseTableOptions<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, Pagination>,
|
||||
'pagination' | 'getColumnChecks' | 'getColumns'
|
||||
> & {
|
||||
/**
|
||||
* get column visible
|
||||
*
|
||||
* @param column
|
||||
*
|
||||
* @default true
|
||||
*
|
||||
* @returns true if the column is visible, false otherwise
|
||||
*/
|
||||
getColumnVisible?: (column: NaiveUI.TableColumn<ApiData>) => boolean;
|
||||
};
|
||||
type TableData = NaiveUI.TableData;
|
||||
type GetTableData<A extends NaiveUI.TableApiFn> = NaiveUI.GetTableData<A>;
|
||||
type TableColumn<T> = NaiveUI.TableColumn<T>;
|
||||
|
||||
const SELECTION_KEY = '__selection__';
|
||||
|
||||
const EXPAND_KEY = '__expand__';
|
||||
|
||||
export function useNaiveTable<ResponseData, ApiData>(options: UseNaiveTableOptions<ResponseData, ApiData, false>) {
|
||||
const scope = effectScope();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, false>({
|
||||
...options,
|
||||
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
|
||||
getColumns
|
||||
});
|
||||
|
||||
// calculate the total width of the table this is used for horizontal scrolling
|
||||
const scrollX = computed(() => {
|
||||
return result.columns.value.reduce((acc, column) => {
|
||||
return acc + Number(column.width ?? column.minWidth ?? 120);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
scope.run(() => {
|
||||
watch(
|
||||
() => appStore.locale,
|
||||
() => {
|
||||
result.reloadColumns();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
onScopeDispose(() => {
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
scrollX
|
||||
};
|
||||
}
|
||||
|
||||
type PaginationParams = Pick<PaginationProps, 'page' | 'pageSize'>;
|
||||
|
||||
type UseNaivePaginatedTableOptions<ResponseData, ApiData> = UseNaiveTableOptions<ResponseData, ApiData, true> & {
|
||||
paginationProps?: Omit<PaginationProps, 'page' | 'pageSize' | 'itemCount'>;
|
||||
/**
|
||||
* whether to show the total count of the table
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
showTotal?: boolean;
|
||||
onPaginationParamsChange?: (params: PaginationParams) => void | Promise<void>;
|
||||
};
|
||||
|
||||
export function useNaivePaginatedTable<ResponseData, ApiData>(
|
||||
options: UseNaivePaginatedTableOptions<ResponseData, ApiData>
|
||||
) {
|
||||
export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTableConfig<A>) {
|
||||
const scope = effectScope();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const isMobile = computed(() => appStore.isMobile);
|
||||
|
||||
const showTotal = computed(() => options.showTotal ?? true);
|
||||
const { apiFn, apiParams, immediate, showTotal = true } = config;
|
||||
|
||||
const pagination = reactive({
|
||||
const SELECTION_KEY = '__selection__';
|
||||
|
||||
const EXPAND_KEY = '__expand__';
|
||||
|
||||
const {
|
||||
loading,
|
||||
empty,
|
||||
data,
|
||||
columns,
|
||||
columnChecks,
|
||||
reloadColumns,
|
||||
getData,
|
||||
searchParams,
|
||||
updateSearchParams,
|
||||
resetSearchParams
|
||||
} = useHookTable<A, GetTableData<A>, TableColumn<NaiveUI.TableDataWithIndex<GetTableData<A>>>>({
|
||||
apiFn,
|
||||
apiParams,
|
||||
columns: config.columns,
|
||||
transformer: res => {
|
||||
const { rows: records = [], total = 0 } = res.data || {};
|
||||
|
||||
const current = searchParams.pageNum as number;
|
||||
const size = (searchParams.pageSize || 0) as number;
|
||||
|
||||
// Ensure that the size is greater than 0, If it is less than 0, it will cause paging calculation errors.
|
||||
const pageSize = size <= 0 ? 10 : size;
|
||||
|
||||
const recordsWithIndex = records.map((item, index) => {
|
||||
return {
|
||||
...item,
|
||||
index: (current - 1) * pageSize + index + 1
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
data: recordsWithIndex,
|
||||
pageNum: current,
|
||||
pageSize,
|
||||
total
|
||||
};
|
||||
},
|
||||
getColumnChecks: cols => {
|
||||
const checks: NaiveUI.TableColumnCheck[] = [];
|
||||
|
||||
cols.forEach(column => {
|
||||
if (isTableColumnHasKey(column)) {
|
||||
checks.push({
|
||||
key: column.key as string,
|
||||
title: column.title!,
|
||||
checked: true
|
||||
});
|
||||
} else if (column.type === 'selection') {
|
||||
checks.push({
|
||||
key: SELECTION_KEY,
|
||||
title: $t('common.check'),
|
||||
checked: true
|
||||
});
|
||||
} else if (column.type === 'expand') {
|
||||
checks.push({
|
||||
key: EXPAND_KEY,
|
||||
title: $t('common.expandColumn'),
|
||||
checked: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return checks;
|
||||
},
|
||||
getColumns: (cols, checks) => {
|
||||
const columnMap = new Map<string, TableColumn<GetTableData<A>>>();
|
||||
|
||||
cols.forEach(column => {
|
||||
if (isTableColumnHasKey(column)) {
|
||||
columnMap.set(column.key as string, column);
|
||||
} else if (column.type === 'selection') {
|
||||
columnMap.set(SELECTION_KEY, column);
|
||||
} else if (column.type === 'expand') {
|
||||
columnMap.set(EXPAND_KEY, column);
|
||||
}
|
||||
});
|
||||
|
||||
const filteredColumns = checks
|
||||
.filter(item => item.checked)
|
||||
.map(check => columnMap.get(check.key) as TableColumn<GetTableData<A>>);
|
||||
|
||||
return filteredColumns;
|
||||
},
|
||||
onFetched: async transformed => {
|
||||
const { total } = transformed;
|
||||
|
||||
updatePagination({
|
||||
page: searchParams.pageNum,
|
||||
pageSize: searchParams.pageSize,
|
||||
itemCount: total
|
||||
});
|
||||
},
|
||||
immediate
|
||||
});
|
||||
|
||||
const pagination: PaginationProps = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
showSizePicker: true,
|
||||
itemCount: 0,
|
||||
pageSizes: [10, 15, 20, 25, 30],
|
||||
prefix: showTotal.value ? page => $t('datatable.itemCount', { total: page.itemCount }) : undefined,
|
||||
onUpdatePage(page) {
|
||||
onUpdatePage: async (page: number) => {
|
||||
pagination.page = page;
|
||||
|
||||
updateSearchParams({
|
||||
pageNum: page,
|
||||
pageSize: pagination.pageSize!
|
||||
});
|
||||
|
||||
getData();
|
||||
},
|
||||
onUpdatePageSize(pageSize) {
|
||||
onUpdatePageSize: async (pageSize: number) => {
|
||||
pagination.pageSize = pageSize;
|
||||
pagination.page = 1;
|
||||
|
||||
updateSearchParams({
|
||||
pageNum: pagination.page,
|
||||
pageSize
|
||||
});
|
||||
|
||||
getData();
|
||||
},
|
||||
...options.paginationProps
|
||||
}) as PaginationProps;
|
||||
...(showTotal
|
||||
? {
|
||||
prefix: page => $t('datatable.itemCount', { total: page.itemCount })
|
||||
}
|
||||
: {})
|
||||
});
|
||||
|
||||
// this is for mobile, if the system does not support mobile, you can use `pagination` directly
|
||||
const mobilePagination = computed(() => {
|
||||
const p: PaginationProps = {
|
||||
...pagination,
|
||||
pageSlot: isMobile.value ? 3 : 9,
|
||||
prefix: !isMobile.value && showTotal.value ? pagination.prefix : undefined
|
||||
prefix: !isMobile.value && showTotal ? pagination.prefix : undefined
|
||||
};
|
||||
|
||||
return p;
|
||||
});
|
||||
|
||||
const paginationParams = computed(() => {
|
||||
const { page, pageSize } = pagination;
|
||||
|
||||
return {
|
||||
page,
|
||||
pageSize
|
||||
};
|
||||
});
|
||||
|
||||
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, true>({
|
||||
...options,
|
||||
pagination: true,
|
||||
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
|
||||
getColumns,
|
||||
onFetched: data => {
|
||||
pagination.itemCount = data.total;
|
||||
}
|
||||
});
|
||||
|
||||
async function getDataByPage(page: number = 1) {
|
||||
if (page !== pagination.page) {
|
||||
pagination.page = page;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await result.getData();
|
||||
function updatePagination(update: Partial<PaginationProps>) {
|
||||
Object.assign(pagination, update);
|
||||
}
|
||||
|
||||
scope.run(() => {
|
||||
watch(
|
||||
() => appStore.locale,
|
||||
() => {
|
||||
result.reloadColumns();
|
||||
}
|
||||
);
|
||||
|
||||
watch(paginationParams, async newVal => {
|
||||
await options.onPaginationParamsChange?.(newVal);
|
||||
|
||||
await result.getData();
|
||||
/**
|
||||
* get data by page number
|
||||
*
|
||||
* @param pageNum the page number. default is 1
|
||||
*/
|
||||
async function getDataByPage(pageNum: number = 1) {
|
||||
updatePagination({
|
||||
page: pageNum
|
||||
});
|
||||
});
|
||||
|
||||
onScopeDispose(() => {
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
getDataByPage,
|
||||
pagination,
|
||||
mobilePagination
|
||||
};
|
||||
}
|
||||
|
||||
export function useTableOperate<TableData>(
|
||||
data: Ref<TableData[]>,
|
||||
idKey: keyof TableData,
|
||||
getData: () => Promise<void>
|
||||
) {
|
||||
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
|
||||
|
||||
const operateType = shallowRef<NaiveUI.TableOperateType>('add');
|
||||
|
||||
function handleAdd() {
|
||||
operateType.value = 'add';
|
||||
openDrawer();
|
||||
}
|
||||
|
||||
/** the editing row data */
|
||||
const editingData = shallowRef<TableData | null>(null);
|
||||
|
||||
function handleEdit(id: TableData[keyof TableData]) {
|
||||
operateType.value = 'edit';
|
||||
const findItem = data.value.find(item => item[idKey] === id) || null;
|
||||
editingData.value = jsonClone(findItem);
|
||||
|
||||
openDrawer();
|
||||
}
|
||||
|
||||
/** the checked row keys of table */
|
||||
const checkedRowKeys = shallowRef<string[]>([]);
|
||||
|
||||
/** the hook after the batch delete operation is completed */
|
||||
async function onBatchDeleted() {
|
||||
window.$message?.success($t('common.deleteSuccess'));
|
||||
|
||||
checkedRowKeys.value = [];
|
||||
updateSearchParams({
|
||||
pageNum,
|
||||
pageSize: pagination.pageSize!
|
||||
});
|
||||
|
||||
await getData();
|
||||
}
|
||||
|
||||
/** the hook after the delete operation is completed */
|
||||
async function onDeleted() {
|
||||
window.$message?.success($t('common.deleteSuccess'));
|
||||
|
||||
await getData();
|
||||
}
|
||||
|
||||
return {
|
||||
drawerVisible,
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
operateType,
|
||||
handleAdd,
|
||||
editingData,
|
||||
handleEdit,
|
||||
checkedRowKeys,
|
||||
onBatchDeleted,
|
||||
onDeleted
|
||||
};
|
||||
}
|
||||
|
||||
export function defaultTransform<ApiData>(
|
||||
response: FlatResponseData<any, Api.Common.PaginatingQueryRecord<ApiData>>
|
||||
): PaginationData<ApiData> {
|
||||
const { data, error } = response;
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
data: [],
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
};
|
||||
}
|
||||
|
||||
const { rows: records, pageSize: current, pageNum: size, total } = data;
|
||||
|
||||
return {
|
||||
data: records,
|
||||
pageNum: current,
|
||||
pageSize: size,
|
||||
total
|
||||
};
|
||||
}
|
||||
|
||||
type UseNaiveTreeTableOptions<ResponseData, ApiData> = UseNaiveTableOptions<ResponseData, ApiData, false> & {
|
||||
keyField: keyof ApiData;
|
||||
defaultExpandAll?: boolean;
|
||||
};
|
||||
|
||||
export function useNaiveTreeTable<ResponseData, ApiData>(options: UseNaiveTreeTableOptions<ResponseData, ApiData>) {
|
||||
const scope = effectScope();
|
||||
const appStore = useAppStore();
|
||||
const rows: Ref<ApiData[]> = ref([]);
|
||||
|
||||
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, false>({
|
||||
...options,
|
||||
pagination: false,
|
||||
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
|
||||
getColumns,
|
||||
onFetched: transformData => {
|
||||
const data: ApiData[] = [];
|
||||
|
||||
const collect = (nodes: any[]) => {
|
||||
nodes.forEach(node => {
|
||||
data.push(node);
|
||||
if (node?.children?.length) {
|
||||
collect(node.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
collect(transformData);
|
||||
rows.value = data;
|
||||
}
|
||||
});
|
||||
|
||||
const { keyField = 'id', defaultExpandAll = false } = options;
|
||||
|
||||
const expandedRowKeys = ref<ApiData[keyof ApiData][]>([]);
|
||||
const { bool: isCollapse, toggle: toggleCollapse } = useBoolean(defaultExpandAll);
|
||||
|
||||
/** expand all nodes */
|
||||
function expandAll() {
|
||||
toggleCollapse();
|
||||
expandedRowKeys.value = rows.value.map(item => item[keyField as keyof ApiData]);
|
||||
}
|
||||
|
||||
/** collapse all nodes */
|
||||
function collapseAll() {
|
||||
toggleCollapse();
|
||||
expandedRowKeys.value = [];
|
||||
}
|
||||
|
||||
scope.run(() => {
|
||||
watch(
|
||||
() => appStore.locale,
|
||||
() => {
|
||||
result.reloadColumns();
|
||||
reloadColumns();
|
||||
}
|
||||
);
|
||||
});
|
||||
@ -316,19 +199,27 @@ export function useNaiveTreeTable<ResponseData, ApiData>(options: UseNaiveTreeTa
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
rows,
|
||||
isCollapse,
|
||||
expandedRowKeys,
|
||||
expandAll,
|
||||
collapseAll
|
||||
loading,
|
||||
empty,
|
||||
data,
|
||||
columns,
|
||||
columnChecks,
|
||||
reloadColumns,
|
||||
pagination,
|
||||
mobilePagination,
|
||||
updatePagination,
|
||||
getData,
|
||||
getDataByPage,
|
||||
searchParams,
|
||||
updateSearchParams,
|
||||
resetSearchParams
|
||||
};
|
||||
}
|
||||
|
||||
export function useTreeTableOperate<ApiData>(data: Ref<ApiData[]>, idKey: keyof ApiData, getData: () => Promise<void>) {
|
||||
export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>, getData: () => Promise<void>) {
|
||||
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
|
||||
|
||||
const operateType = shallowRef<NaiveUI.TableOperateType>('add');
|
||||
const operateType = ref<NaiveUI.TableOperateType>('add');
|
||||
|
||||
function handleAdd() {
|
||||
operateType.value = 'add';
|
||||
@ -336,18 +227,18 @@ export function useTreeTableOperate<ApiData>(data: Ref<ApiData[]>, idKey: keyof
|
||||
}
|
||||
|
||||
/** the editing row data */
|
||||
const editingData = shallowRef<ApiData | null>(null);
|
||||
const editingData: Ref<T | null> = ref(null);
|
||||
|
||||
function handleEdit(id: ApiData[keyof ApiData]) {
|
||||
function handleEdit(field: keyof T, id: CommonType.IdType) {
|
||||
operateType.value = 'edit';
|
||||
const findItem = data.value.find(item => item[idKey] === id) || null;
|
||||
const findItem = data.value.find(item => item[field] === id) || null;
|
||||
editingData.value = jsonClone(findItem);
|
||||
|
||||
openDrawer();
|
||||
}
|
||||
|
||||
/** the checked row keys of table */
|
||||
const checkedRowKeys = shallowRef<string[]>([]);
|
||||
const checkedRowKeys = ref<CommonType.IdType[]>([]);
|
||||
|
||||
/** the hook after the batch delete operation is completed */
|
||||
async function onBatchDeleted() {
|
||||
@ -379,135 +270,6 @@ export function useTreeTableOperate<ApiData>(data: Ref<ApiData[]>, idKey: keyof
|
||||
};
|
||||
}
|
||||
|
||||
type TreeTableOptions<ApiData> = {
|
||||
/** id field name */
|
||||
idField?: keyof ApiData;
|
||||
/** parent id field name */
|
||||
parentIdField?: keyof ApiData;
|
||||
/** children field name */
|
||||
childrenField?: keyof ApiData;
|
||||
/** filter function */
|
||||
filterFn?: (node: ApiData) => boolean;
|
||||
};
|
||||
|
||||
export function treeTransform<ApiData>(
|
||||
response: FlatResponseData<any, ApiData[]>,
|
||||
options: TreeTableOptions<ApiData>
|
||||
): ApiData[] {
|
||||
const { data, error } = response;
|
||||
|
||||
if (error || !data.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { idField = 'id', parentIdField = 'parentId', childrenField = 'children', filterFn = () => true } = options;
|
||||
|
||||
// 使用 Map 替代普通对象,提高性能
|
||||
const childrenMap = new Map<ApiData[keyof ApiData], ApiData[]>();
|
||||
const nodeMap = new Map<ApiData[keyof ApiData], ApiData>();
|
||||
const tree: ApiData[] = [];
|
||||
|
||||
// 第一遍遍历:构建节点映射
|
||||
for (const item of data) {
|
||||
const id = item[idField as keyof ApiData];
|
||||
const parentId = item[parentIdField as keyof ApiData];
|
||||
|
||||
nodeMap.set(id, item);
|
||||
|
||||
if (!childrenMap.has(parentId)) {
|
||||
childrenMap.set(parentId, []);
|
||||
}
|
||||
// 应用过滤函数
|
||||
if (filterFn(item)) {
|
||||
childrenMap.get(parentId)!.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// 第二遍遍历:找出根节点
|
||||
for (const item of data) {
|
||||
const parentId = item[parentIdField as keyof ApiData];
|
||||
if (!nodeMap.has(parentId) && filterFn(item)) {
|
||||
tree.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// 递归构建树形结构
|
||||
const buildTree = (node: ApiData) => {
|
||||
const id = node[idField as keyof ApiData];
|
||||
const children = childrenMap.get(id);
|
||||
|
||||
if (children?.length) {
|
||||
// 使用类型断言确保类型安全
|
||||
(node as any)[childrenField] = children;
|
||||
for (const child of children) {
|
||||
buildTree(child);
|
||||
}
|
||||
} else {
|
||||
// 如果没有子节点,设置为 undefined
|
||||
(node as any)[childrenField] = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// 从根节点开始构建树
|
||||
for (const root of tree) {
|
||||
buildTree(root);
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
function getColumnChecks<Column extends NaiveUI.TableColumn<any>>(
|
||||
cols: Column[],
|
||||
getColumnVisible?: (column: Column) => boolean
|
||||
) {
|
||||
const checks: TableColumnCheck[] = [];
|
||||
|
||||
cols.forEach(column => {
|
||||
if (isTableColumnHasKey(column)) {
|
||||
checks.push({
|
||||
key: column.key as string,
|
||||
title: column.title!,
|
||||
checked: true,
|
||||
visible: getColumnVisible?.(column) ?? true
|
||||
});
|
||||
} else if (column.type === 'selection') {
|
||||
checks.push({
|
||||
key: SELECTION_KEY,
|
||||
title: $t('common.check'),
|
||||
checked: true,
|
||||
visible: getColumnVisible?.(column) ?? false
|
||||
});
|
||||
} else if (column.type === 'expand') {
|
||||
checks.push({
|
||||
key: EXPAND_KEY,
|
||||
title: $t('common.expandColumn'),
|
||||
checked: true,
|
||||
visible: getColumnVisible?.(column) ?? false
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
function getColumns<Column extends NaiveUI.TableColumn<any>>(cols: Column[], checks: TableColumnCheck[]) {
|
||||
const columnMap = new Map<string, Column>();
|
||||
|
||||
cols.forEach(column => {
|
||||
if (isTableColumnHasKey(column)) {
|
||||
columnMap.set(column.key as string, column);
|
||||
} else if (column.type === 'selection') {
|
||||
columnMap.set(SELECTION_KEY, column);
|
||||
} else if (column.type === 'expand') {
|
||||
columnMap.set(EXPAND_KEY, column);
|
||||
}
|
||||
});
|
||||
|
||||
const filteredColumns = checks.filter(item => item.checked).map(check => columnMap.get(check.key) as Column);
|
||||
|
||||
return filteredColumns;
|
||||
}
|
||||
|
||||
export function isTableColumnHasKey<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
|
||||
function isTableColumnHasKey<T>(column: TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
|
||||
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
|
||||
}
|
||||
|