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.
@@ -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
+ 导&nbsp;&nbsp;出
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
+ 导&nbsp;&nbsp;出
193
+ </span>
194
+ </div>
195
+ </div>
196
+ )}
197
+ </>
198
+ ) : null}
199
+ </div>
200
+ )
201
+ }
202
+ },
203
+ })
204
+
205
+ export default PluginLocalExport
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 导出插件 API 独立入口(可不挂载 UI)
3
+ * @example
4
+ * import { pluginApi } from "zkqh-lagrange-utils/export_plugin/api"
5
+ */
6
+ export { pluginApi } from "./plugin.js"
@@ -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