zkqh-lagrange-utils 0.0.52 → 0.0.53
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/dist/assets/font/iconfont-lc.css +472 -0
- package/package.json +22 -3
- package/src/service/export_plugin/PluginLocalExport.jsx +205 -0
- package/src/service/export_plugin/api/index.js +6 -0
- package/src/service/export_plugin/api/plugin-bridge.js +154 -0
- package/src/service/export_plugin/api/plugin.js +95 -0
- package/src/service/export_plugin/hooks/use-plugin-health.js +94 -0
- package/src/service/export_plugin/index.js +15 -0
- package/src/service/export_plugin/internal/create-folder-modal.jsx +102 -0
- package/src/service/export_plugin/internal/plugin-dir-select.jsx +343 -0
- package/src/service/export_plugin/internal/styles/export-plugin-modals.less +4 -0
- package/src/service/export_plugin/internal/styles/plugin-dir-select.less +85 -0
- package/src/service/export_plugin/internal/styles/plugin-local-export.less +65 -0
- package/src/service/export_plugin/utils/plugin-dir.js +101 -0
- package/src/service/export_plugin/utils/size.js +41 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { defineComponent, ref, computed, toRefs, onMounted } from "vue"
|
|
2
|
+
import { PluginDirSelect } from "./internal/plugin-dir-select.jsx"
|
|
3
|
+
import { usePluginHealth } from "./hooks/use-plugin-health.js"
|
|
4
|
+
import { isOverSizeLimit, isPreviewOverflowDisk } from "./utils/size.js"
|
|
5
|
+
import "./internal/styles/plugin-local-export.less"
|
|
6
|
+
import("./assets/font/iconfont-lc.css")
|
|
7
|
+
|
|
8
|
+
const DEFAULT_PREVIEW = {
|
|
9
|
+
show: false,
|
|
10
|
+
totalSize: 0,
|
|
11
|
+
exportDisabled: false,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 本地导出组合区:平台/应用小文件导出 + 插件目录导出 + 50MB 分流
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* <PluginLocalExport
|
|
19
|
+
* previewInfo={{ show, totalSize, exportDisabled }}
|
|
20
|
+
* downloadUrl={url}
|
|
21
|
+
* onSuccess={({ mode, dirname }) => {}}
|
|
22
|
+
* />
|
|
23
|
+
*/
|
|
24
|
+
export const PluginLocalExport = defineComponent({
|
|
25
|
+
name: "PluginLocalExport",
|
|
26
|
+
props: {
|
|
27
|
+
/**
|
|
28
|
+
* 预览与分流控制
|
|
29
|
+
* @type {{ show?: boolean, totalSize?: number, exportDisabled?: boolean }}
|
|
30
|
+
*/
|
|
31
|
+
previewInfo: {
|
|
32
|
+
type: Object,
|
|
33
|
+
default: () => ({ ...DEFAULT_PREVIEW }),
|
|
34
|
+
},
|
|
35
|
+
/** 插件安装包下载地址 */
|
|
36
|
+
downloadUrl: { type: String, default: "" },
|
|
37
|
+
},
|
|
38
|
+
emits: ["success"],
|
|
39
|
+
setup(props, { emit }) {
|
|
40
|
+
const { previewInfo, downloadUrl } = toRefs(props)
|
|
41
|
+
const dirname = ref("")
|
|
42
|
+
const diskInfo = ref({ total_size: 0, used_size: 0 })
|
|
43
|
+
|
|
44
|
+
const health = usePluginHealth({
|
|
45
|
+
downloadUrl: () => downloadUrl.value,
|
|
46
|
+
})
|
|
47
|
+
const { pluginReady, checkHealth, triggerInstall } = health
|
|
48
|
+
|
|
49
|
+
onMounted(() => {
|
|
50
|
+
checkHealth()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const preview = computed(() => ({
|
|
54
|
+
...DEFAULT_PREVIEW,
|
|
55
|
+
...(previewInfo.value || {}),
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 是否展示平台/应用导出区(≤50MB)
|
|
60
|
+
* show=true 且未禁用且有大小且未超 50MB
|
|
61
|
+
*/
|
|
62
|
+
const usePlatformExport = computed(() => {
|
|
63
|
+
const info = preview.value
|
|
64
|
+
if (!info.show || info.exportDisabled) return false
|
|
65
|
+
const size = Number(info.totalSize) || 0
|
|
66
|
+
if (size <= 0) return false
|
|
67
|
+
return !isOverSizeLimit(size)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 是否展示插件导出区
|
|
72
|
+
* - show=false:始终插件
|
|
73
|
+
* - show=true:已可导出且 >50MB
|
|
74
|
+
*/
|
|
75
|
+
const usePluginExport = computed(() => {
|
|
76
|
+
const info = preview.value
|
|
77
|
+
if (!info.show) return true
|
|
78
|
+
if (info.exportDisabled) return false
|
|
79
|
+
const size = Number(info.totalSize) || 0
|
|
80
|
+
if (size <= 0) return false
|
|
81
|
+
return isOverSizeLimit(size)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
/** 插件路径:预览大小是否超出当前目录剩余空间 */
|
|
85
|
+
const diskOverflow = computed(() => {
|
|
86
|
+
if (!pluginReady.value || !usePluginExport.value) return false
|
|
87
|
+
const info = preview.value
|
|
88
|
+
if (!info.show) return false
|
|
89
|
+
return isPreviewOverflowDisk(info.totalSize, diskInfo.value)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const pluginExportDisabled = computed(() => {
|
|
93
|
+
if (!pluginReady.value) return true
|
|
94
|
+
if (!dirname.value) return true
|
|
95
|
+
if (diskOverflow.value) return true
|
|
96
|
+
return false
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const pluginButtonTitle = computed(() => {
|
|
100
|
+
if (!pluginReady.value) return "请先安装并启动导出插件"
|
|
101
|
+
if (!dirname.value) return "请选择本地导出目录"
|
|
102
|
+
if (diskOverflow.value) return "预估大小超出本地目录剩余空间,无法导出"
|
|
103
|
+
return ""
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const onDiskInfoUpdate = (info) => {
|
|
107
|
+
diskInfo.value = info || { total_size: 0, used_size: 0 }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const onInstall = () => {
|
|
111
|
+
triggerInstall()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 平台/应用路径:确认后回调 */
|
|
115
|
+
const onPlatformExportClick = () => {
|
|
116
|
+
window.$dialog?.warning?.({
|
|
117
|
+
title: "确认导出",
|
|
118
|
+
content: "确认按当前筛选条件通过平台打包下载?",
|
|
119
|
+
positiveText: "确认",
|
|
120
|
+
negativeText: "取消",
|
|
121
|
+
onPositiveClick: () => {
|
|
122
|
+
emit("success", { mode: "platform" })
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 插件路径:校验后回调(含 dirname) */
|
|
128
|
+
const onPluginExportClick = () => {
|
|
129
|
+
if (pluginExportDisabled.value) return
|
|
130
|
+
emit("success", { mode: "plugin", dirname: dirname.value })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return () => {
|
|
134
|
+
const showPlatform = usePlatformExport.value
|
|
135
|
+
const showPlugin = usePluginExport.value
|
|
136
|
+
|
|
137
|
+
if (!showPlatform && !showPlugin) {
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div class="plugin_local_export">
|
|
143
|
+
{showPlatform ? (
|
|
144
|
+
<div class="_ple_platform">
|
|
145
|
+
<span class="__tip lc-fs-m">文件小于50MB,请直接导出!</span>
|
|
146
|
+
<span
|
|
147
|
+
class="__action lc_btn_primary lc_btn_m"
|
|
148
|
+
onClick={onPlatformExportClick}>
|
|
149
|
+
导 出
|
|
150
|
+
</span>
|
|
151
|
+
</div>
|
|
152
|
+
) : null}
|
|
153
|
+
|
|
154
|
+
{showPlugin ? (
|
|
155
|
+
<>
|
|
156
|
+
<div class="_ple_label lc-fs-m">选择本地目录</div>
|
|
157
|
+
{pluginReady.value && diskOverflow.value ? (
|
|
158
|
+
<div class="_ple_warn lc-fs-s">
|
|
159
|
+
<i class="lc lc-warning-circle-outlined"></i>
|
|
160
|
+
<span>预估大小超出本地目录剩余空间,请更换目录或缩小导出范围</span>
|
|
161
|
+
</div>
|
|
162
|
+
) : null}
|
|
163
|
+
|
|
164
|
+
{!pluginReady.value ? (
|
|
165
|
+
<div class="_ple_row">
|
|
166
|
+
<n-tree-select
|
|
167
|
+
size="small"
|
|
168
|
+
disabled
|
|
169
|
+
placeholder="安装插件后可选择导出文件"
|
|
170
|
+
options={[]}
|
|
171
|
+
/>
|
|
172
|
+
<span
|
|
173
|
+
class="lc_btn_primary lc_btn_m _ple_install"
|
|
174
|
+
onClick={onInstall}>
|
|
175
|
+
立即安装插件
|
|
176
|
+
</span>
|
|
177
|
+
</div>
|
|
178
|
+
) : (
|
|
179
|
+
<div class="_ple_row">
|
|
180
|
+
<PluginDirSelect
|
|
181
|
+
modelValue={dirname.value}
|
|
182
|
+
onUpdate:modelValue={(v) => {
|
|
183
|
+
dirname.value = v
|
|
184
|
+
}}
|
|
185
|
+
onUpdate:diskInfo={onDiskInfoUpdate}
|
|
186
|
+
/>
|
|
187
|
+
<div class="_ple_action">
|
|
188
|
+
<span
|
|
189
|
+
class={["lc_btn_primary", "lc_btn_m", pluginExportDisabled.value ? "_disabled" : ""]}
|
|
190
|
+
title={pluginButtonTitle.value}
|
|
191
|
+
onClick={onPluginExportClick}>
|
|
192
|
+
导 出
|
|
193
|
+
</span>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</>
|
|
198
|
+
) : null}
|
|
199
|
+
</div>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
export default PluginLocalExport
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/** 与 front_plugins/lagrange-export-bridge 内容脚本约定的消息源(内置,不对外) */
|
|
2
|
+
export const PLUGIN_BRIDGE_SOURCE = "lagrange-export-bridge"
|
|
3
|
+
|
|
4
|
+
const REQUEST = "LAGRANGE_EXPORT_BRIDGE_REQUEST"
|
|
5
|
+
const RESPONSE = "LAGRANGE_EXPORT_BRIDGE_RESPONSE"
|
|
6
|
+
const READY = "LAGRANGE_EXPORT_BRIDGE_READY"
|
|
7
|
+
const PING = "LAGRANGE_EXPORT_BRIDGE_PING"
|
|
8
|
+
|
|
9
|
+
const BRIDGE_WAIT_MS = 1200
|
|
10
|
+
const BRIDGE_TIMEOUT_MS = 15000
|
|
11
|
+
|
|
12
|
+
const debugEnabled = () =>
|
|
13
|
+
typeof localStorage !== "undefined" && localStorage.getItem("LAGRANGE_PLUGIN_BRIDGE_DEBUG") === "1"
|
|
14
|
+
|
|
15
|
+
/** @param {...unknown} args */
|
|
16
|
+
function bridgeLog(...args) {
|
|
17
|
+
if (debugEnabled()) {
|
|
18
|
+
console.log("[plugin-bridge]", ...args)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let bridgeReady = false
|
|
23
|
+
let listenerAttached = false
|
|
24
|
+
|
|
25
|
+
/** 内容脚本注入后写入,避免 READY 早于业务脚本 */
|
|
26
|
+
function isBridgeDomMarked() {
|
|
27
|
+
return typeof document !== "undefined" && document.documentElement?.dataset?.lagrangeExportBridge === "1"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function markBridgeReady(reason) {
|
|
31
|
+
if (!bridgeReady) {
|
|
32
|
+
bridgeReady = true
|
|
33
|
+
bridgeLog("ready", reason)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function attachBridgeListener() {
|
|
38
|
+
if (listenerAttached || typeof window === "undefined") return
|
|
39
|
+
listenerAttached = true
|
|
40
|
+
window.addEventListener("message", (event) => {
|
|
41
|
+
if (event.origin !== window.location.origin) return
|
|
42
|
+
const data = event.data
|
|
43
|
+
if (data?.source !== PLUGIN_BRIDGE_SOURCE || data?.type !== READY) return
|
|
44
|
+
markBridgeReady("message")
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
attachBridgeListener()
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 扩展桥接是否可用
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
export function isPluginBridgeReady() {
|
|
55
|
+
return bridgeReady || isBridgeDomMarked()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 等待扩展 content script 就绪
|
|
60
|
+
* @returns {Promise<boolean>}
|
|
61
|
+
*/
|
|
62
|
+
export function waitForPluginBridge() {
|
|
63
|
+
if (isPluginBridgeReady()) {
|
|
64
|
+
return Promise.resolve(true)
|
|
65
|
+
}
|
|
66
|
+
attachBridgeListener()
|
|
67
|
+
window.postMessage(
|
|
68
|
+
{ source: PLUGIN_BRIDGE_SOURCE, type: PING },
|
|
69
|
+
window.location.origin,
|
|
70
|
+
)
|
|
71
|
+
bridgeLog("ping sent", window.location.origin)
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
const deadline = Date.now() + BRIDGE_WAIT_MS
|
|
74
|
+
const tick = () => {
|
|
75
|
+
if (isBridgeDomMarked()) {
|
|
76
|
+
markBridgeReady("dom-dataset")
|
|
77
|
+
}
|
|
78
|
+
if (isPluginBridgeReady()) {
|
|
79
|
+
resolve(true)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
if (Date.now() >= deadline) {
|
|
83
|
+
bridgeLog("wait timeout, bridge not detected")
|
|
84
|
+
resolve(false)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
requestAnimationFrame(tick)
|
|
88
|
+
}
|
|
89
|
+
tick()
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createBridgeRequestId() {
|
|
94
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
95
|
+
try {
|
|
96
|
+
return crypto.randomUUID()
|
|
97
|
+
} catch {
|
|
98
|
+
// HTTP 非安全上下文下 randomUUID 不可用
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return `ep-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 经 Chrome 扩展访问本机插件 API
|
|
106
|
+
* @param {string} method
|
|
107
|
+
* @param {string} path 如 /health
|
|
108
|
+
* @param {object} [body]
|
|
109
|
+
* @returns {Promise<object>}
|
|
110
|
+
*/
|
|
111
|
+
export function pluginFetchViaBridge(method, path, body) {
|
|
112
|
+
bridgeLog("request", method, path)
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
const id = createBridgeRequestId()
|
|
115
|
+
|
|
116
|
+
const timer = setTimeout(() => {
|
|
117
|
+
window.removeEventListener("message", onMessage)
|
|
118
|
+
reject(new Error("扩展桥接请求超时"))
|
|
119
|
+
}, BRIDGE_TIMEOUT_MS)
|
|
120
|
+
|
|
121
|
+
const onMessage = (event) => {
|
|
122
|
+
if (event.origin !== window.location.origin) return
|
|
123
|
+
const data = event.data
|
|
124
|
+
if (data?.source !== PLUGIN_BRIDGE_SOURCE || data?.type !== RESPONSE || data.id !== id) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
clearTimeout(timer)
|
|
128
|
+
window.removeEventListener("message", onMessage)
|
|
129
|
+
|
|
130
|
+
if (data.error) {
|
|
131
|
+
reject(new Error(data.error))
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
if (data.data == null && data.ok === false) {
|
|
135
|
+
reject(new Error("扩展桥接失败"))
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
resolve(data.data)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
window.addEventListener("message", onMessage)
|
|
142
|
+
window.postMessage(
|
|
143
|
+
{
|
|
144
|
+
source: PLUGIN_BRIDGE_SOURCE,
|
|
145
|
+
type: REQUEST,
|
|
146
|
+
id,
|
|
147
|
+
method,
|
|
148
|
+
path,
|
|
149
|
+
body,
|
|
150
|
+
},
|
|
151
|
+
window.location.origin,
|
|
152
|
+
)
|
|
153
|
+
})
|
|
154
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import axios from "axios"
|
|
2
|
+
import {
|
|
3
|
+
isPluginBridgeReady,
|
|
4
|
+
pluginFetchViaBridge,
|
|
5
|
+
waitForPluginBridge,
|
|
6
|
+
} from "./plugin-bridge.js"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 本地导出插件 API(独立于平台 /plat)
|
|
10
|
+
* 优先经 Chrome 扩展桥接;无扩展时直连 127.0.0.1
|
|
11
|
+
*/
|
|
12
|
+
const PLUGIN_HOST = "http://127.0.0.1:12000"
|
|
13
|
+
|
|
14
|
+
const pluginRequest = axios.create({
|
|
15
|
+
baseURL: `${PLUGIN_HOST}/plugin`,
|
|
16
|
+
timeout: 15000,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
/** GET 不携带 Content-Type,避免多余的非简单请求预检 */
|
|
20
|
+
pluginRequest.interceptors.request.use((config) => {
|
|
21
|
+
const method = (config.method || "get").toLowerCase()
|
|
22
|
+
if (method === "get" && config.headers) {
|
|
23
|
+
delete config.headers["Content-Type"]
|
|
24
|
+
delete config.headers["content-type"]
|
|
25
|
+
}
|
|
26
|
+
return config
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
pluginRequest.interceptors.response.use(
|
|
30
|
+
(response) => response.data,
|
|
31
|
+
(error) => Promise.reject(error),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
function pluginApiDebugLog(...args) {
|
|
35
|
+
if (typeof localStorage !== "undefined" && localStorage.getItem("LAGRANGE_PLUGIN_BRIDGE_DEBUG") === "1") {
|
|
36
|
+
console.log("[plugin-api]", ...args)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} method
|
|
42
|
+
* @param {string} path
|
|
43
|
+
* @param {object} [body]
|
|
44
|
+
*/
|
|
45
|
+
async function pluginRequestUnified(method, path, body) {
|
|
46
|
+
await waitForPluginBridge()
|
|
47
|
+
if (isPluginBridgeReady()) {
|
|
48
|
+
pluginApiDebugLog("via extension bridge", method, path)
|
|
49
|
+
try {
|
|
50
|
+
return await pluginFetchViaBridge(method, path, body)
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.warn("[plugin-api] 扩展桥接失败:", err?.message || err)
|
|
53
|
+
throw err
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
pluginApiDebugLog("fallback axios → 127.0.0.1", method, path)
|
|
57
|
+
const config = { method, url: path }
|
|
58
|
+
if (body !== undefined && body !== null) {
|
|
59
|
+
config.data = body
|
|
60
|
+
}
|
|
61
|
+
return pluginRequest.request(config)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 将对象拼成 query string(跳过空值)
|
|
66
|
+
* @param {Record<string, any>} params
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
function toQueryString(params = {}) {
|
|
70
|
+
const query = new URLSearchParams()
|
|
71
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
72
|
+
if (value === undefined || value === null || value === "") return
|
|
73
|
+
query.set(key, String(value))
|
|
74
|
+
})
|
|
75
|
+
const qs = query.toString()
|
|
76
|
+
return qs ? `?${qs}` : ""
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const pluginApi = {
|
|
80
|
+
health: () => pluginRequestUnified("GET", "/health"),
|
|
81
|
+
getDiskList: () => pluginRequestUnified("GET", "/listdir"),
|
|
82
|
+
getDirList: (dirname = "/") => pluginRequestUnified("PUT", "/listdir", { dirname }),
|
|
83
|
+
createFolder: (dirname, foldername) => pluginRequestUnified("POST", "/listdir", { dirname, foldername }),
|
|
84
|
+
getDefaultDir: () => pluginRequestUnified("GET", "/dirname"),
|
|
85
|
+
setDefaultDir: (dirname) => pluginRequestUnified("PATCH", "/dirname", { dirname }),
|
|
86
|
+
/** 导出作业列表(无入参) */
|
|
87
|
+
getExportTasks: () => pluginRequestUnified("GET", "/export"),
|
|
88
|
+
/**
|
|
89
|
+
* 导出作业列表(分页)
|
|
90
|
+
* @param {{ farm: string, page_size?: number, current_page?: number, kind?: string }} params
|
|
91
|
+
*/
|
|
92
|
+
getExportTasksPage: (params = {}) => pluginRequestUnified("GET", `/exports${toQueryString(params)}`),
|
|
93
|
+
createExport: (params) => pluginRequestUnified("POST", "/export", params),
|
|
94
|
+
controlExport: (id, params) => pluginRequestUnified("PATCH", `/export/${id}`, params),
|
|
95
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ref, onBeforeUnmount, unref } from "vue"
|
|
2
|
+
import { pluginApi } from "../api/plugin.js"
|
|
3
|
+
import { PLUGIN_HEALTH_INTERVAL } from "../utils/size.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 触发导出插件安装包下载
|
|
7
|
+
* @param {string} [url]
|
|
8
|
+
*/
|
|
9
|
+
function _downloadPlugin(url = "") {
|
|
10
|
+
if (!url) {
|
|
11
|
+
window.$message?.info?.("请联系管理员获取导出插件安装包")
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
const link = document.createElement("a")
|
|
15
|
+
link.href = url
|
|
16
|
+
link.download = ""
|
|
17
|
+
document.body.appendChild(link)
|
|
18
|
+
link.click()
|
|
19
|
+
link.remove()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 导出插件 health:单次检测 + 安装后轮询,就绪后自动停止
|
|
24
|
+
* @param {{ interval?: number, downloadUrl?: string | import('vue').Ref<string> | (() => string) }} [options]
|
|
25
|
+
*/
|
|
26
|
+
export function usePluginHealth(options = {}) {
|
|
27
|
+
const intervalMs = options.interval ?? PLUGIN_HEALTH_INTERVAL
|
|
28
|
+
const pluginReady = ref(false)
|
|
29
|
+
const polling = ref(false)
|
|
30
|
+
let timerId = null
|
|
31
|
+
|
|
32
|
+
const resolveDownloadUrl = () => {
|
|
33
|
+
const raw = options.downloadUrl
|
|
34
|
+
if (typeof raw === "function") return raw() || ""
|
|
35
|
+
return unref(raw) || ""
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const stopPoll = () => {
|
|
39
|
+
if (timerId != null) {
|
|
40
|
+
clearInterval(timerId)
|
|
41
|
+
timerId = null
|
|
42
|
+
}
|
|
43
|
+
polling.value = false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 检测插件是否可用;就绪时自动停止轮询
|
|
48
|
+
* @returns {Promise<boolean>}
|
|
49
|
+
*/
|
|
50
|
+
const checkHealth = async () => {
|
|
51
|
+
try {
|
|
52
|
+
const res = await pluginApi.health()
|
|
53
|
+
pluginReady.value = res?.code === 200
|
|
54
|
+
} catch {
|
|
55
|
+
pluginReady.value = false
|
|
56
|
+
}
|
|
57
|
+
if (pluginReady.value) {
|
|
58
|
+
stopPoll()
|
|
59
|
+
}
|
|
60
|
+
return pluginReady.value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 开始轮询 health;插件激活后自动停止;重复调用不会叠加定时器
|
|
65
|
+
*/
|
|
66
|
+
const startPoll = () => {
|
|
67
|
+
if (pluginReady.value || polling.value) return
|
|
68
|
+
polling.value = true
|
|
69
|
+
checkHealth()
|
|
70
|
+
timerId = setInterval(() => {
|
|
71
|
+
checkHealth()
|
|
72
|
+
}, intervalMs)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 下载安装包并启动 health 轮询
|
|
77
|
+
* @param {string} [url] 优先使用入参,否则取 options.downloadUrl
|
|
78
|
+
*/
|
|
79
|
+
const triggerInstall = (url) => {
|
|
80
|
+
_downloadPlugin(url || resolveDownloadUrl())
|
|
81
|
+
startPoll()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
onBeforeUnmount(stopPoll)
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
pluginReady,
|
|
88
|
+
polling,
|
|
89
|
+
checkHealth,
|
|
90
|
+
startPoll,
|
|
91
|
+
stopPoll,
|
|
92
|
+
triggerInstall,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 导出插件服务模块
|
|
3
|
+
*
|
|
4
|
+
* 对外:
|
|
5
|
+
* - pluginApi:可独立引用
|
|
6
|
+
* - PluginLocalExport:平台/插件导出区组合组件
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* import { pluginApi, PluginLocalExport } from "zkqh-lagrange-utils/export_plugin"
|
|
10
|
+
* import { pluginApi } from "zkqh-lagrange-utils/export_plugin/api"
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { pluginApi } from "./api/plugin.js"
|
|
14
|
+
export { PluginLocalExport } from "./PluginLocalExport.jsx"
|
|
15
|
+
export { default } from "./PluginLocalExport.jsx"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { defineComponent, ref, computed } from "vue"
|
|
2
|
+
import { pluginApi } from "../api/plugin.js"
|
|
3
|
+
import { formatPathDisplay, normalizeDir } from "../utils/plugin-dir.js"
|
|
4
|
+
import "./styles/export-plugin-modals.less"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 新建文件夹弹窗(由父级 v-if 控制挂载,内部组件不对外)
|
|
8
|
+
*/
|
|
9
|
+
export const CreateFolderModal = defineComponent({
|
|
10
|
+
name: "CreateFolderModal",
|
|
11
|
+
props: {
|
|
12
|
+
/** 父目录路径 */
|
|
13
|
+
parentPath: { type: String, default: "" },
|
|
14
|
+
},
|
|
15
|
+
emits: ["close", "created"],
|
|
16
|
+
setup(props, { emit }) {
|
|
17
|
+
const folderName = ref("")
|
|
18
|
+
const parentDir = ref(normalizeDir(props.parentPath))
|
|
19
|
+
const parentDisplay = computed(() => formatPathDisplay(parentDir.value))
|
|
20
|
+
|
|
21
|
+
const close = () => {
|
|
22
|
+
emit("close")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 新建文件夹
|
|
27
|
+
* @param {string} name
|
|
28
|
+
* @param {string} dir
|
|
29
|
+
* @returns {Promise<{ parentDir: string, newPath: string } | null>}
|
|
30
|
+
*/
|
|
31
|
+
const createFolder = async (name, dir) => {
|
|
32
|
+
try {
|
|
33
|
+
const res = await pluginApi.createFolder(dir, name)
|
|
34
|
+
if (res.code !== 200) {
|
|
35
|
+
window.$message?.error?.(res.msg || "创建失败")
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const newPath = normalizeDir(res.data?.path || `${dir}${name}`)
|
|
40
|
+
window.$message?.success?.(res.msg || "创建成功")
|
|
41
|
+
return { parentDir: dir, newPath }
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.error(e)
|
|
44
|
+
window.$message?.error?.("创建失败,请确认插件可用")
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const onConfirm = async () => {
|
|
50
|
+
const name = folderName.value.trim()
|
|
51
|
+
if (!name) {
|
|
52
|
+
window.$message?.warning?.("请输入文件夹名称")
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
if (/[\\/:*?"<>|]/.test(name)) {
|
|
56
|
+
window.$message?.warning?.('名称不能包含 \\ / : * ? " < > |')
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
if (!parentDir.value) {
|
|
60
|
+
window.$message?.warning?.("请选择要创建的父目录")
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = await createFolder(name, parentDir.value)
|
|
65
|
+
if (!result) return false
|
|
66
|
+
|
|
67
|
+
emit("created", result)
|
|
68
|
+
close()
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return () => (
|
|
73
|
+
<n-modal
|
|
74
|
+
show={true}
|
|
75
|
+
onUpdate:show={(v) => {
|
|
76
|
+
if (!v) close()
|
|
77
|
+
}}
|
|
78
|
+
preset="dialog"
|
|
79
|
+
title="新建文件夹"
|
|
80
|
+
positiveText="确认"
|
|
81
|
+
negativeText="取消"
|
|
82
|
+
maskClosable={false}
|
|
83
|
+
onPositiveClick={onConfirm}>
|
|
84
|
+
<div class="export_plugin_modals_hint lc-fs-s">将在以下目录下创建:{parentDisplay.value}</div>
|
|
85
|
+
<n-input
|
|
86
|
+
value={folderName.value}
|
|
87
|
+
onUpdate:value={(v) => {
|
|
88
|
+
folderName.value = v
|
|
89
|
+
}}
|
|
90
|
+
size="small"
|
|
91
|
+
placeholder="请输入文件夹名称"
|
|
92
|
+
class="mt-8"
|
|
93
|
+
onKeyup={(e) => {
|
|
94
|
+
if (e.key === "Enter") onConfirm()
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
</n-modal>
|
|
98
|
+
)
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
export default CreateFolderModal
|