fdb2 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +21 -0
- package/.editorconfig +11 -0
- package/.eslintrc.cjs +14 -0
- package/.eslintrc.json +7 -0
- package/.prettierrc.js +3 -0
- package/.tpl.env +22 -0
- package/README.md +260 -0
- package/bin/build.sh +28 -0
- package/bin/deploy.sh +8 -0
- package/bin/dev.sh +10 -0
- package/bin/docker/.env +4 -0
- package/bin/docker/dev-docker-compose.yml +43 -0
- package/bin/docker/dev.Dockerfile +24 -0
- package/bin/docker/prod-docker-compose.yml +17 -0
- package/bin/docker/prod.Dockerfile +29 -0
- package/bin/fdb2.js +142 -0
- package/data/connections.demo.json +32 -0
- package/env.d.ts +1 -0
- package/nw-build.js +120 -0
- package/nw-dev.js +65 -0
- package/package.json +114 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +9 -0
- package/public/modules/header.tpl +14 -0
- package/public/modules/initial_state.tpl +55 -0
- package/server/index.ts +677 -0
- package/server/model/connection.entity.ts +66 -0
- package/server/model/database.entity.ts +246 -0
- package/server/service/connection.service.ts +334 -0
- package/server/service/database/base.service.ts +363 -0
- package/server/service/database/database.service.ts +510 -0
- package/server/service/database/index.ts +7 -0
- package/server/service/database/mssql.service.ts +723 -0
- package/server/service/database/mysql.service.ts +761 -0
- package/server/service/database/oracle.service.ts +839 -0
- package/server/service/database/postgres.service.ts +744 -0
- package/server/service/database/sqlite.service.ts +559 -0
- package/server/service/session.service.ts +158 -0
- package/server.js +128 -0
- package/src/adapter/ajax.ts +135 -0
- package/src/assets/base.css +1 -0
- package/src/assets/database.css +950 -0
- package/src/assets/images/collapse.png +0 -0
- package/src/assets/images/no-login.png +0 -0
- package/src/assets/images/svg/illustrations/illustration-1.svg +1 -0
- package/src/assets/images/svg/illustrations/illustration-2.svg +2 -0
- package/src/assets/images/svg/illustrations/illustration-3.svg +50 -0
- package/src/assets/images/svg/illustrations/illustration-4.svg +1 -0
- package/src/assets/images/svg/illustrations/illustration-5.svg +73 -0
- package/src/assets/images/svg/illustrations/illustration-6.svg +89 -0
- package/src/assets/images/svg/illustrations/illustration-7.svg +39 -0
- package/src/assets/images/svg/illustrations/illustration-8.svg +1 -0
- package/src/assets/images/svg/separators/curve-2.svg +3 -0
- package/src/assets/images/svg/separators/curve.svg +3 -0
- package/src/assets/images/svg/separators/line.svg +3 -0
- package/src/assets/images/theme/light/screen-1-1000x800.jpg +0 -0
- package/src/assets/images/theme/light/screen-2-1000x800.jpg +0 -0
- package/src/assets/login/bg.jpg +0 -0
- package/src/assets/login/bg.png +0 -0
- package/src/assets/login/left.jpg +0 -0
- package/src/assets/logo.svg +73 -0
- package/src/assets/logo.webp +0 -0
- package/src/assets/main.css +1 -0
- package/src/base/config.ts +20 -0
- package/src/base/detect.ts +134 -0
- package/src/base/entity.ts +92 -0
- package/src/base/eventBus.ts +37 -0
- package/src/base//345/237/272/347/241/200/345/261/202.md +7 -0
- package/src/components/connection-editor/index.vue +590 -0
- package/src/components/dataGrid/index.vue +105 -0
- package/src/components/dataGrid/pagination.vue +106 -0
- package/src/components/loading/index.vue +43 -0
- package/src/components/modal/index.ts +181 -0
- package/src/components/modal/index.vue +560 -0
- package/src/components/toast/index.ts +44 -0
- package/src/components/toast/toast.vue +58 -0
- package/src/components/user/name.vue +104 -0
- package/src/components/user/selector.vue +416 -0
- package/src/domain/SysConfig.ts +74 -0
- package/src/platform/App.vue +8 -0
- package/src/platform/database/components/connection-detail.vue +1154 -0
- package/src/platform/database/components/data-editor.vue +478 -0
- package/src/platform/database/components/data-import-export.vue +1602 -0
- package/src/platform/database/components/database-detail.vue +1173 -0
- package/src/platform/database/components/database-monitor.vue +1086 -0
- package/src/platform/database/components/db-tools.vue +577 -0
- package/src/platform/database/components/query-history.vue +1349 -0
- package/src/platform/database/components/sql-executor.vue +738 -0
- package/src/platform/database/components/sql-query-editor.vue +1046 -0
- package/src/platform/database/components/table-detail.vue +1376 -0
- package/src/platform/database/components/table-editor.vue +690 -0
- package/src/platform/database/explorer.vue +1840 -0
- package/src/platform/database/index.vue +1193 -0
- package/src/platform/database/layout.vue +367 -0
- package/src/platform/database/router.ts +37 -0
- package/src/platform/database/styles/common.scss +602 -0
- package/src/platform/database/types/common.ts +445 -0
- package/src/platform/database/utils/export.ts +232 -0
- package/src/platform/database/utils/helpers.ts +437 -0
- package/src/platform/index.ts +33 -0
- package/src/platform/router.ts +41 -0
- package/src/service/base.ts +128 -0
- package/src/service/database.ts +500 -0
- package/src/service/login.ts +121 -0
- package/src/shims-vue.d.ts +7 -0
- package/src/stores/connection.ts +266 -0
- package/src/stores/session.ts +87 -0
- package/src/typings/database-types.ts +413 -0
- package/src/typings/database.ts +364 -0
- package/src/typings/global.d.ts +58 -0
- package/src/typings/pinia.d.ts +8 -0
- package/src/utils/clipboard.ts +30 -0
- package/src/utils/database-types.ts +243 -0
- package/src/utils/modal.ts +124 -0
- package/src/utils/request.ts +55 -0
- package/src/utils/sleep.ts +4 -0
- package/src/utils/toast.ts +73 -0
- package/src/utils/util.ts +171 -0
- package/src/utils/xlsx.ts +228 -0
- package/tsconfig.json +33 -0
- package/tsconfig.server.json +19 -0
- package/view/index.html +9 -0
- package/view/modules/header.tpl +14 -0
- package/view/modules/initial_state.tpl +20 -0
- package/vite.config.ts +384 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { getCurrentInstance } from 'vue';
|
|
2
|
+
import { getModalInstance, showAlert, showConfirm, type ModalTypeWithMethods } from '@/components/modal';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 全局Modal工具类
|
|
6
|
+
* 提供统一的弹窗接口
|
|
7
|
+
*/
|
|
8
|
+
export class ModalHelper {
|
|
9
|
+
/**
|
|
10
|
+
* 获取全局modal实例
|
|
11
|
+
*/
|
|
12
|
+
private getModal(): ModalTypeWithMethods|null {
|
|
13
|
+
const instance = getModalInstance();
|
|
14
|
+
if (!instance) {
|
|
15
|
+
console.warn('ModalHelper: Vue实例不存在,使用默认实现');
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
return instance;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 成功提示
|
|
25
|
+
*/
|
|
26
|
+
success(content: string): Promise<boolean> {
|
|
27
|
+
const modal = this.getModal();
|
|
28
|
+
if (modal?.success) {
|
|
29
|
+
return modal.success(content);
|
|
30
|
+
}
|
|
31
|
+
return showAlert(content, 'success') || Promise.resolve(true);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 错误提示
|
|
36
|
+
*/
|
|
37
|
+
error(content: string | Error, detail?: any): Promise<boolean> {
|
|
38
|
+
const modal = this.getModal();
|
|
39
|
+
|
|
40
|
+
// 处理错误对象
|
|
41
|
+
let errorMessage: string;
|
|
42
|
+
if (content instanceof Error) {
|
|
43
|
+
errorMessage = content.message;
|
|
44
|
+
console.error('Error details:', {
|
|
45
|
+
name: content.name || detail.name,
|
|
46
|
+
message: content.message || detail.message,
|
|
47
|
+
stack: content.stack || detail.stack,
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
errorMessage = String(content);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (modal?.error) {
|
|
54
|
+
return modal.error(errorMessage, detail);
|
|
55
|
+
}
|
|
56
|
+
return showAlert(errorMessage, 'error', detail) || Promise.resolve(true);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 警告提示
|
|
63
|
+
*/
|
|
64
|
+
warning(content: string): Promise<boolean> {
|
|
65
|
+
const modal = this.getModal();
|
|
66
|
+
if (modal?.warning) {
|
|
67
|
+
return modal.warning(content);
|
|
68
|
+
}
|
|
69
|
+
return showAlert(content, 'warning') || Promise.resolve(true);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 信息提示
|
|
74
|
+
*/
|
|
75
|
+
info(content: string): Promise<boolean> {
|
|
76
|
+
const modal = this.getModal();
|
|
77
|
+
if (modal?.info) {
|
|
78
|
+
return modal.info(content);
|
|
79
|
+
}
|
|
80
|
+
return showAlert(content, 'info') || Promise.resolve(true);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 简单提示(替代alert)
|
|
85
|
+
*/
|
|
86
|
+
alert(content: string): Promise<boolean> {
|
|
87
|
+
const modal = this.getModal();
|
|
88
|
+
if (modal?.alert) {
|
|
89
|
+
return modal.alert(content);
|
|
90
|
+
}
|
|
91
|
+
return showAlert(content, 'info') || Promise.resolve(true);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 确认对话框(替代confirm)
|
|
96
|
+
*/
|
|
97
|
+
confirm(content: string, options?: {
|
|
98
|
+
title?: string;
|
|
99
|
+
onConfirm?: () => void;
|
|
100
|
+
onCancel?: () => void;
|
|
101
|
+
}): Promise<boolean> {
|
|
102
|
+
const modal = this.getModal();
|
|
103
|
+
if (modal?.confirm) {
|
|
104
|
+
return modal.confirm({
|
|
105
|
+
...options,
|
|
106
|
+
content,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return showConfirm(content, options) || Promise.resolve(false);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 导出单例实例
|
|
114
|
+
export const modal = new ModalHelper();
|
|
115
|
+
|
|
116
|
+
// 兼容性导出
|
|
117
|
+
export const showToast = modal.info.bind(modal);
|
|
118
|
+
export const showError = modal.error.bind(modal);
|
|
119
|
+
export const showSuccess = modal.success.bind(modal);
|
|
120
|
+
export const showWarning = modal.warning.bind(modal);
|
|
121
|
+
|
|
122
|
+
// 替代原生函数
|
|
123
|
+
export const alert = modal.alert.bind(modal);
|
|
124
|
+
export const confirm = modal.confirm.bind(modal);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
import { useSessionStore } from '@/stores/session';
|
|
3
|
+
import Axios from 'axios';
|
|
4
|
+
import type { AxiosRequestConfig } from 'axios';
|
|
5
|
+
|
|
6
|
+
const axios = Axios.create();
|
|
7
|
+
|
|
8
|
+
export async function request(url: string, data?: any, option?: AxiosRequestConfig) {
|
|
9
|
+
|
|
10
|
+
const session = useSessionStore();
|
|
11
|
+
option = {
|
|
12
|
+
...(option || {})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
option = {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
withCredentials: true,
|
|
18
|
+
...option,
|
|
19
|
+
headers: {
|
|
20
|
+
...(option.headers || {}),
|
|
21
|
+
'x-auth-token': (session.id || ''),
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
let res;
|
|
25
|
+
if(option.method === 'GET' || option.method === 'get') {
|
|
26
|
+
res = await axios.get(url, {
|
|
27
|
+
...option,
|
|
28
|
+
params: {
|
|
29
|
+
...(option.params||{}),
|
|
30
|
+
...data||{}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
res = await axios.post(url, data, option);
|
|
36
|
+
}
|
|
37
|
+
return res;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function post(url: string, data?: any, option?: AxiosRequestConfig) {
|
|
41
|
+
return request(url, data, {
|
|
42
|
+
method: 'post',
|
|
43
|
+
...option,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
export async function get(url: string, data?: any, option?: AxiosRequestConfig) {
|
|
50
|
+
return request(url, data, {
|
|
51
|
+
method: 'get',
|
|
52
|
+
...option,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { getCurrentInstance } from 'vue';
|
|
2
|
+
|
|
3
|
+
export type ToastType = {
|
|
4
|
+
show: (title: string, message: string, type: string, duration?: number) => void,
|
|
5
|
+
success: (msg: string, title?: string, duration?: number) => void,
|
|
6
|
+
error: (msg: string, title?: string, duration?: number) => void,
|
|
7
|
+
warning: (msg: string, title?: string, duration?: number) => void,
|
|
8
|
+
info: (msg: string, title?: string, duration?: number) => void
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
let globalToast: ToastType | null = null;
|
|
12
|
+
|
|
13
|
+
export function setGlobalToast(toast: ToastType) {
|
|
14
|
+
globalToast = toast;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getGlobalToast(): ToastType | null {
|
|
18
|
+
return globalToast;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class ToastHelper {
|
|
22
|
+
private getToast(): ToastType | null {
|
|
23
|
+
if (globalToast) {
|
|
24
|
+
return globalToast;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const instance = getCurrentInstance();
|
|
28
|
+
if (!instance) {
|
|
29
|
+
console.warn('ToastHelper: Vue实例不存在,请确保已注册toast插件');
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const toast = instance.appContext.config?.globalProperties?.$toast as ToastType;
|
|
34
|
+
return toast || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
success(message: string, title = '', duration = 3000): void {
|
|
38
|
+
const toast = this.getToast();
|
|
39
|
+
if (toast?.success) {
|
|
40
|
+
toast.success(message, title, duration);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
error(message: string, title = '', duration = 3000): void {
|
|
45
|
+
const toast = this.getToast();
|
|
46
|
+
if (toast?.error) {
|
|
47
|
+
toast.error(message, title, duration);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
warning(message: string, title = '', duration = 3000): void {
|
|
52
|
+
const toast = this.getToast();
|
|
53
|
+
if (toast?.warning) {
|
|
54
|
+
toast.warning(message, title, duration);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
info(message: string, title = '', duration = 3000): void {
|
|
59
|
+
const toast = this.getToast();
|
|
60
|
+
if (toast?.info) {
|
|
61
|
+
toast.info(message, title, duration);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
show(title: string, message: string, type: string, duration = 3000): void {
|
|
66
|
+
const toast = this.getToast();
|
|
67
|
+
if (toast?.show) {
|
|
68
|
+
toast.show(message, title, type, duration);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const toast = new ToastHelper();
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { h, render, type Component, type ComponentObjectPropsOptions } from 'vue';
|
|
2
|
+
import { snapdom } from '@zumer/snapdom';
|
|
3
|
+
import JSZip from 'jszip';
|
|
4
|
+
|
|
5
|
+
// 渲染组件
|
|
6
|
+
export function renderComponent(component: Component, props?: ComponentObjectPropsOptions, target: HTMLElement = document.body) {
|
|
7
|
+
const vnode = h(component, props||null);
|
|
8
|
+
render(vnode, target);
|
|
9
|
+
|
|
10
|
+
return vnode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 转自符串为json,主要用于ai返回的json格式问题
|
|
14
|
+
export function parseAiJSON(str: string) {
|
|
15
|
+
str = str.trim();
|
|
16
|
+
if(str.startsWith('```json')) str = str.replace('```json', '');
|
|
17
|
+
if(str.startsWith('json')) str = str.replace('json', '');
|
|
18
|
+
if(str.endsWith('```')) str = str.substring(0, str.length - 3);
|
|
19
|
+
try {
|
|
20
|
+
const res = JSON.parse(str);
|
|
21
|
+
return res;
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
//str = str.replace(/{\s*{/g, '{').replace(/}\s*}/g, '}');
|
|
25
|
+
let jsonIndex = str.indexOf('{');
|
|
26
|
+
let jsonLast = str.lastIndexOf('}');
|
|
27
|
+
if(jsonIndex === -1) jsonIndex = 0;
|
|
28
|
+
if(jsonLast === -1) jsonLast = str.length - 1;
|
|
29
|
+
let jsonStr = str.substring(jsonIndex, jsonLast + 1);
|
|
30
|
+
jsonStr = jsonStr.replace(/\s*\/\/(.*)?\s+/g, '');// 去除注释
|
|
31
|
+
try {
|
|
32
|
+
const result = JSON.parse(jsonStr);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
catch(e) {
|
|
36
|
+
console.error(jsonStr);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 导出dom为图片
|
|
43
|
+
export const exportDomAsImage = async (dom: HTMLElement, option: {
|
|
44
|
+
type: 'jpg'|'png',
|
|
45
|
+
filename: string
|
|
46
|
+
}) => {
|
|
47
|
+
if (dom) {
|
|
48
|
+
try {
|
|
49
|
+
await snapdom.download(dom, {
|
|
50
|
+
format: option.type || 'png',
|
|
51
|
+
type: option.type || 'png',
|
|
52
|
+
filename: option.filename,
|
|
53
|
+
// 1. 解决锯齿/模糊的核心:提升分辨率
|
|
54
|
+
dpr: Math.max(window.devicePixelRatio||2, 2), // 关键!用设备物理像素比,无则默认2倍
|
|
55
|
+
scale: 2, // 额外缩放DOM渲染尺寸(1.2-2倍为宜,避免过度放大导致性能问题)
|
|
56
|
+
quality: 1, // 质量拉满(仅对jpeg/webp生效,png无需此参数但设1不影响)
|
|
57
|
+
embedFonts: true, // 嵌入字体(防止文字因字体缺失导致锯齿/错位)
|
|
58
|
+
fast: false, // 关闭"快速模式"(快速模式会跳过部分细节渲染,优先保证质量)
|
|
59
|
+
width: dom.offsetWidth,
|
|
60
|
+
height: dom.offsetHeight,
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('导出图片时出错:', error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 将Blob类型的数组打包成ZIP文件并下载
|
|
70
|
+
* @param blobs 包含Blob和文件名的数组
|
|
71
|
+
* @param zipFileName 导出的ZIP文件名
|
|
72
|
+
*/
|
|
73
|
+
export const compressBlobToZip = async (
|
|
74
|
+
blobs: Array<{ blob: Blob; fileName: string }|Blob>,
|
|
75
|
+
): Promise<Blob> => {
|
|
76
|
+
// 验证输入
|
|
77
|
+
if (!blobs || blobs.length === 0) {
|
|
78
|
+
throw new Error('没有需要导出的文件');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return new Promise<Blob>(async (resolve, reject) => {
|
|
82
|
+
// 创建JSZip实例
|
|
83
|
+
const zip = new JSZip();
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// 遍历图片数组,添加到ZIP
|
|
87
|
+
for (let [index, blob] of blobs.entries()) {
|
|
88
|
+
const data = blob instanceof Blob? {
|
|
89
|
+
fileName: `file-${index + 1}`,
|
|
90
|
+
blob,
|
|
91
|
+
}: blob;
|
|
92
|
+
|
|
93
|
+
// 确保文件名有效,添加默认扩展名
|
|
94
|
+
const fileName = data.fileName || `file-${index + 1}`;
|
|
95
|
+
const fileWithExtension = fileName.includes('.')
|
|
96
|
+
? fileName
|
|
97
|
+
: `${fileName}.png`; // 默认使用png扩展名
|
|
98
|
+
|
|
99
|
+
// 将Blob转换为ArrayBuffer
|
|
100
|
+
const arrayBuffer = await new Promise<ArrayBuffer>((resolve, reject) => {
|
|
101
|
+
const reader = new FileReader();
|
|
102
|
+
reader.onload = () => resolve(reader.result as ArrayBuffer);
|
|
103
|
+
reader.onerror = () => reject(reader.error);
|
|
104
|
+
reader.readAsArrayBuffer(data.blob);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// 添加文件到ZIP
|
|
108
|
+
zip.file(fileWithExtension, arrayBuffer);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 生成ZIP文件并下载
|
|
112
|
+
zip.generateAsync({ type: 'blob', }, (metadata) => {
|
|
113
|
+
// 可以在这里添加进度提示逻辑
|
|
114
|
+
console.log(`打包进度: ${metadata.percent.toFixed(2)}%`);
|
|
115
|
+
}).then(content => {
|
|
116
|
+
resolve(content);
|
|
117
|
+
}).catch((reason) => {
|
|
118
|
+
reject(reason)
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('打包ZIP文件失败:', error);
|
|
123
|
+
reject(error)
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const download = async (blob: Blob, fileName: string) => {
|
|
129
|
+
const url = URL.createObjectURL(blob);
|
|
130
|
+
const a = document.createElement('a');
|
|
131
|
+
a.href = url;
|
|
132
|
+
a.download = fileName;
|
|
133
|
+
document.body.appendChild(a);
|
|
134
|
+
a.click();
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
document.body.removeChild(a);
|
|
137
|
+
URL.revokeObjectURL(url);
|
|
138
|
+
}, 200);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 用form触发下载
|
|
142
|
+
export const downloadWithForm = function (url: string, params: any = {}): void {
|
|
143
|
+
// 1. 创建表单元素
|
|
144
|
+
const form = document.createElement('form');
|
|
145
|
+
|
|
146
|
+
// 2. 设置表单属性
|
|
147
|
+
form.method = 'post'; // 使用POST方法
|
|
148
|
+
form.action = url; // 接口地址
|
|
149
|
+
form.target = '_blank'; // 在新窗口打开,避免当前页面跳转
|
|
150
|
+
form.style.display = 'none'; // 隐藏表单(不影响页面)
|
|
151
|
+
|
|
152
|
+
// 3. 动态添加表单参数(将params转为隐藏输入框)
|
|
153
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
154
|
+
const input = document.createElement('input');
|
|
155
|
+
input.type = 'hidden'; // 隐藏输入框,不显示在页面
|
|
156
|
+
input.name = key; // 参数名
|
|
157
|
+
input.value = value as string; // 参数值
|
|
158
|
+
form.appendChild(input);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// 4. 将表单添加到页面
|
|
162
|
+
document.body.appendChild(form);
|
|
163
|
+
|
|
164
|
+
// 5. 提交表单(触发下载)
|
|
165
|
+
form.submit();
|
|
166
|
+
|
|
167
|
+
// 6. 清理:移除表单(可选,不影响功能)
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
document.body.removeChild(form);
|
|
170
|
+
}, 100);
|
|
171
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import * as xlsx from "xlsx";
|
|
3
|
+
import ExcelJS from 'exceljs';
|
|
4
|
+
|
|
5
|
+
// 读取excel文件内容(支持xls和xlsx)
|
|
6
|
+
export async function readExcel(file: Blob): Promise<xlsx.WorkBook> {
|
|
7
|
+
return new Promise(resolve => {
|
|
8
|
+
if (!file) {
|
|
9
|
+
resolve({} as any);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const reader = new FileReader();
|
|
13
|
+
reader.onload = (e) => {
|
|
14
|
+
if (!e.target) {
|
|
15
|
+
resolve({} as any);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const data = e.target.result;
|
|
19
|
+
const book = xlsx.read(data, {
|
|
20
|
+
type: 'binary',
|
|
21
|
+
cellDates: true,
|
|
22
|
+
cellText: false
|
|
23
|
+
});
|
|
24
|
+
resolve(book);
|
|
25
|
+
}
|
|
26
|
+
reader.readAsBinaryString(file);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 文档解成json对象(处理合并单元格)
|
|
31
|
+
export function decodeBook(book: xlsx.WorkBook) {
|
|
32
|
+
const result = {
|
|
33
|
+
sheetNames: book.SheetNames || [],
|
|
34
|
+
sheets: []
|
|
35
|
+
} as any;
|
|
36
|
+
if (!book.SheetNames || !book.SheetNames.length) return result;
|
|
37
|
+
for (const name of book.SheetNames) {
|
|
38
|
+
result.sheets.push({
|
|
39
|
+
name,
|
|
40
|
+
data: decodeSheet(book.Sheets[name] as any),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 解析单个表(修复合并单元格多行问题)
|
|
47
|
+
export function decodeSheet(sheet: xlsx.WorkSheet) {
|
|
48
|
+
const res = {
|
|
49
|
+
cols: {} as any,
|
|
50
|
+
data: [] as any,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// 获取表格范围和合并单元格信息
|
|
54
|
+
const range = xlsx.utils.decode_range(sheet['!ref'] || 'A1:A1');
|
|
55
|
+
const merges = sheet['!merges'] || [];
|
|
56
|
+
|
|
57
|
+
// 记录哪些行是合并区域的一部分(用于后续去重)
|
|
58
|
+
const mergedRows = new Set<number>();
|
|
59
|
+
|
|
60
|
+
// 1. 先解析所有单元格数据
|
|
61
|
+
for (const key in sheet) {
|
|
62
|
+
if (!key || typeof key !== 'string' || key.startsWith('!')) continue;
|
|
63
|
+
|
|
64
|
+
const { r: rowIndex, c: colIndex } = xlsx.utils.decode_cell(key);
|
|
65
|
+
const colKey = xlsx.utils.encode_col(colIndex);
|
|
66
|
+
const cellValue = sheet[key].v || '';
|
|
67
|
+
|
|
68
|
+
// 处理表头行(第0行)
|
|
69
|
+
if (rowIndex === 0) {
|
|
70
|
+
res.cols[colKey] = cellValue;
|
|
71
|
+
}
|
|
72
|
+
// 处理数据行
|
|
73
|
+
else {
|
|
74
|
+
const dataRowIndex = rowIndex - 1;
|
|
75
|
+
if (!res.data[dataRowIndex]) {
|
|
76
|
+
res.data[dataRowIndex] = {};
|
|
77
|
+
}
|
|
78
|
+
res.data[dataRowIndex][colKey] = cellValue;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2. 处理合并单元格并标记合并行
|
|
83
|
+
merges.forEach((merge: xlsx.Range) => {
|
|
84
|
+
const startRow = merge.s.r;
|
|
85
|
+
const endRow = merge.e.r;
|
|
86
|
+
const startCol = merge.s.c;
|
|
87
|
+
const endCol = merge.e.c;
|
|
88
|
+
|
|
89
|
+
// 获取合并区域左上角单元格的值作为基准值
|
|
90
|
+
const startCellKey = xlsx.utils.encode_cell({ r: startRow, c: startCol });
|
|
91
|
+
const mergedValue = sheet[startCellKey]?.v || '';
|
|
92
|
+
|
|
93
|
+
// 填充合并区域内的所有单元格
|
|
94
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
95
|
+
// 标记合并行(除了起始行外的其他行)
|
|
96
|
+
if (r > startRow && startRow > 0) { // 只标记数据行(startRow > 0)
|
|
97
|
+
mergedRows.add(r - 1); // 转换为数据行索引
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
101
|
+
const colKey = xlsx.utils.encode_col(c);
|
|
102
|
+
|
|
103
|
+
if (r === 0) {
|
|
104
|
+
res.cols[colKey] = mergedValue;
|
|
105
|
+
} else {
|
|
106
|
+
const dataRowIndex = r - 1;
|
|
107
|
+
if (!res.data[dataRowIndex]) {
|
|
108
|
+
res.data[dataRowIndex] = {};
|
|
109
|
+
}
|
|
110
|
+
res.data[dataRowIndex][colKey] = mergedValue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// 3. 移除合并产生的重复行(只保留起始行)
|
|
117
|
+
const filteredData = [] as Array<any>;
|
|
118
|
+
for (let i = 0; i < res.data.length; i++) {
|
|
119
|
+
// 只保留非合并行或合并起始行
|
|
120
|
+
if (!mergedRows.has(i)) {
|
|
121
|
+
filteredData.push(res.data[i]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
res.data = filteredData;
|
|
125
|
+
|
|
126
|
+
// 4. 补全空列,确保数据结构完整
|
|
127
|
+
res.data.forEach((row: any) => {
|
|
128
|
+
Object.keys(res.cols).forEach(colKey => {
|
|
129
|
+
if (row[colKey] === undefined) {
|
|
130
|
+
row[colKey] = '';
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return res;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 导出excel
|
|
139
|
+
/**
|
|
140
|
+
* 导出 Excel 文件(支持表头样式)
|
|
141
|
+
* @param data 数据数组
|
|
142
|
+
* @param headers 字段映射(如 { id: 'ID', name: '姓名' })
|
|
143
|
+
* @param sheetName 工作表名(默认 'Sheet1')
|
|
144
|
+
*/
|
|
145
|
+
export async function exportToExcel(
|
|
146
|
+
data: Array<any>,
|
|
147
|
+
headers: Record<string, string> = {},
|
|
148
|
+
sheetName = 'Sheet1'
|
|
149
|
+
) {
|
|
150
|
+
// 1. 创建工作簿和工作表
|
|
151
|
+
const workbook = new ExcelJS.Workbook();
|
|
152
|
+
const worksheet = workbook.addWorksheet(sheetName);
|
|
153
|
+
|
|
154
|
+
// 2. 准备表头和数据行
|
|
155
|
+
const headerKeys = Object.keys(headers);
|
|
156
|
+
const headerLabels = Object.values(headers);
|
|
157
|
+
|
|
158
|
+
// 3. 添加表头(并设置样式)
|
|
159
|
+
const headerRow = worksheet.addRow(headerLabels);
|
|
160
|
+
|
|
161
|
+
// 4. 设置表头样式
|
|
162
|
+
headerRow.font = {
|
|
163
|
+
bold: true,
|
|
164
|
+
color: { argb: 'FF000000' } // 白色字体
|
|
165
|
+
};
|
|
166
|
+
headerRow.fill = {
|
|
167
|
+
type: 'pattern',
|
|
168
|
+
pattern: 'solid',
|
|
169
|
+
fgColor: { argb: 'FF4F81BD' } // 蓝色背景
|
|
170
|
+
};
|
|
171
|
+
headerRow.alignment = { horizontal: 'center' }; // 居中
|
|
172
|
+
|
|
173
|
+
// 5. 添加数据行
|
|
174
|
+
data.forEach(row => {
|
|
175
|
+
const rowData = headerKeys.map(key => row[key] ?? '');
|
|
176
|
+
const r = worksheet.addRow(rowData);
|
|
177
|
+
// 如果是图片,则插入图片
|
|
178
|
+
for(const key in rowData) {
|
|
179
|
+
const d = rowData[key];
|
|
180
|
+
if(d?.type !== 'image') continue;
|
|
181
|
+
const cellNum = Number(key);
|
|
182
|
+
if(d.data instanceof ArrayBuffer) {
|
|
183
|
+
const id = workbook.addImage({
|
|
184
|
+
buffer: d.data,
|
|
185
|
+
extension: d.ext || 'png',
|
|
186
|
+
})
|
|
187
|
+
worksheet.addImage(id, {
|
|
188
|
+
tl: {
|
|
189
|
+
col: cellNum,
|
|
190
|
+
row: r.number - 1,
|
|
191
|
+
},
|
|
192
|
+
ext: {
|
|
193
|
+
width: 32,
|
|
194
|
+
height: 32
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
else if(d.url) {
|
|
199
|
+
const cell = r.getCell(cellNum + 1);
|
|
200
|
+
cell.value = {
|
|
201
|
+
hyperlink: d.url,
|
|
202
|
+
text: d.text || '查看图片'
|
|
203
|
+
};
|
|
204
|
+
cell.font = { color: { argb: 'FF0000FF' }, underline: true};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// 6. 自动调整列宽
|
|
210
|
+
worksheet.columns.forEach(column => {
|
|
211
|
+
if(column.values) {
|
|
212
|
+
let maxW = 20;
|
|
213
|
+
column.values.map(v => {
|
|
214
|
+
const w = String(v)?.length || 1;
|
|
215
|
+
maxW = Math.max(maxW, w);
|
|
216
|
+
});
|
|
217
|
+
column.width = maxW;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// 7. 导出 Excel 文件
|
|
222
|
+
const buffer = await workbook.xlsx.writeBuffer();
|
|
223
|
+
const blob = new Blob([buffer], {
|
|
224
|
+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return blob;
|
|
228
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
|
3
|
+
"files": [],
|
|
4
|
+
"include": [
|
|
5
|
+
"env.d.ts",
|
|
6
|
+
"src/typings/*.d.ts",
|
|
7
|
+
"src/**/*",
|
|
8
|
+
"server/**/*.ts",
|
|
9
|
+
"src/**/*.vue",
|
|
10
|
+
"src/**/**/**/**/**/*.vue",
|
|
11
|
+
"package.json",
|
|
12
|
+
"../src/config/devops.config.ts",
|
|
13
|
+
"vite.config.*",
|
|
14
|
+
"vitest.config.*",
|
|
15
|
+
"cypress.config.*",
|
|
16
|
+
"playwright.config.*"
|
|
17
|
+
],
|
|
18
|
+
"exclude": ["src/**/__tests__/*"],
|
|
19
|
+
"compilerOptions": {
|
|
20
|
+
"composite": true,
|
|
21
|
+
"module": "ESNext",
|
|
22
|
+
"types": ["vue", "node", "jsdom"],
|
|
23
|
+
"baseUrl": ".",
|
|
24
|
+
"paths": {
|
|
25
|
+
"@/*": ["./src/*"],
|
|
26
|
+
"#/*": ["../*"]
|
|
27
|
+
},
|
|
28
|
+
"noImplicitAny": false,
|
|
29
|
+
"esModuleInterop": true,
|
|
30
|
+
"allowSyntheticDefaultImports": true,
|
|
31
|
+
"moduleResolution": "bundler"
|
|
32
|
+
}
|
|
33
|
+
}
|