zkqh-lagrange-utils 0.0.53 → 0.0.55
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/package.json +1 -1
- package/src/service/export_plugin/PluginLocalExport.vue +258 -0
- package/src/service/export_plugin/hooks/use-plugin-health.js +8 -0
- package/src/service/export_plugin/index.js +8 -5
- package/src/service/export_plugin/internal/{create-folder-modal.jsx → create-folder-modal.vue} +33 -30
- package/src/service/export_plugin/internal/export-task-modal.vue +149 -0
- package/src/service/export_plugin/internal/{plugin-dir-select.jsx → plugin-dir-select.vue} +72 -100
- package/src/service/export_plugin/utils/export-task.js +49 -0
- package/src/service/export_plugin/PluginLocalExport.jsx +0 -205
package/package.json
CHANGED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-if="usePlatformExport || usePluginExport"
|
|
4
|
+
class="plugin_local_export">
|
|
5
|
+
<!-- 平台 / 应用小文件导出区 -->
|
|
6
|
+
<div
|
|
7
|
+
v-if="usePlatformExport"
|
|
8
|
+
class="_ple_platform">
|
|
9
|
+
<span class="__tip lc-fs-m">文件小于50MB,请直接导出!</span>
|
|
10
|
+
<span
|
|
11
|
+
class="__action lc_btn_primary lc_btn_m"
|
|
12
|
+
@click="onPlatformExportClick">
|
|
13
|
+
导 出
|
|
14
|
+
</span>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<!-- 插件本地目录导出区 -->
|
|
18
|
+
<template v-if="usePluginExport">
|
|
19
|
+
<div class="_ple_label lc-fs-m">选择本地目录</div>
|
|
20
|
+
<div
|
|
21
|
+
v-if="pluginReady && diskOverflow"
|
|
22
|
+
class="_ple_warn lc-fs-s">
|
|
23
|
+
<i class="lc lc-warning-circle-outlined"></i>
|
|
24
|
+
<span>预估大小超出本地目录剩余空间,请更换目录或缩小导出范围</span>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div
|
|
28
|
+
v-if="!pluginReady"
|
|
29
|
+
class="_ple_row">
|
|
30
|
+
<n-tree-select
|
|
31
|
+
size="small"
|
|
32
|
+
disabled
|
|
33
|
+
placeholder="安装插件后可选择导出文件"
|
|
34
|
+
:options="[]" />
|
|
35
|
+
<span
|
|
36
|
+
class="lc_btn_primary lc_btn_m _ple_install"
|
|
37
|
+
@click="onInstall">
|
|
38
|
+
立即安装插件
|
|
39
|
+
</span>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div
|
|
43
|
+
v-else
|
|
44
|
+
class="_ple_row">
|
|
45
|
+
<PluginDirSelect
|
|
46
|
+
v-model="dirname"
|
|
47
|
+
@update:disk-info="onDiskInfoUpdate" />
|
|
48
|
+
<div class="_ple_action">
|
|
49
|
+
<span
|
|
50
|
+
class="lc_btn_primary lc_btn_m"
|
|
51
|
+
:class="{ _disabled: pluginExportDisabled }"
|
|
52
|
+
:title="pluginButtonTitle"
|
|
53
|
+
@click="onPluginExportClick">
|
|
54
|
+
导 出
|
|
55
|
+
</span>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<!-- 插件导出:任务名称弹窗(内置) -->
|
|
61
|
+
<ExportTaskModal
|
|
62
|
+
v-if="showTaskModal"
|
|
63
|
+
:kind="kind"
|
|
64
|
+
:args="args"
|
|
65
|
+
:dirname="dirname"
|
|
66
|
+
:get-token="getToken"
|
|
67
|
+
:get-url="getUrl"
|
|
68
|
+
:validate-args="validateArgs"
|
|
69
|
+
@success="onTaskModalSuccess"
|
|
70
|
+
@close="showTaskModal = false" />
|
|
71
|
+
</div>
|
|
72
|
+
</template>
|
|
73
|
+
|
|
74
|
+
<script>
|
|
75
|
+
import { defineComponent, ref, computed, toRefs, onMounted } from "vue"
|
|
76
|
+
import PluginDirSelect from "./internal/plugin-dir-select.vue"
|
|
77
|
+
import ExportTaskModal from "./internal/export-task-modal.vue"
|
|
78
|
+
import { usePluginHealth } from "./hooks/use-plugin-health.js"
|
|
79
|
+
import { isOverSizeLimit, isPreviewOverflowDisk } from "./utils/size.js"
|
|
80
|
+
import { validateTimeRangeArgs } from "./utils/export-task.js"
|
|
81
|
+
/** 组件局部使用 lc iconfont(不依赖宿主全局引入) */
|
|
82
|
+
import "zkqh-lagrange-utils/assets/font/iconfont-lc.css"
|
|
83
|
+
import "./internal/styles/plugin-local-export.less"
|
|
84
|
+
|
|
85
|
+
const DEFAULT_PREVIEW = {
|
|
86
|
+
show: false,
|
|
87
|
+
totalSize: 0,
|
|
88
|
+
exportDisabled: false,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 本地导出组合区:平台/应用小文件导出 + 插件目录导出 + 50MB 分流 + 任务弹窗
|
|
93
|
+
*/
|
|
94
|
+
export default defineComponent({
|
|
95
|
+
name: "PluginLocalExport",
|
|
96
|
+
components: { PluginDirSelect, ExportTaskModal },
|
|
97
|
+
props: {
|
|
98
|
+
/**
|
|
99
|
+
* 预览与分流控制
|
|
100
|
+
* @type {{ show?: boolean, totalSize?: number, exportDisabled?: boolean }}
|
|
101
|
+
*/
|
|
102
|
+
previewInfo: {
|
|
103
|
+
type: Object,
|
|
104
|
+
default: () => ({ ...DEFAULT_PREVIEW }),
|
|
105
|
+
},
|
|
106
|
+
/** 插件安装包下载地址 */
|
|
107
|
+
downloadUrl: { type: String, default: "" },
|
|
108
|
+
/** 导出 kind(插件创建任务用) */
|
|
109
|
+
kind: { type: String, default: "event" },
|
|
110
|
+
/** 筛选参数(插件创建任务用) */
|
|
111
|
+
args: { type: Object, default: () => ({}) },
|
|
112
|
+
/**
|
|
113
|
+
* 获取登录 token;不传则读 localStorage.token
|
|
114
|
+
* @type {(() => string) | string}
|
|
115
|
+
*/
|
|
116
|
+
getToken: { type: [Function, String], default: null },
|
|
117
|
+
/**
|
|
118
|
+
* 获取平台根 URL;不传则读 window.sys_con.baseURL
|
|
119
|
+
* @type {(() => string) | string}
|
|
120
|
+
*/
|
|
121
|
+
getUrl: { type: [Function, String], default: null },
|
|
122
|
+
/**
|
|
123
|
+
* 导出前校验 args;不传则校验时间范围
|
|
124
|
+
* @type {(args: object) => boolean}
|
|
125
|
+
*/
|
|
126
|
+
validateArgs: { type: Function, default: null },
|
|
127
|
+
},
|
|
128
|
+
emits: ["success"],
|
|
129
|
+
setup(props, { emit }) {
|
|
130
|
+
const { previewInfo, downloadUrl, kind, args, getToken, getUrl, validateArgs } = toRefs(props)
|
|
131
|
+
const dirname = ref("")
|
|
132
|
+
const diskInfo = ref({ total_size: 0, used_size: 0 })
|
|
133
|
+
const showTaskModal = ref(false)
|
|
134
|
+
|
|
135
|
+
const health = usePluginHealth({
|
|
136
|
+
downloadUrl: () => downloadUrl.value,
|
|
137
|
+
})
|
|
138
|
+
const { pluginReady, checkHealth, triggerInstall } = health
|
|
139
|
+
|
|
140
|
+
onMounted(() => {
|
|
141
|
+
checkHealth()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const preview = computed(() => ({
|
|
145
|
+
...DEFAULT_PREVIEW,
|
|
146
|
+
...(previewInfo.value || {}),
|
|
147
|
+
}))
|
|
148
|
+
|
|
149
|
+
const resolveValidate = () => validateArgs.value || validateTimeRangeArgs
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 是否展示平台/应用导出区(≤50MB)
|
|
153
|
+
*/
|
|
154
|
+
const usePlatformExport = computed(() => {
|
|
155
|
+
const info = preview.value
|
|
156
|
+
if (!info.show || info.exportDisabled) return false
|
|
157
|
+
const size = Number(info.totalSize) || 0
|
|
158
|
+
if (size <= 0) return false
|
|
159
|
+
return !isOverSizeLimit(size)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 是否展示插件导出区
|
|
164
|
+
* - show=false:始终插件
|
|
165
|
+
* - show=true:已可导出且 >50MB
|
|
166
|
+
*/
|
|
167
|
+
const usePluginExport = computed(() => {
|
|
168
|
+
const info = preview.value
|
|
169
|
+
if (!info.show) return true
|
|
170
|
+
if (info.exportDisabled) return false
|
|
171
|
+
const size = Number(info.totalSize) || 0
|
|
172
|
+
if (size <= 0) return false
|
|
173
|
+
return isOverSizeLimit(size)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
/** 插件路径:预览大小是否超出当前目录剩余空间 */
|
|
177
|
+
const diskOverflow = computed(() => {
|
|
178
|
+
if (!pluginReady.value || !usePluginExport.value) return false
|
|
179
|
+
const info = preview.value
|
|
180
|
+
if (!info.show) return false
|
|
181
|
+
return isPreviewOverflowDisk(info.totalSize, diskInfo.value)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
const pluginExportDisabled = computed(() => {
|
|
185
|
+
if (!pluginReady.value) return true
|
|
186
|
+
if (!dirname.value) return true
|
|
187
|
+
if (diskOverflow.value) return true
|
|
188
|
+
return false
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const pluginButtonTitle = computed(() => {
|
|
192
|
+
if (!pluginReady.value) return "请先安装并启动导出插件"
|
|
193
|
+
if (!dirname.value) return "请选择本地导出目录"
|
|
194
|
+
if (diskOverflow.value) return "预估大小超出本地目录剩余空间,无法导出"
|
|
195
|
+
return ""
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
const onDiskInfoUpdate = (info) => {
|
|
199
|
+
diskInfo.value = info || { total_size: 0, used_size: 0 }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const onInstall = () => {
|
|
203
|
+
triggerInstall()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 平台/应用路径:确认后回调 */
|
|
207
|
+
const onPlatformExportClick = () => {
|
|
208
|
+
const exportArgs = { ...(args.value || {}) }
|
|
209
|
+
if (!resolveValidate()(exportArgs)) return
|
|
210
|
+
|
|
211
|
+
window.$dialog?.warning?.({
|
|
212
|
+
title: "确认导出",
|
|
213
|
+
content: "确认按当前筛选条件通过平台打包下载?",
|
|
214
|
+
positiveText: "确认",
|
|
215
|
+
negativeText: "取消",
|
|
216
|
+
onPositiveClick: () => {
|
|
217
|
+
emit("success", { mode: "platform" })
|
|
218
|
+
},
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** 插件路径:打开任务名称弹窗 */
|
|
223
|
+
const onPluginExportClick = () => {
|
|
224
|
+
if (pluginExportDisabled.value) return
|
|
225
|
+
const exportArgs = { ...(args.value || {}) }
|
|
226
|
+
if (!resolveValidate()(exportArgs)) return
|
|
227
|
+
showTaskModal.value = true
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** 插件任务创建成功 */
|
|
231
|
+
const onTaskModalSuccess = () => {
|
|
232
|
+
showTaskModal.value = false
|
|
233
|
+
emit("success", { mode: "plugin", dirname: dirname.value })
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
dirname,
|
|
238
|
+
kind,
|
|
239
|
+
args,
|
|
240
|
+
getToken,
|
|
241
|
+
getUrl,
|
|
242
|
+
validateArgs,
|
|
243
|
+
showTaskModal,
|
|
244
|
+
pluginReady,
|
|
245
|
+
usePlatformExport,
|
|
246
|
+
usePluginExport,
|
|
247
|
+
diskOverflow,
|
|
248
|
+
pluginExportDisabled,
|
|
249
|
+
pluginButtonTitle,
|
|
250
|
+
onDiskInfoUpdate,
|
|
251
|
+
onInstall,
|
|
252
|
+
onPlatformExportClick,
|
|
253
|
+
onPluginExportClick,
|
|
254
|
+
onTaskModalSuccess,
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
</script>
|
|
@@ -22,6 +22,14 @@ function _downloadPlugin(url = "") {
|
|
|
22
22
|
/**
|
|
23
23
|
* 导出插件 health:单次检测 + 安装后轮询,就绪后自动停止
|
|
24
24
|
* @param {{ interval?: number, downloadUrl?: string | import('vue').Ref<string> | (() => string) }} [options]
|
|
25
|
+
* @returns {{
|
|
26
|
+
* pluginReady: import('vue').Ref<boolean>,
|
|
27
|
+
* polling: import('vue').Ref<boolean>,
|
|
28
|
+
* checkHealth: () => Promise<boolean>,
|
|
29
|
+
* startPoll: () => void,
|
|
30
|
+
* stopPoll: () => void,
|
|
31
|
+
* triggerInstall: (url?: string) => void,
|
|
32
|
+
* }}
|
|
25
33
|
*/
|
|
26
34
|
export function usePluginHealth(options = {}) {
|
|
27
35
|
const intervalMs = options.interval ?? PLUGIN_HEALTH_INTERVAL
|
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 对外:
|
|
5
5
|
* - pluginApi:可独立引用
|
|
6
|
-
* - PluginLocalExport
|
|
6
|
+
* - PluginLocalExport:平台/插件导出区组合组件(含任务名称弹窗)
|
|
7
|
+
* - ExportTaskModal:也可单独使用
|
|
8
|
+
* - usePluginHealth:插件存活检测 / 安装轮询
|
|
7
9
|
*
|
|
8
10
|
* @example
|
|
9
|
-
* import { pluginApi, PluginLocalExport } from "zkqh-lagrange-utils/export_plugin"
|
|
10
|
-
* import { pluginApi } from "zkqh-lagrange-utils/export_plugin/api"
|
|
11
|
+
* import { pluginApi, PluginLocalExport, usePluginHealth } from "zkqh-lagrange-utils/export_plugin"
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
export { pluginApi } from "./api/plugin.js"
|
|
14
|
-
export { PluginLocalExport } from "./PluginLocalExport.
|
|
15
|
-
export { default } from "./
|
|
15
|
+
export { default as PluginLocalExport } from "./PluginLocalExport.vue"
|
|
16
|
+
export { default as ExportTaskModal } from "./internal/export-task-modal.vue"
|
|
17
|
+
export { usePluginHealth } from "./hooks/use-plugin-health.js"
|
|
18
|
+
export { default } from "./PluginLocalExport.vue"
|
package/src/service/export_plugin/internal/{create-folder-modal.jsx → create-folder-modal.vue}
RENAMED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<n-modal
|
|
3
|
+
:show="true"
|
|
4
|
+
preset="dialog"
|
|
5
|
+
title="新建文件夹"
|
|
6
|
+
positive-text="确认"
|
|
7
|
+
negative-text="取消"
|
|
8
|
+
:mask-closable="false"
|
|
9
|
+
@update:show="onShowUpdate"
|
|
10
|
+
@positive-click="onConfirm">
|
|
11
|
+
<div class="export_plugin_modals_hint lc-fs-s">将在以下目录下创建:{{ parentDisplay }}</div>
|
|
12
|
+
<n-input
|
|
13
|
+
v-model:value="folderName"
|
|
14
|
+
size="small"
|
|
15
|
+
placeholder="请输入文件夹名称"
|
|
16
|
+
class="mt-8"
|
|
17
|
+
@keyup.enter="onConfirm" />
|
|
18
|
+
</n-modal>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script>
|
|
1
22
|
import { defineComponent, ref, computed } from "vue"
|
|
2
23
|
import { pluginApi } from "../api/plugin.js"
|
|
3
24
|
import { formatPathDisplay, normalizeDir } from "../utils/plugin-dir.js"
|
|
@@ -6,7 +27,7 @@ import "./styles/export-plugin-modals.less"
|
|
|
6
27
|
/**
|
|
7
28
|
* 新建文件夹弹窗(由父级 v-if 控制挂载,内部组件不对外)
|
|
8
29
|
*/
|
|
9
|
-
export
|
|
30
|
+
export default defineComponent({
|
|
10
31
|
name: "CreateFolderModal",
|
|
11
32
|
props: {
|
|
12
33
|
/** 父目录路径 */
|
|
@@ -22,6 +43,10 @@ export const CreateFolderModal = defineComponent({
|
|
|
22
43
|
emit("close")
|
|
23
44
|
}
|
|
24
45
|
|
|
46
|
+
const onShowUpdate = (v) => {
|
|
47
|
+
if (!v) close()
|
|
48
|
+
}
|
|
49
|
+
|
|
25
50
|
/**
|
|
26
51
|
* 新建文件夹
|
|
27
52
|
* @param {string} name
|
|
@@ -69,34 +94,12 @@ export const CreateFolderModal = defineComponent({
|
|
|
69
94
|
return true
|
|
70
95
|
}
|
|
71
96
|
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
)
|
|
97
|
+
return {
|
|
98
|
+
folderName,
|
|
99
|
+
parentDisplay,
|
|
100
|
+
onShowUpdate,
|
|
101
|
+
onConfirm,
|
|
102
|
+
}
|
|
99
103
|
},
|
|
100
104
|
})
|
|
101
|
-
|
|
102
|
-
export default CreateFolderModal
|
|
105
|
+
</script>
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<n-modal
|
|
3
|
+
:show="true"
|
|
4
|
+
preset="dialog"
|
|
5
|
+
title="导出任务"
|
|
6
|
+
positive-text="确认导出"
|
|
7
|
+
negative-text="取消"
|
|
8
|
+
:mask-closable="false"
|
|
9
|
+
:loading="exporting"
|
|
10
|
+
@update:show="onShowUpdate"
|
|
11
|
+
@positive-click="onConfirm">
|
|
12
|
+
<div class="export_plugin_modals_hint lc-fs-s">请填写导出任务名称,任务可在「导出进度」中查看。</div>
|
|
13
|
+
<n-input
|
|
14
|
+
v-model:value="taskName"
|
|
15
|
+
size="small"
|
|
16
|
+
placeholder="请输入导出任务名称"
|
|
17
|
+
class="mt-8"
|
|
18
|
+
@keyup.enter="onConfirm" />
|
|
19
|
+
</n-modal>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script>
|
|
23
|
+
import { defineComponent, ref } from "vue"
|
|
24
|
+
import { pluginApi } from "../api/plugin.js"
|
|
25
|
+
import {
|
|
26
|
+
buildTaskName,
|
|
27
|
+
getDefaultToken,
|
|
28
|
+
getDefaultUrl,
|
|
29
|
+
validateTimeRangeArgs,
|
|
30
|
+
} from "../utils/export-task.js"
|
|
31
|
+
import "./styles/export-plugin-modals.less"
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 导出任务名称弹窗(创建插件导出作业)
|
|
35
|
+
*/
|
|
36
|
+
export default defineComponent({
|
|
37
|
+
name: "ExportTaskModal",
|
|
38
|
+
props: {
|
|
39
|
+
/** 导出 kind */
|
|
40
|
+
kind: { type: String, default: "event" },
|
|
41
|
+
/** 筛选参数 */
|
|
42
|
+
args: { type: Object, default: () => ({}) },
|
|
43
|
+
/** 本地导出目录 */
|
|
44
|
+
dirname: { type: String, default: "" },
|
|
45
|
+
/**
|
|
46
|
+
* 获取登录 token;不传则读 localStorage.token
|
|
47
|
+
* @type {(() => string) | string}
|
|
48
|
+
*/
|
|
49
|
+
getToken: { type: [Function, String], default: null },
|
|
50
|
+
/**
|
|
51
|
+
* 获取平台根 URL;不传则读 window.sys_con.baseURL
|
|
52
|
+
* @type {(() => string) | string}
|
|
53
|
+
*/
|
|
54
|
+
getUrl: { type: [Function, String], default: null },
|
|
55
|
+
/**
|
|
56
|
+
* 导出前校验 args;不传则校验时间范围
|
|
57
|
+
* @type {(args: object) => boolean}
|
|
58
|
+
*/
|
|
59
|
+
validateArgs: { type: Function, default: null },
|
|
60
|
+
},
|
|
61
|
+
emits: ["close", "success"],
|
|
62
|
+
setup(props, { emit }) {
|
|
63
|
+
const taskName = ref(buildTaskName())
|
|
64
|
+
const exporting = ref(false)
|
|
65
|
+
|
|
66
|
+
const resolveToken = () => {
|
|
67
|
+
if (props.getToken == null) return getDefaultToken()
|
|
68
|
+
return typeof props.getToken === "function" ? props.getToken() : String(props.getToken || "")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const resolveUrl = () => {
|
|
72
|
+
if (props.getUrl == null) return getDefaultUrl()
|
|
73
|
+
return typeof props.getUrl === "function" ? props.getUrl() : String(props.getUrl || "")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const close = () => {
|
|
77
|
+
emit("close")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const onShowUpdate = (v) => {
|
|
81
|
+
if (!v) close()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 创建导出任务
|
|
86
|
+
* @param {string} name 任务名称
|
|
87
|
+
* @returns {Promise<boolean>}
|
|
88
|
+
*/
|
|
89
|
+
const createExport = async (name) => {
|
|
90
|
+
const exportArgs = { ...(props.args || {}) }
|
|
91
|
+
const validate = props.validateArgs || validateTimeRangeArgs
|
|
92
|
+
if (!validate(exportArgs)) return false
|
|
93
|
+
|
|
94
|
+
const token = resolveToken()
|
|
95
|
+
const url = resolveUrl()
|
|
96
|
+
if (!token) {
|
|
97
|
+
window.$message?.error?.("登录态失效,请重新登录")
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
exporting.value = true
|
|
102
|
+
try {
|
|
103
|
+
const res = await pluginApi.createExport({
|
|
104
|
+
name,
|
|
105
|
+
kind: props.kind,
|
|
106
|
+
token,
|
|
107
|
+
url,
|
|
108
|
+
dirname: props.dirname,
|
|
109
|
+
args: exportArgs,
|
|
110
|
+
})
|
|
111
|
+
if (res.code === 200) {
|
|
112
|
+
window.$message?.success?.(res.msg || "已创建导出任务")
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
window.$message?.error?.(res.msg || "创建导出任务失败")
|
|
116
|
+
return false
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.error(e)
|
|
119
|
+
window.$message?.error?.("创建导出任务失败,请确认插件可用")
|
|
120
|
+
return false
|
|
121
|
+
} finally {
|
|
122
|
+
exporting.value = false
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const onConfirm = async () => {
|
|
127
|
+
const name = taskName.value.trim()
|
|
128
|
+
if (!name) {
|
|
129
|
+
window.$message?.warning?.("请输入导出任务名称")
|
|
130
|
+
return false
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const ok = await createExport(name)
|
|
134
|
+
if (!ok) return false
|
|
135
|
+
|
|
136
|
+
emit("success")
|
|
137
|
+
close()
|
|
138
|
+
return true
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
taskName,
|
|
143
|
+
exporting,
|
|
144
|
+
onShowUpdate,
|
|
145
|
+
onConfirm,
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
})
|
|
149
|
+
</script>
|
|
@@ -1,83 +1,63 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
buildPathChain,
|
|
5
|
-
findNodeByKey,
|
|
6
|
-
formatDiskRemain,
|
|
7
|
-
isPathUnder,
|
|
8
|
-
mapDirNodes,
|
|
9
|
-
normalizeDir,
|
|
10
|
-
normalizePathKey,
|
|
11
|
-
toTreeKey,
|
|
12
|
-
} from "../utils/plugin-dir.js"
|
|
13
|
-
import { CreateFolderModal } from "./create-folder-modal.jsx"
|
|
14
|
-
import "./styles/plugin-dir-select.less"
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* 目录树选择
|
|
18
|
-
* @param {object} ctx
|
|
19
|
-
*/
|
|
20
|
-
const renderDirTreeSelect = (ctx) => {
|
|
21
|
-
const {
|
|
22
|
-
treeSelectKey,
|
|
23
|
-
treeSelectValue,
|
|
24
|
-
treeOptions,
|
|
25
|
-
expandedKeys,
|
|
26
|
-
renderTreeLabel,
|
|
27
|
-
onLoadTreeNode,
|
|
28
|
-
onExpandedKeysUpdate,
|
|
29
|
-
onDirChange,
|
|
30
|
-
} = ctx
|
|
31
|
-
|
|
32
|
-
return (
|
|
1
|
+
<template>
|
|
2
|
+
<div class="plugin_dir_select">
|
|
33
3
|
<n-tree-select
|
|
34
|
-
key=
|
|
35
|
-
value=
|
|
4
|
+
:key="treeSelectKey"
|
|
5
|
+
:value="treeSelectValue"
|
|
36
6
|
size="small"
|
|
37
7
|
placeholder="请选择本地目录"
|
|
38
|
-
|
|
39
|
-
|
|
8
|
+
:consistent-menu-width="false"
|
|
9
|
+
show-path
|
|
40
10
|
separator="/"
|
|
41
11
|
filterable
|
|
42
12
|
clearable
|
|
43
|
-
options=
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* 文件夹图标 + 磁盘占用
|
|
56
|
-
* @param {object} ctx
|
|
57
|
-
*/
|
|
58
|
-
const renderDirUsage = (ctx) => {
|
|
59
|
-
const { diskUsageText, usagePercent } = ctx
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<>
|
|
13
|
+
:options="treeOptions"
|
|
14
|
+
:expanded-keys="expandedKeys"
|
|
15
|
+
:menu-props="{ class: 'plugin_dir_select_tree' }"
|
|
16
|
+
:render-label="renderTreeLabel"
|
|
17
|
+
:on-load="onLoadTreeNode"
|
|
18
|
+
@update:expanded-keys="onExpandedKeysUpdate"
|
|
19
|
+
@update:value="onDirChange" />
|
|
20
|
+
<template v-if="diskUsageText">
|
|
63
21
|
<span class="_pds_folder_icon">
|
|
64
22
|
<i class="lc lc-tuxing1"></i>
|
|
65
23
|
</span>
|
|
66
24
|
<div class="_pds_usage">
|
|
67
25
|
<div class="__bar">
|
|
68
|
-
<span style={
|
|
26
|
+
<span :style="{ width: `${usagePercent}%` }"></span>
|
|
69
27
|
</div>
|
|
70
|
-
<span class="lc-fs-s">{diskUsageText
|
|
28
|
+
<span class="lc-fs-s">{{ diskUsageText }}</span>
|
|
71
29
|
</div>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
30
|
+
</template>
|
|
31
|
+
<CreateFolderModal
|
|
32
|
+
v-if="showCreateModal"
|
|
33
|
+
:parent-path="createParentPath"
|
|
34
|
+
@created="onFolderCreated"
|
|
35
|
+
@close="showCreateModal = false" />
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script>
|
|
40
|
+
import { defineComponent, ref, computed, toRefs, h } from "vue"
|
|
41
|
+
import { pluginApi } from "../api/plugin.js"
|
|
42
|
+
import {
|
|
43
|
+
buildPathChain,
|
|
44
|
+
findNodeByKey,
|
|
45
|
+
formatDiskRemain,
|
|
46
|
+
isPathUnder,
|
|
47
|
+
mapDirNodes,
|
|
48
|
+
normalizeDir,
|
|
49
|
+
normalizePathKey,
|
|
50
|
+
toTreeKey,
|
|
51
|
+
} from "../utils/plugin-dir.js"
|
|
52
|
+
import CreateFolderModal from "./create-folder-modal.vue"
|
|
53
|
+
import "./styles/plugin-dir-select.less"
|
|
75
54
|
|
|
76
55
|
/**
|
|
77
56
|
* 插件本地目录选择(树选择 + 占用展示 + 新建文件夹,内部组件)
|
|
78
57
|
*/
|
|
79
|
-
export
|
|
58
|
+
export default defineComponent({
|
|
80
59
|
name: "PluginDirSelect",
|
|
60
|
+
components: { CreateFolderModal },
|
|
81
61
|
props: {
|
|
82
62
|
/** 当前选中目录(v-model) */
|
|
83
63
|
modelValue: { type: String, default: "" },
|
|
@@ -126,18 +106,20 @@ export const PluginDirSelect = defineComponent({
|
|
|
126
106
|
}
|
|
127
107
|
|
|
128
108
|
/** 目录树节点:文件夹名 + 新建子目录 */
|
|
129
|
-
const renderTreeLabel = ({ option }) =>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
109
|
+
const renderTreeLabel = ({ option }) =>
|
|
110
|
+
h("span", { class: "_pds_tree_node" }, [
|
|
111
|
+
h("span", { class: "__name" }, option.label),
|
|
112
|
+
h(
|
|
113
|
+
"span",
|
|
114
|
+
{
|
|
115
|
+
class: "__add",
|
|
116
|
+
title: "新建子文件夹",
|
|
117
|
+
onMousedown: (e) => e.stopPropagation(),
|
|
118
|
+
onClick: (e) => openCreateFolder(option.key, e),
|
|
119
|
+
},
|
|
120
|
+
"+",
|
|
121
|
+
),
|
|
122
|
+
])
|
|
141
123
|
|
|
142
124
|
const loadNodeChildren = async (node) => {
|
|
143
125
|
if (Array.isArray(node.children)) return node.children
|
|
@@ -313,31 +295,21 @@ export const PluginDirSelect = defineComponent({
|
|
|
313
295
|
|
|
314
296
|
initDirectory()
|
|
315
297
|
|
|
316
|
-
return
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
parentPath={createParentPath.value}
|
|
332
|
-
onCreated={onFolderCreated}
|
|
333
|
-
onClose={() => {
|
|
334
|
-
showCreateModal.value = false
|
|
335
|
-
}}
|
|
336
|
-
/>
|
|
337
|
-
) : null}
|
|
338
|
-
</div>
|
|
339
|
-
)
|
|
298
|
+
return {
|
|
299
|
+
showCreateModal,
|
|
300
|
+
createParentPath,
|
|
301
|
+
treeOptions,
|
|
302
|
+
expandedKeys,
|
|
303
|
+
treeSelectKey,
|
|
304
|
+
treeSelectValue,
|
|
305
|
+
diskUsageText,
|
|
306
|
+
usagePercent,
|
|
307
|
+
renderTreeLabel,
|
|
308
|
+
onLoadTreeNode,
|
|
309
|
+
onExpandedKeysUpdate,
|
|
310
|
+
onDirChange,
|
|
311
|
+
onFolderCreated,
|
|
312
|
+
}
|
|
340
313
|
},
|
|
341
314
|
})
|
|
342
|
-
|
|
343
|
-
export default PluginDirSelect
|
|
315
|
+
</script>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 默认读取平台登录 token(可被 props.getToken 覆盖)
|
|
3
|
+
* @returns {string}
|
|
4
|
+
*/
|
|
5
|
+
export const getDefaultToken = () => {
|
|
6
|
+
try {
|
|
7
|
+
const token = localStorage.getItem("token")
|
|
8
|
+
if (!token) return ""
|
|
9
|
+
return JSON.parse(token)?.access_token || ""
|
|
10
|
+
} catch {
|
|
11
|
+
return ""
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 默认平台根地址(可被 props.getUrl 覆盖)
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export const getDefaultUrl = () => {
|
|
20
|
+
const base =
|
|
21
|
+
(typeof window !== "undefined" && window.sys_con?.baseURL) ||
|
|
22
|
+
`${location.protocol}//${location.host}`
|
|
23
|
+
return String(base).replace(/\/$/, "")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 默认校验:导出 args 需含完整时间范围
|
|
28
|
+
* @param {Record<string, any>} args
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
export const validateTimeRangeArgs = (args = {}) => {
|
|
32
|
+
if (!(args.start_time && args.end_time)) {
|
|
33
|
+
window.$message?.warning?.("请选择发生时间")
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 生成导出默认名称:导出数据_<年><月><日>
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
export const buildTaskName = () => {
|
|
44
|
+
const d = new Date()
|
|
45
|
+
const y = d.getFullYear()
|
|
46
|
+
const m = String(d.getMonth() + 1).padStart(2, "0")
|
|
47
|
+
const day = String(d.getDate()).padStart(2, "0")
|
|
48
|
+
return `导出数据_${y}${m}${day}`
|
|
49
|
+
}
|
|
@@ -1,205 +0,0 @@
|
|
|
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
|