Commit 22485358 by pangchong

refactor(api): 优化接口请求处理与表单提交方式

- 移除旧的错误信息智能识别和处理逻辑,精简响应码拦截代码
- 新增 postForm 方法支持自动处理 multipart/form-data,优化文件上传逻辑
- 删除下载文件相关的复杂实现,简化请求发送流程
- 在批量翻译功能中添加取消翻译支持,完善用户交互
- 优化编辑器选中节点逻辑,确保滚动时正确设置选中状态
- 调整编辑器工具栏分隔线的样式,提升界面一致性
- 修改搜索翻译模块的警示卡样式,增强视觉效果
- 移除多个无用的 utils 导入,精简编辑器相关代码
- 修正设置面板确认清空并退出弹窗调用,使用标准对象参数
- 新增 Splitter 组件的类型定义,调整相关实现导入方式
parent a05473b7
...@@ -93,77 +93,6 @@ const createService = (baseURL: string) => { ...@@ -93,77 +93,6 @@ const createService = (baseURL: string) => {
} }
} }
// 此处可根据后台全局的格式状态码做拦截或直接返回
if (json && json.code !== 200) {
// 1. 定义识别“技术性垃圾 ID”的逻辑
const isTechnicalId = (s: any) =>
!s ||
typeof s !== 'string' ||
s.trim() === '' ||
s.startsWith('ERRMSG.') || // 过滤标识符
s.startsWith('#{') || // 过滤占位符
/^[A-Z0-9_.]+$/.test(s) || // 过滤全大写/点号连接的代码名
(json && s === String(json.code)) // 过滤纯状态码
// 2. 初始回退信息
let errorMsg = json.msg || '请求发生错误'
// 如果在 RequestConfig 中指定了字段名,则以此为主 (最高优先级)
const customField = method.meta?.msgField
if (customField && json.data && typeof json.data === 'object' && json.data[customField]) {
errorMsg = json.data[customField]
} else {
// 3. 构建候选字段池 (按质量优先级排序)
const { default: i18n } = await import('@/locales')
const isZh = String(i18n.global.locale.value || i18n.global.locale)
.toLowerCase()
.includes('zh')
// 收集所有候选,优先处理 data 内部字段
const dataObj = json.data && typeof json.data === 'object' ? json.data : {}
const candidates = [dataObj.message, dataObj.remark, dataObj.msg, dataObj.i18nText, dataObj.msgData]
// 注入外层候选字段
candidates.push(json.msg)
candidates.push(json.msgData)
// 4. 智能筛选逻辑
for (let cand of candidates) {
if (!cand) continue
// 处理数组类型的错误信息(常见于 msgData)
if (Array.isArray(cand)) {
cand = cand.length > 0 ? cand[0] : null
}
if (!cand) continue
if (!isTechnicalId(cand)) {
// 如果是中文环境,进一步检查是否有中文字符,提升准确度
if (isZh && /[\u4e00-\u9fa5]/.test(cand!)) {
errorMsg = cand!
break
}
// 如果是非中文环境或未匹配到中文,但这个候选词不是技术 ID,我们也选它
if (!isZh || !errorMsg || isTechnicalId(errorMsg)) {
errorMsg = cand!
// 如果在非中文环境下找到了 i18nText,直接结束
if (!isZh && cand === json.data?.i18nText) break
}
}
}
}
// 【核心修复】将智能识别出的信息回写到响应对象中,确保业务层获取到的也是处理后的文案
json.msg = errorMsg
if (json.code === 100 || json.code === '100') {
window.$message.error(errorMsg)
window.location.hash = '/login'
throw new Error('未登录')
}
window.$message.error(errorMsg)
// 视业务约定是否抛出异常来中断后续 promise 处理
// throw new Error(errorMsg)
}
return json! return json!
}, },
onError: (err, method) => { onError: (err, method) => {
...@@ -261,128 +190,42 @@ const createService = (baseURL: string) => { ...@@ -261,128 +190,42 @@ const createService = (baseURL: string) => {
}) })
.send(true) .send(true)
}, },
/** 下载文件 (支持流式下载及自动两步验证模式) */ /** 提交 multipart/form-data 数据,不设置 Content-Type,由浏览器自动携带 boundary
async download(url: string, data?: any, fileName?: string, config?: RequestConfig) { * - 普通值: `{ key: value }`
const { showLoading = true, ...rest } = config || {} * - 带文件名的 Blob: `{ key: [blob, 'filename.ext'] }`
*/
const getProcessedBody = (dataObj: any) => { postForm<T = any>(url: string, data?: FormData | Record<string, any>, config?: RequestConfig) {
if (dataObj instanceof FormData) return dataObj const { showLoading = false, msgField, ...rest } = config || {}
const searchParams = new URLSearchParams() let formData: FormData
if (dataObj && typeof dataObj === 'object') { if (data instanceof FormData) {
Object.keys(dataObj).forEach((key) => { formData = data
const val = dataObj[key] } else {
searchParams.append(key, val === null || val === undefined ? '' : val) formData = new FormData()
if (data) {
Object.entries(data).forEach(([key, val]) => {
if (val === null || val === undefined) return
// 支持 [Blob, filename] 元组格式,用于带文件名的文件上传
if (Array.isArray(val) && val.length === 2 && val[0] instanceof Blob) {
formData.append(key, val[0], val[1])
} else if (val instanceof Blob) {
formData.append(key, val)
} else {
formData.append(key, String(val))
}
}) })
} }
return searchParams.toString()
} }
// 不设置 Content-Type,让浏览器自动生成带 boundary 的 multipart/form-data
const isFormData = data instanceof FormData
const headers = { ...rest.headers } const headers = { ...rest.headers }
if (isFormData) {
delete headers['Content-Type'] delete headers['Content-Type']
delete headers['content-type'] delete headers['content-type']
} else { return alovaInst
headers['Content-Type'] = 'application/x-www-form-urlencoded' .Post<ResponseData<T>>(url, formData, {
}
try {
// 第一步:发送请求(可能是预检查 JSON,也可能是直接文件流)
const method = alovaInst.Post(url, getProcessedBody(data), {
...rest,
headers,
meta: { ...rest.meta, showLoading, isDownload: true }
})
const result = await method.send()
// 情况 A:返回的是 Response 对象(即已识别出的二进制流)
if (result instanceof Response) {
const blob = await result.blob()
this._triggerDownload(blob, fileName, result)
return true
}
// 情况 B:返回的是 JSON 对象(预检查成功)
const resData = result as any
if (resData && typeof resData === 'object' && (resData.code === 200 || resData.code === '200')) {
// 如果已经是带 down=Y 的请求返回了 JSON(可能是某些特殊接口),则不再重试
if (data?.down === 'Y') return true
// 自动执行第二步:携带 down=Y 获取正式文件流
const retryData = isFormData ? data : { ...data, down: 'Y' }
if (isFormData) {
;(retryData as FormData).append('down', 'Y')
}
const downloadMethod = alovaInst.Post(url, getProcessedBody(retryData), {
...rest, ...rest,
headers, headers,
meta: { ...rest.meta, showLoading: false, isDownload: true } meta: { ...rest.meta, showLoading, msgField }
}) })
const dlRes = await downloadMethod.send() .send(true)
if (dlRes instanceof Response) {
const blob = await dlRes.blob()
this._triggerDownload(blob, fileName, dlRes)
return true
}
}
return false
} catch (err) {
console.error('Download error:', err)
return false
}
},
/** 内部辅助:触发浏览器下载 */
_triggerDownload(blob: Blob, fileName?: string, response?: Response) {
let parsedFileName = fileName
if (!parsedFileName && response) {
const disposition = response.headers.get('content-disposition') || response.headers.get('Content-Disposition')
if (disposition) {
// 优先匹配 filename* (可能包含 UTF-8 编码格式)
const filenameStarRegex = /filename\*=utf-8''([^;\n]*)/i
const starMatches = filenameStarRegex.exec(disposition)
if (starMatches && starMatches[1]) {
try {
parsedFileName = decodeURIComponent(starMatches[1])
} catch (e) {
// Ignore
}
} else {
// 兜底匹配普通 filename
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
const matches = filenameRegex.exec(disposition)
if (matches && matches[1]) {
parsedFileName = matches[1].replace(/['"]/g, '')
try {
parsedFileName = decodeURIComponent(parsedFileName)
} catch (e) {
// Ignore
}
// 核心修复:如果文件名是 ISO-8859-1 编码的 UTF-8 字节串(常见于 Java 后端导出),则进行转换
if (parsedFileName && !/[^\x00-\xff]/.test(parsedFileName)) {
try {
const bytes = new Uint8Array(parsedFileName.split('').map((c) => c.charCodeAt(0)))
parsedFileName = new TextDecoder('utf-8').decode(bytes)
} catch (e) {
// Ignore
}
}
}
}
}
}
const downloadUrl = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = downloadUrl
a.download = parsedFileName || `export_${new Date().getTime()}.xlsx`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(downloadUrl)
document.body.removeChild(a)
}, },
// 对外暴露 alova 实例,以便使用 useRequest 或者其他 hook // 对外暴露 alova 实例,以便使用 useRequest 或者其他 hook
alova: alovaInst alova: alovaInst
......
...@@ -466,7 +466,10 @@ const importPrefs = async () => { ...@@ -466,7 +466,10 @@ const importPrefs = async () => {
const clearAndLogout = async () => { const clearAndLogout = async () => {
try { try {
await window.$dialog.warning('确认清空并退出', '确定要清空所有本地缓存并退出登录吗?此操作不可逆。') await window.$dialog.warning({
title: '确认清空并退出',
content: '确定要清空所有本地缓存并退出登录吗?此操作不可逆。'
})
localStorage.clear() localStorage.clear()
router.push('/login') router.push('/login')
window.$message.success('已清空并退出') window.$message.success('已清空并退出')
......
...@@ -10,7 +10,6 @@ import { setupNaiveDefaults } from '@/plugins/naive-ui-defaults' ...@@ -10,7 +10,6 @@ import { setupNaiveDefaults } from '@/plugins/naive-ui-defaults'
// 设置 Naive UI 组件默认属性 // 设置 Naive UI 组件默认属性
setupNaiveDefaults() setupNaiveDefaults()
import { setupNaiveDiscreteApi } from '@/utils/naive'
const app = createApp(App) const app = createApp(App)
......
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
import type { EditorState } from './types' import type { EditorState } from './types'
import { createDefaultAttributes, canAddChild } from '@/utils/dtdManager'
import { parseXmlToTree } from '@/utils/xmlParser'
import { useAppStore } from '@/store/app' import { useAppStore } from '@/store/app'
import { CHINESE_FIRST_PARA_TAGS } from '@/configs/xmlTags' import { CHINESE_FIRST_PARA_TAGS } from '@/configs/xmlTags'
......
...@@ -24,7 +24,6 @@ ...@@ -24,7 +24,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { SettingsOutline } from '@vicons/ionicons5' import { SettingsOutline } from '@vicons/ionicons5'
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
import { getElementAttributes } from '@/utils/dtdManager'
import { useAttributeEditor } from './functionals' import { useAttributeEditor } from './functionals'
const props = defineProps<{ const props = defineProps<{
......
import { useEditorStore } from '@/store/editor' import { useEditorStore } from '@/store/editor'
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
import { serializeTreeToXml } from '@/utils/xmlParser'
import { getElementRule, isMixedContentElement } from '@/utils/dtdManager'
import type { EditAreaContextMenuProps } from '../constants' import type { EditAreaContextMenuProps } from '../constants'
// 导入共享弹窗状态 // 导入共享弹窗状态
......
...@@ -311,6 +311,9 @@ export function useEditorPanel() { ...@@ -311,6 +311,9 @@ export function useEditorPanel() {
return return
} }
el.scrollIntoView({ behavior: 'auto', block: 'center' }) el.scrollIntoView({ behavior: 'auto', block: 'center' })
if (foundNodeId && foundNodeId !== editorStore.selectedNodeId) {
editorStore.setSelectedNodeId(foundNodeId)
}
return return
} }
...@@ -344,6 +347,9 @@ export function useEditorPanel() { ...@@ -344,6 +347,9 @@ export function useEditorPanel() {
} }
if (targetEl) { if (targetEl) {
targetEl.scrollIntoView({ behavior: 'auto', block: 'center' }) targetEl.scrollIntoView({ behavior: 'auto', block: 'center' })
if (subFoundId && subFoundId !== editorStore.selectedNodeId) {
editorStore.setSelectedNodeId(subFoundId)
}
} }
}, 100) }, 100)
}) })
......
...@@ -73,6 +73,22 @@ export function useBatchTranslate() { ...@@ -73,6 +73,22 @@ export function useBatchTranslate() {
} }
} }
const cancelTranslation = () => {
if (!loading.value) return
if (pollInterval) {
clearInterval(pollInterval)
pollInterval = null
}
loading.value = false
completed.value = false
progress.value = 0
resultFileId.value = ''
resultDownloadUrl.value = ''
progressText.value = '正在准备翻译文档...'
addLog('翻译已取消,返回配置页面')
window.$message.info('已取消翻译')
}
const open = async () => { const open = async () => {
showModal.value = true showModal.value = true
if (loading.value || completed.value) { if (loading.value || completed.value) {
...@@ -80,7 +96,7 @@ export function useBatchTranslate() { ...@@ -80,7 +96,7 @@ export function useBatchTranslate() {
} }
reset() reset()
try { try {
const res = (await service.get('/xml/tags')) as any as { success: boolean; message: string; tags: string[] } const res = await service.get('/xml/tags')
if (res && res.success && res.tags && res.tags.length > 0) { if (res && res.success && res.tags && res.tags.length > 0) {
availableTags.value = res.tags availableTags.value = res.tags
// 自动勾选所有获取到的标签 // 自动勾选所有获取到的标签
...@@ -98,7 +114,7 @@ export function useBatchTranslate() { ...@@ -98,7 +114,7 @@ export function useBatchTranslate() {
pollInterval = setInterval(async () => { pollInterval = setInterval(async () => {
try { try {
const statusRes = (await service.get(`/translation/status/${fileId}`)) as any as TranslationStatusResponse const statusRes = await service.get(`/translation/status/${fileId}`)
if (statusRes) { if (statusRes) {
if (statusRes.status === 'completed') { if (statusRes.status === 'completed') {
clearInterval(pollInterval) clearInterval(pollInterval)
...@@ -159,28 +175,19 @@ export function useBatchTranslate() { ...@@ -159,28 +175,19 @@ export function useBatchTranslate() {
addLog(`文档序列化完成,大小: ${Math.round(blob.size / 1024)} KB。准备上传...`) addLog(`文档序列化完成,大小: ${Math.round(blob.size / 1024)} KB。准备上传...`)
addLog(`请求配置: 模型 = ${form.value.model_name}, 目标标签 = [${form.value.target_tags.join(', ')}]`) addLog(`请求配置: 模型 = ${form.value.model_name}, 目标标签 = [${form.value.target_tags.join(', ')}]`)
const formData = new FormData() const res = await service.postForm('/translate/xml', {
formData.append('files', blob, 'document.xml') query_partition_names: import.meta.env.VITE_PARTITION_NAME,
if (form.value.model_name) { files: [blob, 'document.xml'],
formData.append('model_name', form.value.model_name) ...(form.value.model_name ? { model_name: form.value.model_name } : {}),
} search_direction: form.value.search_direction,
formData.append('search_direction', form.value.search_direction) target_tags: form.value.target_tags.join(','),
formData.append('target_tags', form.value.target_tags.join(',')) ...(form.value.suffix ? { suffix: form.value.suffix } : {}),
if (form.value.suffix) { save_to_database: form.value.save_to_database,
formData.append('suffix', form.value.suffix) use_parallel_method: form.value.use_parallel_method,
} ...(form.value.save_to_database && form.value.database_partition_name
formData.append('save_to_database', String(form.value.save_to_database)) ? { database_partition_name: form.value.database_partition_name }
formData.append('use_parallel_method', String(form.value.use_parallel_method)) : {})
const dbPartition = form.value.save_to_database ? form.value.database_partition_name : undefined })
if (dbPartition) {
formData.append('database_partition_name', dbPartition)
}
const res = (await service.post('/translate/xml', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})) as any as TranslateXmlResponse
if (res && res.success && res.file_id) { if (res && res.success && res.file_id) {
resultFileId.value = res.file_id resultFileId.value = res.file_id
...@@ -259,6 +266,7 @@ export function useBatchTranslate() { ...@@ -259,6 +266,7 @@ export function useBatchTranslate() {
open, open,
reset, reset,
startTranslation, startTranslation,
cancelTranslation,
downloadResult, downloadResult,
importResult, importResult,
logs logs
......
...@@ -78,8 +78,9 @@ ...@@ -78,8 +78,9 @@
<template #footer> <template #footer>
<div class="flex justify-end gap-3 w-full"> <div class="flex justify-end gap-3 w-full">
<!-- 翻译中状态:允许隐入后台 --> <!-- 翻译中状态:允许隐入后台或取消 -->
<template v-if="loading"> <template v-if="loading">
<CommonButton type="error" secondary @click="cancelTranslation">取消翻译</CommonButton>
<CommonButton type="primary" secondary @click="showModal = false">后台运行 (隐藏)</CommonButton> <CommonButton type="primary" secondary @click="showModal = false">后台运行 (隐藏)</CommonButton>
</template> </template>
<!-- 翻译完成状态 --> <!-- 翻译完成状态 -->
...@@ -118,6 +119,7 @@ const { ...@@ -118,6 +119,7 @@ const {
open, open,
reset, reset,
startTranslation, startTranslation,
cancelTranslation,
downloadResult, downloadResult,
importResult, importResult,
logs logs
......
import { type ExtractResponse } from '../constants' import { type ExtractResponse } from '../constants'
import { useEditorStore } from '@/store/editor' import { useEditorStore } from '@/store/editor'
import { serializeTreeToXml } from '@/utils/xmlParser'
import { getBaseOrigin } from '@/utils/render'
export function useExtractTranslate() { export function useExtractTranslate() {
const editorStore = useEditorStore() const editorStore = useEditorStore()
......
import { type SearchResult, type SearchResponse, type SearchRequest } from '../constants' import { type SearchResult, type SearchResponse, type SearchRequest } from '../constants'
import { serviceManage } from '@/api'
import dayjs from 'dayjs' import dayjs from 'dayjs'
export function useSearchTranslate() { export function useSearchTranslate() {
......
...@@ -160,7 +160,7 @@ ...@@ -160,7 +160,7 @@
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<!-- 警示卡片:说明删除操作 --> <!-- 警示卡片:说明删除操作 -->
<div <div
class="bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-900 rounded-lg p-4 text-xs text-amber-800 dark:text-amber-300 flex items-start gap-2" class="bg-[color-mix(in_srgb,var(--warning-color)_10%,transparent)] border border-[color-mix(in_srgb,var(--warning-color)_25%,transparent)] rounded-lg p-4 text-xs text-warning flex items-start gap-2"
> >
<n-icon size="18" class="mt-0.5"><alert-circle-outline /></n-icon> <n-icon size="18" class="mt-0.5"><alert-circle-outline /></n-icon>
<div class="flex-1 flex flex-col gap-1"> <div class="flex-1 flex flex-col gap-1">
......
import { useEditorStore, createTableStructure } from '@/store/editor' import { useEditorStore, createTableStructure } from '@/store/editor'
import { useAppStore } from '@/store/app/index' import { useAppStore } from '@/store/app/index'
import { canAddChild } from '@/utils/dtdManager'
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
/** /**
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<div class="w-[1px] h-4 bg-divider mx-1 flex-shrink-0"></div> <div class="w-px h-5 flex-shrink-0 mx-2" style="background: #d0d0d0"></div>
<!-- 组 2:插入元素按钮组 --> <!-- 组 2:插入元素按钮组 -->
<div class="flex items-center gap-0.5 flex-shrink-0"> <div class="flex items-center gap-0.5 flex-shrink-0">
...@@ -51,9 +51,8 @@ ...@@ -51,9 +51,8 @@
</button> </button>
</div> </div>
<div class="w-[1px] h-4 bg-divider mx-1 flex-shrink-0"></div> <div class="w-px h-5 flex-shrink-0 mx-3" style="background: #d0d0d0"></div>
<!-- 组 3:翻译辅助组 -->
<div class="flex items-center gap-0.5 flex-shrink-0"> <div class="flex items-center gap-0.5 flex-shrink-0">
<button <button
type="button" type="button"
...@@ -94,7 +93,7 @@ ...@@ -94,7 +93,7 @@
</button> </button>
</div> </div>
<div class="w-[1px] h-4 bg-divider mx-1 flex-shrink-0"></div> <div class="w-px h-5 flex-shrink-0 mx-3" style="background: #d0d0d0"></div>
<!-- 组 4:文件管理与导入导出 --> <!-- 组 4:文件管理与导入导出 -->
<div class="flex items-center gap-0.5 flex-shrink-0"> <div class="flex items-center gap-0.5 flex-shrink-0">
...@@ -136,7 +135,7 @@ ...@@ -136,7 +135,7 @@
</button> </button>
</div> </div>
<div class="w-[1px] h-4 bg-divider mx-1 flex-shrink-0"></div> <div class="w-px h-5 flex-shrink-0 mx-2" style="background: #d0d0d0"></div>
<!-- 组 5:历史操作 (撤销 / 重做) --> <!-- 组 5:历史操作 (撤销 / 重做) -->
<div class="flex items-center gap-0.5 flex-shrink-0"> <div class="flex items-center gap-0.5 flex-shrink-0">
......
import type { FormInst } from 'naive-ui' import type { FormInst } from 'naive-ui'
import { getElementAttributes, createDefaultAttributes, isTextOnlyElement, isMixedContentElement } from '@/utils/dtdManager'
import { useEditorStore } from '@/store/editor' import { useEditorStore } from '@/store/editor'
import { useAppStore } from '@/store/app' import { useAppStore } from '@/store/app'
......
import { useEditorStore } from '@/store/editor' import { useEditorStore } from '@/store/editor'
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
import type { BlockedNodeDetail, SafeNodeItem } from '../../../constants' import type { BlockedNodeDetail, SafeNodeItem } from '../../../constants'
import { canDeleteChild } from '@/utils/dtdManager'
export function useBatchDeleteConfirmModal(emit: (event: 'confirm') => void) { export function useBatchDeleteConfirmModal(emit: (event: 'confirm') => void) {
const editorStore = useEditorStore() const editorStore = useEditorStore()
......
...@@ -19,8 +19,6 @@ import { ...@@ -19,8 +19,6 @@ import {
import { useEditorStore } from '@/store/editor' import { useEditorStore } from '@/store/editor'
import { useAppStore } from '@/store/app' import { useAppStore } from '@/store/app'
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
import { serializeTreeToXml } from '@/utils/xmlParser'
import { isMixedContentElement, isTextOnlyElement, sortChildrenByDtd } from '@/utils/dtdManager'
import type { CheckRuleData, InsertMode, FlatNode, TranslationResponse } from '../constants' import type { CheckRuleData, InsertMode, FlatNode, TranslationResponse } from '../constants'
import { DOCUMENT_LIKE_TAGS } from '../constants' import { DOCUMENT_LIKE_TAGS } from '../constants'
import { STRUCTURAL_TAGS, WARNING_LIKE_TAGS, NOTE_AND_REF_TAGS, COLORED_TAGS } from '@/configs/xmlTags' import { STRUCTURAL_TAGS, WARNING_LIKE_TAGS, NOTE_AND_REF_TAGS, COLORED_TAGS } from '@/configs/xmlTags'
......
export interface SplitterProps {
width: number
collapsed: boolean
collapseThreshold?: number
defaultWidth?: number
maxRatio?: number
}
export interface SplitterProps { import { type SplitterProps } from '../constants'
width: number
collapsed: boolean
collapseThreshold?: number
defaultWidth?: number
maxRatio?: number
}
export function useSplitter( export function useSplitter(
props: SplitterProps, props: SplitterProps,
......
import { useEditorStore } from '@/store/editor' import { useEditorStore } from '@/store/editor'
import { loadDtdSchema, getElementRule } from '@/utils/dtdManager'
import { parseXmlToTreeAsync, serializeTreeToXml } from '@/utils/xmlParser'
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
import { DEFAULT_FILE_NAME } from '../constants' import { DEFAULT_FILE_NAME } from '../constants'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment