Commit c65b23cc by pangchong

feat(dtd): 支持上传和切换自定义DTD规范文件

- 新增DTD状态管理仓库,支持保存和切换用户上传的自定义DTD
- 实现DTD文件的上传流程,支持语法解析和校验
- 提供恢复默认DTD(系统内置JOBCARD.dtd)功能
- 编辑器工具栏新增上传DTD按钮及相关交互菜单
- 优化DTD解析器,支持解析完整DTD规范内容及元素属性定义
- 实现DTD规则动态加载与应用,提高元素约束校验灵活性
- 自定义DTD内容存储在本地,刷新页面依然生效
- 更新类型声明,支持.dtd文件模块导入
- 文档FAQ新增上传自定义DTD文件的说明与操作步骤
parent afe8d920
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -1012,6 +1012,28 @@ export const FAQ_DATABASE: FaqItem[] = [
isHot: true
},
{
id: 'faq_upload_dtd',
category: 'file',
categoryName: '文件与导出',
title: '如何上传自定义 DTD 规范文件?',
keywords: ['dtd', '上传dtd', '导入dtd', '切换dtd', 'dtd规范', '自定义dtd', '恢复默认dtd', 'scdtd', 'drdtd', 'hfmrdtd'],
synonyms: ['怎么上传dtd', '自定义dtd规范', '怎么切换dtd', '换一个dtd文件', '恢复默认dtd'],
answer: '系统支持上传自定义的 DTD 规范文件。上传后将优先采用新 DTD 规则进行元素约束校验;若未上传或恢复默认,则自动使用系统内置的 JOBCARD.dtd 规范。',
steps: [
'点击顶部工具栏的【上传 DTD】按钮(或鼠标悬浮展开管理菜单)。',
'在弹出的文件选择器中选择您的 `.dtd` 文件。',
'系统自动校验 DTD 语法并实时生效,后续节点添加、位置移动与 DTD 校验将全面遵循新规则。',
'如需恢复系统默认规则,可在【上传 DTD】悬浮菜单中点击【恢复默认 DTD】。'
],
highlights: [
'上传的自定义 DTD 会自动保存在浏览器本地,刷新页面依然有效。',
'若未上传或执行了恢复默认,系统将无缝回退至内置的 JOBCARD.dtd。'
],
actions: [{ label: '上传 DTD 文件', type: 'event', payload: 'trigger_upload_dtd' }],
relatedQuestions: ['faq_import_xml', 'faq_validate_xml'],
isHot: false
},
{
id: 'faq_export_xml',
category: 'file',
categoryName: '文件与导出',
......
import type { DtdState } from './types'
import defaultDtdRaw from '@/assets/json/JOBCARD.dtd?raw'
/**
* DTD 规则状态仓库 (Pinia)
* 管理用户上传的自定义 DTD 与系统默认 JOBCARD.dtd 之间的状态切换与持久化
*/
export const useDtdStore = defineStore('dtd', {
state: (): DtdState => ({
customDtdContent: null,
customDtdName: ''
}),
getters: {
/**
* 是否正在使用用户上传的自定义 DTD
*/
isCustomDtdActive: (state): boolean => Boolean(state.customDtdContent),
/**
* 当前生效的 DTD 文件名
*/
activeDtdName: (state): string => (state.customDtdContent ? state.customDtdName || '自定义.dtd' : 'JOBCARD.dtd')
},
actions: {
/**
* 上传并应用新的自定义 DTD
*/
setCustomDtd(content: string, fileName = 'custom.dtd') {
// 严格预校验 DTD 语法与 AST 内容
const parsed = parseDtd(content)
if (!parsed || Object.keys(parsed.elements || {}).length === 0) {
throw new Error('DTD 内容不合法或未能解析出任何 ELEMENT 声明')
}
this.customDtdContent = content
this.customDtdName = fileName
loadDtdSchema(content)
},
/**
* 恢复为系统内置默认 JOBCARD.dtd 规则
*/
resetDefaultDtd() {
this.customDtdContent = null
this.customDtdName = ''
loadDtdSchema(defaultDtdRaw)
},
/**
* 初始化加载 DTD(若 Pinia 状态中存在自定义 DTD 则优先应用,否则应用系统默认 DTD)
*/
initDtd() {
if (this.customDtdContent) {
try {
loadDtdSchema(this.customDtdContent)
return
} catch (err) {
console.warn('加载自定义 DTD 失败,回退到默认 DTD 规则:', err)
this.customDtdContent = null
this.customDtdName = ''
}
}
loadDtdSchema(defaultDtdRaw)
}
},
persist: true
})
export * from './types'
/**
* DTD 规则仓库状态类型定义
*/
export interface DtdState {
/** 用户上传的自定义 DTD 原始内容 (null 表示使用系统默认) */
customDtdContent: string | null
/** 用户上传的 DTD 文件名 */
customDtdName: string
}
......@@ -143,3 +143,13 @@ declare module 'naive-ui' {
colorBg5?: string
}
}
declare module '*.dtd' {
const content: string
export default content
}
declare module '*.dtd?raw' {
const content: string
export default content
}
......@@ -3,17 +3,19 @@ import { VIRTUAL_LAYOUT_TAGS } from '@/configs/xmlTags'
/**
* DTD 规则管理器
* 提供基于 dtd.json 的元素规则查询能力
* 提供基于 DTD 规范的元素规则查询能力
*/
let _schema: DtdSchema | null = null
/**
* 加载 DTD Schema
* 加载 DTD Schema(支持传入 DtdSchema 对象或 DTD 原文文本)
*/
export function loadDtdSchema(json: DtdSchema): void {
export function loadDtdSchema(input: DtdSchema | string): void {
const rawSchema = typeof input === 'string' ? parseDtd(input) : input
// 深度复制一份原始 schema 避免直接改变导入的数据
const schema = JSON.parse(JSON.stringify(json)) as DtdSchema
const schema = JSON.parse(JSON.stringify(rawSchema)) as DtdSchema
// 根据定制环境代号 VITE_CUSTOM_CODE 自动加载对应的 DTD 扩展配置
const customCode = import.meta.env.VITE_CUSTOM_CODE
......
import { useEditorStore, createTableStructure } from '@/store/editor'
import { useAppStore } from '@/store/app/index'
import { useDtdStore } from '@/store/dtd'
import type { XmlNode } from '@/types/xmlNode'
/**
......@@ -8,6 +9,7 @@ import type { XmlNode } from '@/types/xmlNode'
export function useEditorToolbar(emit: any) {
const editorStore = useEditorStore()
const appStore = useAppStore()
const dtdStore = useDtdStore()
const insertBelow = ref(true)
const fileInputRef = ref<HTMLInputElement | null>(null)
......@@ -202,6 +204,88 @@ export function useEditorToolbar(emit: any) {
}
}
const isUploadingDtd = ref(false)
const triggerUploadDtd = () => {
openUploadModal({
accept: '.dtd',
title: '上传 DTD 规范文件',
onSuccess: async (res, file) => {
let text = ''
if (file) {
text = await file.text()
} else if (res && typeof res === 'string') {
text = res
}
if (!text) {
window.$message.error('未获取到 DTD 文件内容')
return
}
isUploadingDtd.value = true
window.$loading?.show('解析并载入 DTD 规范中…')
try {
dtdStore.setCustomDtd(text, file?.name || 'custom.dtd')
window.$message.success(`DTD 规则上传成功!已切换并生效新规则(${file?.name || 'custom.dtd'})`)
} catch (err: any) {
window.$message.error('DTD 解析失败: ' + (err.message || '格式错误'))
} finally {
isUploadingDtd.value = false
window.$loading?.hide()
}
}
})
}
const handleRestoreDefaultDtd = async () => {
if (!dtdStore.isCustomDtdActive) {
window.$message.info('当前已在使用系统默认 JOBCARD.dtd 规则')
return
}
try {
await window.$dialog.warning({
title: '恢复默认 DTD',
content: `确定要清除已上传的自定义 DTD(${dtdStore.activeDtdName}),恢复为系统默认的 JOBCARD.dtd 规则吗?`
})
dtdStore.resetDefaultDtd()
window.$message.success('已恢复为系统默认 JOBCARD.dtd 规则!')
} catch {
// 用户取消无需处理
}
}
const dtdDropdownOptions = computed(() => {
return [
{
label: '上传新 DTD 文件 (.dtd)',
key: 'upload'
},
{
label: `当前规则: ${dtdStore.activeDtdName}${dtdStore.isCustomDtdActive ? ' (自定义)' : ' (系统默认)'}`,
key: 'info',
disabled: true
},
{
type: 'divider',
key: 'd1'
},
{
label: '恢复默认 DTD (JOBCARD.dtd)',
key: 'restore',
disabled: !dtdStore.isCustomDtdActive
}
]
})
const handleDtdDropdownSelect = (key: string) => {
if (key === 'upload') {
triggerUploadDtd()
} else if (key === 'restore') {
handleRestoreDefaultDtd()
}
}
const triggerUpload = () => {
openUploadModal({
accept: '.xml',
......@@ -268,8 +352,12 @@ export function useEditorToolbar(emit: any) {
}
}
const isCustomDtdActive = computed(() => dtdStore.isCustomDtdActive)
const activeDtdName = computed(() => dtdStore.activeDtdName)
return {
editorStore,
dtdStore,
appStore,
insertBelow,
fileInputRef,
......@@ -293,6 +381,12 @@ export function useEditorToolbar(emit: any) {
xmlSizeStr,
templateSelectModalRef,
isCurrentMerged,
handleToggleMerged
handleToggleMerged,
isUploadingDtd,
triggerUploadDtd,
isCustomDtdActive,
activeDtdName,
dtdDropdownOptions,
handleDtdDropdownSelect
}
}
......@@ -389,6 +389,27 @@
<!-- 组 4:文件管理与导入导出 -->
<div class="flex items-center gap-0.5 flex-shrink-0">
<!-- 上传 / 管理 DTD 规则 -->
<n-dropdown trigger="hover" :options="dtdDropdownOptions" @select="handleDtdDropdownSelect">
<button
type="button"
class="toolbar-btn flex items-center gap-1.5 px-2.5 h-7 rounded text-color2 hover:bg-fill-2 active:bg-fill-3 transition-colors select-none focus:outline-none flex-shrink-0 disabled:opacity-50"
:disabled="isUploadingDtd"
@click="triggerUploadDtd"
>
<n-icon class="text-base" :class="isCustomDtdActive ? 'text-primary' : ''">
<sync-outline v-if="isUploadingDtd" class="animate-spin" />
<document-text-outline v-else />
</n-icon>
<span class="text-xs font-medium">{{ isUploadingDtd ? '解析中…' : '上传 DTD' }}</span>
<span
v-if="isCustomDtdActive"
class="w-1.5 h-1.5 rounded-full bg-primary"
title="当前正在生效用户自定义 DTD 规则"
></span>
</button>
</n-dropdown>
<button
type="button"
class="toolbar-btn flex items-center gap-1.5 px-2.5 h-7 rounded text-color2 hover:bg-fill-2 active:bg-fill-3 transition-colors select-none focus:outline-none flex-shrink-0 disabled:opacity-50"
......@@ -555,7 +576,8 @@ import {
SettingsOutline,
GitCompareOutline,
ArchiveOutline,
GitMergeOutline
GitMergeOutline,
DocumentTextOutline
} from '@vicons/ionicons5'
import { useEditorToolbar } from './functionals'
import { GREEN_BUTTONS } from './constants'
......@@ -601,7 +623,13 @@ const {
xmlSizeStr,
templateSelectModalRef,
isCurrentMerged,
handleToggleMerged
handleToggleMerged,
isUploadingDtd,
triggerUploadDtd,
isCustomDtdActive,
activeDtdName,
dtdDropdownOptions,
handleDtdDropdownSelect
} = useEditorToolbar(emit)
const insertFragmentModalRef = ref<any>(null)
......@@ -617,6 +645,7 @@ onMounted(() => {
eventBus.on('trigger_insert_selection', () => handleInsert('SELECTION'))
eventBus.on('trigger_insert_template', () => handleInsert('TEMPLATE'))
eventBus.on('trigger_insert_fragment', () => insertFragmentModalRef.value?.open(insertBelow.value))
eventBus.on('trigger_upload_dtd', () => triggerUploadDtd())
eventBus.on('trigger_import_xml', () => triggerUpload())
eventBus.on('trigger_export_xml', () => emit('export'))
eventBus.on('trigger_preview', () => emit('preview'))
......
import { useEditorStore } from '@/store/editor'
import { useDtdStore } from '@/store/dtd'
import type { XmlNode } from '@/types/xmlNode'
// 静态导入 DTD JSON
import dtdJson from '@/assets/json/dtd.json'
import { VIRTUAL_LAYOUT_TAGS } from '@/configs/xmlTags'
import { DEFAULT_XML_FILE } from '@/configs'
import { currentJobCardMainData } from '../constants'
......@@ -17,6 +16,7 @@ const xmlModules = import.meta.glob(['../../../assets/file/*.xml', '../../../*/a
*/
export function useEditor() {
const store = useEditorStore()
const dtdStore = useDtdStore()
const cleanupFns: (() => void)[] = []
onUnmounted(() => {
......@@ -26,7 +26,7 @@ export function useEditor() {
const initialize = async (): Promise<void> => {
window.$loading?.show('加载数据中…')
try {
loadDtdSchema(dtdJson as any)
dtdStore.initDtd()
const customCode = import.meta.env.VITE_CUSTOM_CODE
const customXmlKey = customCode ? `../../../${customCode}/assets/file/${DEFAULT_XML_FILE}` : ''
......
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