Commit 50321a59 by pangchong

chore(deps): 添加 @types/diff 类型定义依赖

- 在 package.json 和 package-lock.json 中新增 @types/diff 依赖
- 确保开发环境对 diff 类型的支持和代码智能提示
- 安装包版本为 7.0.2,许可证为 MIT
parent 8128cbff
......@@ -36,6 +36,7 @@
"devDependencies": {
"@commitlint/cli": "^19.8.0",
"@commitlint/config-conventional": "^19.8.0",
"@types/diff": "^7.0.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.14",
"@vitejs/plugin-vue": "^6.0.2",
......@@ -1667,6 +1668,13 @@
"@types/node": "*"
}
},
"node_modules/@types/diff": {
"version": "7.0.2",
"resolved": "https://registry.npmmirror.com/@types/diff/-/diff-7.0.2.tgz",
"integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
......
......@@ -45,6 +45,7 @@
"devDependencies": {
"@commitlint/cli": "^19.8.0",
"@commitlint/config-conventional": "^19.8.0",
"@types/diff": "^7.0.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.14",
"@vitejs/plugin-vue": "^6.0.2",
......
This source diff could not be displayed because it is too large. You can view the blob instead.
<template>
<CommonModal v-model="show" title="导出确认" :width="500">
<div class="p-4 py-6">
<n-form-item label="选择导出范围" label-placement="left">
<n-radio-group v-model:value="exportMode" name="exportMode">
<n-space>
<n-radio value="page">当前页数据</n-radio>
<n-radio value="all">
全量数据
<n-text depth="3" class="text-xs ml-1">(最多 2,147,483,647 条)</n-text>
</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-alert v-if="exportMode === 'all'" title="提示" type="info" class="mt-4">全量导出可能会耗时较长,具体取决于系统数据总量。</n-alert>
<n-progress v-if="loading" type="line" :percentage="exportProgress" :indicator-placement="'inside'" processing class="mt-4" />
</div>
<template #footer>
<n-space justify="end">
<CommonButton @click="show = false" :disabled="loading">取消</CommonButton>
<CommonButton type="primary" :loading="loading" @click="handleConfirm">开始导出</CommonButton>
</n-space>
</template>
</CommonModal>
</template>
<script setup lang="ts">
const show = ref(false)
const loading = ref(false)
const exportProgress = ref(0)
const exportMode = ref<'page' | 'all'>('page')
let progressTimer: any = null
// 导出上下文
const context = ref<{
fileName: string
functionCode: string
params: any
} | null>(null)
/**
* 打开导出选择弹窗
* @param options 导出配置
*/
const open = (options: { fileName: string; functionCode: string; params: any }) => {
context.value = options
exportMode.value = 'page' // 默认当前页
show.value = true
}
/** 执行下载 */
const handleConfirm = async () => {
if (!context.value) return
loading.value = true
exportProgress.value = 0
// 开启模拟进度条定时器
progressTimer = setInterval(() => {
if (exportProgress.value < 90) {
exportProgress.value += Math.floor(Math.random() * 3) + 1
} else if (exportProgress.value < 98) {
exportProgress.value += 0.3
}
}, 200)
const { fileName, functionCode, params } = context.value
const exportParams = {
...params,
functionCode,
fileName,
page: exportMode.value === 'all' ? 1 : params.page || 1,
rows: exportMode.value === 'all' ? 2147483647 : params.rows || 15
}
try {
const success = await service.download('/excel/export', exportParams, `${fileName}_${new Date().getTime()}.xlsx`, {
showLoading: false
})
if (success) {
exportProgress.value = 100
window.$message.success('导出任务已启动,请查看浏览器下载')
// 延迟关闭,让用户看到 100% 进度
setTimeout(() => {
show.value = false
}, 500)
}
} catch (err) {
console.error('Export failed', err)
exportProgress.value = 0
} finally {
loading.value = false
if (progressTimer) {
clearInterval(progressTimer)
progressTimer = null
}
}
}
defineExpose({ open })
</script>
......@@ -27,26 +27,10 @@
</div>
<template #footer>
<div class="flex justify-between items-center w-full">
<CommonButton v-if="showDownload" type="primary" secondary @click="handleDownloadTemplate">
<template #icon>
<n-icon><DownloadOutline /></n-icon>
</template>
下载导入模板
</CommonButton>
<div v-else></div>
<n-space>
<CommonButton @click="showModal = false">取消</CommonButton>
<CommonButton
type="primary"
:loading="uploading"
:disabled="!importResult?.success && fileList.length === 0"
@click="handleConfirm"
>
{{ importResult?.success ? '完成' : '确定' }}
</CommonButton>
</n-space>
</div>
<CommonButton @click="showModal = false">取消</CommonButton>
<CommonButton type="primary" :loading="uploading" :disabled="!importResult?.success && fileList.length === 0" @click="handleConfirm">
{{ importResult?.success ? '完成' : '确定' }}
</CommonButton>
</template>
</CommonModal>
</template>
......@@ -78,7 +62,6 @@ const context = ref<ImportOptions | null>(null)
let progressTimer: any = null
const title = computed(() => context.value?.title || '批量导入')
const showDownload = computed(() => !!(context.value?.templateName || context.value?.templateApi))
/**
* 打开导入弹窗
......@@ -185,21 +168,6 @@ const handleConfirm = async () => {
}
}
const handleDownloadTemplate = () => {
if (!context.value?.templateName && !context.value?.templateApi) return
const params = {
...(context.value.templateName ? { filename: context.value.templateName } : {}),
...(context.value.downloadParams || {})
}
const downloadApi = context.value.templateApi || '/v1/plugins/ATTACHMENT_DOWN'
const fileName = context.value.templateTitle || '导入模板.xlsx'
openDownloadModal({
downloadFunc: () => service.download(downloadApi, params, fileName),
fileName,
title: '下载导入模板'
})
}
const emit = defineEmits(['success'])
defineExpose({
......
......@@ -9,7 +9,7 @@
:draggable="{ bounds: 'none' }"
v-bind="$attrs"
:style="modalStyle"
class="app-modal"
:class="['app-modal', { 'is-fullscreen': fullscreen }]"
header-class="!px-[20px] !py-[15px]"
content-class="!p-0"
footer-class="!m-0 !p-0"
......@@ -65,6 +65,7 @@ interface Props {
maxHeight?: string | number
padding?: string | number
scrollable?: boolean
fullscreen?: boolean
}
const props = withDefaults(defineProps<Props>(), {
......@@ -78,12 +79,22 @@ const props = withDefaults(defineProps<Props>(), {
width: '600px',
maxHeight: '75vh',
padding: '15px',
scrollable: true
scrollable: true,
fullscreen: false
})
const emit = defineEmits(['update:modelValue', 'confirm', 'cancel'])
const modalStyle = computed(() => {
if (props.fullscreen) {
return {
width: '100vw',
height: '100vh',
maxHeight: '100vh',
margin: 0,
borderRadius: 0
}
}
const w = props.width
return {
width: typeof w === 'number' ? `${w}px` : w
......@@ -104,7 +115,43 @@ const handleCancel = () => {
emit('cancel')
}
</script>
<style></style>
<style>
.app-modal.is-fullscreen {
width: 100vw !important;
height: 100vh !important;
max-height: 100vh !important;
margin: 0 !important;
border-radius: 0 !important;
display: flex !important;
flex-direction: column !important;
}
.app-modal.is-fullscreen > .n-card__content {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
min-height: 0 !important;
padding: 0 !important;
}
.app-modal.is-fullscreen > .n-card__content > .n-spin-container {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
min-height: 0 !important;
}
.app-modal.is-fullscreen > .n-card__content > .n-spin-container > .n-spin-content {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
min-height: 0 !important;
}
.app-modal.is-fullscreen > .n-card__content > .n-spin-container > .n-spin-content > .space-y-4 {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
min-height: 0 !important;
margin: 0 !important;
}
</style>
<style scoped>
:deep(.rule-op) {
@apply text-primary font-bold;
......
......@@ -11,7 +11,6 @@
<!-- 全局弹窗组件,供 window 全局挂载调用 -->
<CommonImportModal ref="globalImportModalRef" />
<CommonExportModal ref="globalExportModalRef" />
<CommonUploadModal ref="globalUploadModalRef" />
<CommonDownloadModal ref="globalDownloadModalRef" />
</div>
......@@ -19,13 +18,10 @@
<script setup lang="ts">
import { useAppStore } from '@/store/app/index'
import { useEditorStore } from '@/store/editor'
import CommonDownloadModal from '@/components/CommonDownloadModal.vue'
const themeVars = useThemeVars()
const appStore = useAppStore()
const globalImportModalRef = ref()
const globalExportModalRef = ref()
const globalAttachmentModalRef = ref()
const globalUploadModalRef = ref()
const globalPreviewModalRef = ref()
......@@ -36,7 +32,6 @@ useKeyboardShortcuts({}, true)
onMounted(() => {
window.$importModal = globalImportModalRef.value
window.$exportModal = globalExportModalRef.value
window.$attachmentModal = globalAttachmentModalRef.value
window.$uploadModal = globalUploadModalRef.value
window.$previewModal = globalPreviewModalRef.value
......
......@@ -242,13 +242,31 @@
<div class="text-sm font-semibold mb-3" :style="{ color: themeVars.textColor2 }">编辑器设置</div>
<!-- 自动展开新增节点 -->
<div class="flex items-center justify-between py-3" :style="{ borderColor: themeVars.dividerColor }">
<div class="flex items-center justify-between py-3 border-b" :style="{ borderColor: themeVars.dividerColor }">
<div>
<div class="text-sm" :style="{ color: themeVars.textColor1 }">插入节点自动展开</div>
<div class="text-xs mt-0.5" :style="{ color: themeVars.textColor3 }">插入/粘贴节点及表格行列时,自动展开相关节点</div>
</div>
<n-switch v-model:value="appStore.autoExpandOnInsert" />
</div>
<!-- 表格新增列默认宽度 -->
<div class="py-3" :style="{ borderColor: themeVars.dividerColor }">
<div class="flex items-center justify-between mb-1.5">
<span class="text-sm" :style="{ color: themeVars.textColor1 }">表格新增列默认宽度比</span>
<div class="w-32">
<n-input-number
v-model:value="appStore.defaultNewColWidth"
:min="0.05"
:max="2.0"
:step="0.05"
size="small"
placeholder="默认 0.2"
/>
</div>
</div>
<div class="text-xs" :style="{ color: themeVars.textColor3 }">设定新增列占常规平分列宽的比例或折算系数</div>
</div>
</div>
</n-tab-pane>
......@@ -410,6 +428,7 @@ const resetPrefs = () => {
appStore.collapsed = false
appStore.transitionName = 'none'
appStore.autoExpandOnInsert = true
appStore.defaultNewColWidth = 0.2
customColor.value = '#165DFF'
applyAndSave()
window.$message.success('已重置为默认设置')
......@@ -425,7 +444,8 @@ const copyPrefs = async () => {
colorWeak: appStore.colorWeak,
grayMode: appStore.grayMode,
transitionName: appStore.transitionName,
autoExpandOnInsert: appStore.autoExpandOnInsert
autoExpandOnInsert: appStore.autoExpandOnInsert,
defaultNewColWidth: appStore.defaultNewColWidth
},
null,
2
......@@ -456,6 +476,7 @@ const importPrefs = async () => {
if (typeof conf.grayMode === 'boolean') appStore.grayMode = conf.grayMode
if (conf.transitionName) appStore.transitionName = conf.transitionName
if (typeof conf.autoExpandOnInsert === 'boolean') appStore.autoExpandOnInsert = conf.autoExpandOnInsert
if (typeof conf.defaultNewColWidth === 'number') appStore.defaultNewColWidth = conf.defaultNewColWidth
applyAndSave()
window.$message.success('偏好设置已成功导入并应用')
......
......@@ -20,7 +20,8 @@ export const useAppStore = defineStore('app', {
transitionName: 'none',
settingsPinned: false,
settingsOpen: false,
autoExpandOnInsert: true
autoExpandOnInsert: true,
defaultNewColWidth: 0.2
}),
actions: {
toggleSidebar() {
......
......@@ -20,4 +20,5 @@ export interface AppState {
settingsPinned: boolean
settingsOpen: boolean
autoExpandOnInsert: boolean // 新增/粘贴节点及表格行列时自动展开
defaultNewColWidth: number // 表格新增列的默认宽度比例 (当前默认 0.2)
}
export interface DiffResult {
value: string
added?: boolean
removed?: boolean
count?: number
}
......@@ -17,6 +17,8 @@ export interface XmlNode {
mixedContent: MixedContentItem[]
/** 父节点 ID(不参与序列化) */
parentId: string | null
/** 比对差异状态 */
diffStatus?: 'added' | 'removed' | 'modified' | 'none'
}
/**
......@@ -30,6 +32,10 @@ export interface MixedContentItem {
text?: string
/** 子元素节点 ID(type=element 时) */
nodeId?: string
/** 比对差异状态 */
diffStatus?: 'added' | 'removed' | 'modified' | 'none'
/** 单词级差异详情 */
diffWords?: { value: string; added?: boolean; removed?: boolean }[]
}
/**
......
import * as Diff from 'diff'
import type { XmlNode } from '@/types/xmlNode'
import type { DiffResult } from '@/types/diff'
import { serializeTreeToXml } from './xmlParser'
/**
* 比较两份工卡(XML 字符串或 XmlNode 树结构)的行差异
* @param oldCard 旧工卡(可以是 XML 字符串或 XmlNode 树结构)
* @param newCard 新工卡(可以是 XML 字符串或 XmlNode 树结构)
* @returns 差异结果数组
*/
export function diffJobCardsLines(
oldCard: string | XmlNode,
newCard: string | XmlNode
): DiffResult[] {
const oldXml = typeof oldCard === 'string' ? oldCard : serializeTreeToXml(oldCard, 0, false)
const newXml = typeof newCard === 'string' ? newCard : serializeTreeToXml(newCard, 0, false)
return Diff.diffLines(oldXml, newXml)
}
/**
* 比较两份工卡(XML 字符串或 XmlNode 树结构)的单词差异
* @param oldCard 旧工卡
* @param newCard 新工卡
* @returns 差异结果数组
*/
export function diffJobCardsWords(
oldCard: string | XmlNode,
newCard: string | XmlNode
): DiffResult[] {
const oldXml = typeof oldCard === 'string' ? oldCard : serializeTreeToXml(oldCard, 0, false)
const newXml = typeof newCard === 'string' ? newCard : serializeTreeToXml(newCard, 0, false)
return Diff.diffWords(oldXml, newXml)
}
/**
* 比较两个 XmlNode 对象的 JSON 结构差异
* @param oldNode 旧节点
* @param newNode 新节点
* @returns 差异结果数组
*/
export function diffJobCardsJson(
oldNode: XmlNode,
newNode: XmlNode
): DiffResult[] {
// 序列化为规范 JSON 字符串,排除 parentId 以免产生循环引用
const replacer = (key: string, value: any) => {
if (key === 'parentId') return undefined
return value
}
const oldJson = JSON.stringify(oldNode, replacer, 2)
const newJson = JSON.stringify(newNode, replacer, 2)
return Diff.diffJson(oldJson, newJson)
}
......@@ -152,6 +152,19 @@ export function serializeTreeToXml(node: XmlNode, indent: number = 0, compact: b
}
/**
* 格式化 XML 字符串(转换为带换行和缩进的排版形式)
*/
export function formatXmlText(xmlStr: string): string {
if (!xmlStr) return ''
try {
const tree = parseXmlToTree(xmlStr)
return serializeTreeToXml(tree, 0, false)
} catch (e) {
return xmlStr
}
}
/**
* 转义 XML 属性值
*/
function escapeXmlAttr(str: string): string {
......
......@@ -496,3 +496,13 @@ export const isAllEinDataSameEffect = (einlst: XmlNode): boolean => {
const firstEff = getEffString(einlst.children[0])
return einlst.children.every((c) => getEffString(c) === firstEff)
}
export const getDiffWordClass = (w: { added?: boolean; removed?: boolean }, notItalic = false): string => {
if (w.added) {
return `bg-success-1 text-success-6 dark:text-success-5 font-bold border-b border-success-3 px-0.5 rounded${notItalic ? ' not-italic' : ''}`
}
if (w.removed) {
return `bg-danger-1 text-danger-6 dark:text-danger-5 line-through border-b border-danger-3 px-0.5 rounded${notItalic ? ' not-italic' : ''}`
}
return ''
}
export interface DiffMixedContentItem {
type: 'text' | 'element'
text?: string
nodeId?: string
diffStatus?: 'added' | 'removed' | 'modified' | 'none'
diffWords?: { value: string; added?: boolean; removed?: boolean }[]
}
export interface DiffXmlNode {
id: string
tagName: string
attributes: Record<string, string>
children: DiffXmlNode[]
textContent: string
mixedContent: DiffMixedContentItem[]
parentId: string | null
diffStatus: 'added' | 'removed' | 'modified' | 'none'
}
export interface DiffLine {
type: 'added' | 'removed' | 'normal' | 'empty'
num?: number
content: string
}
export const CELL_CHILD_OPTIONS = [
{ label: 'PARAC (中文)', value: 'PARAC' },
{ label: 'PARA (英文)', value: 'PARA' }
]
import type { FormInst, FormRules } from 'naive-ui'
import { CHINESE_FIRST_PARA_TAGS } from '@/configs/xmlTags'
export const useCreateTableModal = (
emit: (event: 'confirm', rows: number, cols: number, cellChildTags: string[]) => void
) => {
const show = ref(false)
const formRef = ref<FormInst | null>(null)
const form = reactive({
rows: 3,
cols: 3,
cellChildTags: [...CHINESE_FIRST_PARA_TAGS]
})
const rules: FormRules = {
rows: { type: 'number', required: true, message: '请指定行数', trigger: ['blur', 'change'] },
cols: { type: 'number', required: true, message: '请指定列数', trigger: ['blur', 'change'] }
}
const open = () => {
form.rows = 3
form.cols = 3
form.cellChildTags = [...CHINESE_FIRST_PARA_TAGS]
show.value = true
}
const handleConfirm = async () => {
try {
await formRef.value?.validate()
emit('confirm', form.rows, form.cols, form.cellChildTags)
show.value = false
} catch (err) {
// validation failed
}
}
return {
show,
formRef,
form,
rules,
open,
handleConfirm
}
}
......@@ -30,7 +30,7 @@
</div>
<n-form-item label="单元格初始化段落节点" path="cellChildTags">
<div class="w-full bg-fill-2 p-3 rounded border border-divider">
<CommonCheckbox v-model:value="form.cellChildTags" :options="cellChildOptions" :space-size="24" />
<CommonCheckbox v-model:value="form.cellChildTags" :options="CELL_CHILD_OPTIONS" :space-size="24" />
</div>
</n-form-item>
</div>
......@@ -39,48 +39,14 @@
</template>
<script setup lang="ts">
import type { FormInst, FormRules } from 'naive-ui'
import { CHINESE_FIRST_PARA_TAGS } from '@/configs/xmlTags'
import { CELL_CHILD_OPTIONS } from './constants'
import { useCreateTableModal } from './functionals'
const emit = defineEmits<{
confirm: [rows: number, cols: number, cellChildTags: string[]]
}>()
const cellChildOptions = [
{ label: 'PARAC (中文)', value: 'PARAC' },
{ label: 'PARA (英文)', value: 'PARA' }
]
const show = ref(false)
const formRef = ref<FormInst | null>(null)
const form = reactive({
rows: 3,
cols: 3,
cellChildTags: [...CHINESE_FIRST_PARA_TAGS]
})
const rules: FormRules = {
rows: { type: 'number', required: true, message: '请指定行数', trigger: ['blur', 'change'] },
cols: { type: 'number', required: true, message: '请指定列数', trigger: ['blur', 'change'] }
}
const open = () => {
form.rows = 3
form.cols = 3
form.cellChildTags = [...CHINESE_FIRST_PARA_TAGS]
show.value = true
}
const handleConfirm = async () => {
try {
await formRef.value?.validate()
emit('confirm', form.rows, form.cols, form.cellChildTags)
show.value = false
} catch (err) {
// validation failed
}
}
const { show, formRef, form, rules, open, handleConfirm } = useCreateTableModal(emit)
defineExpose({ open })
</script>
......@@ -91,6 +91,14 @@
<n-icon class="text-base"><search-outline /></n-icon>
<span class="text-xs font-medium">搜索翻译</span>
</button>
<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"
@click="compareModalRef?.open()"
>
<n-icon class="text-base"><git-compare-outline /></n-icon>
<span class="text-xs font-medium">对比工卡</span>
</button>
</div>
<div class="w-px h-5 flex-shrink-0 mx-3" style="background: #d0d0d0"></div>
......@@ -223,6 +231,9 @@
<!-- 搜索翻译数据库弹窗 -->
<SearchTranslateModal ref="searchTranslateModalRef" />
<!-- 工卡对比弹窗 -->
<CompareModal ref="compareModalRef" />
</div>
</template>
......@@ -241,7 +252,8 @@ import {
CodeWorkingOutline,
SyncOutline,
CheckmarkCircleOutline,
SettingsOutline
SettingsOutline,
GitCompareOutline
} from '@vicons/ionicons5'
import { useEditorToolbar } from './functionals'
import { GREEN_BUTTONS } from './constants'
......@@ -252,6 +264,7 @@ import CreateSignoffModal from './components/CreateSignoffModal/index.vue'
import BatchTranslateModal from './components/BatchTranslateModal/index.vue'
import ExtractTranslateModal from './components/ExtractTranslateModal/index.vue'
import SearchTranslateModal from './components/SearchTranslateModal/index.vue'
import CompareModal from './components/CompareModal/index.vue'
const emit = defineEmits(['save', 'validate', 'export', 'preview', 'download-html'])
......@@ -276,6 +289,7 @@ const {
} = useEditorToolbar(emit)
const insertFragmentModalRef = ref<any>(null)
const compareModalRef = ref<any>(null)
</script>
<style scoped>
......
import { viewXmlVisible, viewXmlTitle, viewXmlContent } from '../../../functionals'
export const useViewXmlModal = () => {
const handleCopyXml = async () => {
try {
await navigator.clipboard.writeText(viewXmlContent.value)
window.$message?.success('XML 片段已成功复制到剪贴板')
} catch (err) {
window.$message?.error('复制失败,请手动选择复制')
}
}
const open = (title: string, content: string) => {
viewXmlTitle.value = title
viewXmlContent.value = content
viewXmlVisible.value = true
}
return {
viewXmlVisible,
viewXmlTitle,
viewXmlContent,
handleCopyXml,
open
}
}
......@@ -25,22 +25,9 @@
<script setup lang="ts">
import { ClipboardOutline } from '@vicons/ionicons5'
import { viewXmlVisible, viewXmlTitle, viewXmlContent } from '../../functionals'
import { useViewXmlModal } from './functionals'
const handleCopyXml = async () => {
try {
await navigator.clipboard.writeText(viewXmlContent.value)
window.$message?.success('XML 片段已成功复制到剪贴板')
} catch (err) {
window.$message?.error('复制失败,请手动选择复制')
}
}
const open = (title: string, content: string) => {
viewXmlTitle.value = title
viewXmlContent.value = content
viewXmlVisible.value = true
}
const { viewXmlVisible, viewXmlTitle, viewXmlContent, handleCopyXml, open } = useViewXmlModal()
defineExpose({ open })
</script>
......
......@@ -420,15 +420,9 @@ export function useNodeTree(
return lines
})
// 仅渲染可视区域的垂直虚线
// 渲染所有计算出的垂直虚线,防止视口尺寸监控失效或滚动缓冲区外节点连线断裂
const visibleVerticalLines = computed(() => {
const sTop = scrollTop.value
const vHeight = viewportHeight.value
const sBottom = sTop + vHeight
return verticalLines.value.filter((line) => {
const lineBottom = line.top + line.height
return lineBottom >= sTop && line.top <= sBottom
})
return verticalLines.value
})
// 展开/折叠逻辑
......
......@@ -56,7 +56,7 @@
</div>
<!-- 可见列表节点 -->
<div :style="{ transform: `translateY(${startOffset}px)` }" class="absolute top-0 left-0 right-0 space-y-0.5">
<div :style="{ transform: `translateY(${startOffset}px)` }" class="absolute top-0 left-0 right-0">
<div
v-for="item in visibleItems"
:key="item.id"
......
......@@ -45,6 +45,8 @@ const setDeepText = (node: XmlNode, text: string): void => {
export function useTableEditor(props: { node: XmlNode }) {
const store = useEditorStore()
const appStore = useAppStore()
const isDiffModeRaw = inject<any>('diffMode', false)
const isDiffMode = computed(() => typeof isDiffModeRaw === 'boolean' ? isDiffModeRaw : (isDiffModeRaw?.value ?? false))
const structure = ref<TableStructureModel>({
cols: 0,
......@@ -552,12 +554,44 @@ export function useTableEditor(props: { node: XmlNode }) {
}
const colSpecs = tgroup.children.filter((c) => c.tagName === 'COLSPEC')
// 计算新增列的自适应默认宽度,从偏好设置 store 中读取比重系数,防止权重失衡
const ratio = appStore.defaultNewColWidth ?? 0.2
let defaultWidth = `${ratio}*`
const activeWidths = colSpecs
.map((spec) => spec.attributes.COLWIDTH || spec.attributes.colwidth || '')
.filter((w) => !!w)
if (activeWidths.length > 0) {
const starWidths = activeWidths
.filter((w) => w.includes('*'))
.map((w) => parseFloat(w.replace('*', '')))
.filter((v) => !isNaN(v))
if (starWidths.length > 0) {
const avgStar = starWidths.reduce((sum, v) => sum + v, 0) / starWidths.length
// 基于偏好设置的比例,星号下限根据设定的比例安全限制
const starLimit = Math.max(0.05, ratio / 2)
defaultWidth = `${Math.max(starLimit, Math.round(avgStar * ratio * 10) / 10)}*`
} else {
const absWidths = activeWidths
.map((w) => parseFloat(w))
.filter((v) => !isNaN(v))
if (absWidths.length > 0) {
const avgAbs = absWidths.reduce((sum, v) => sum + v, 0) / absWidths.length
// 基于偏好设置的像素比例,下限为 20px
defaultWidth = `${Math.max(20, Math.round(avgAbs * ratio))}px`
}
}
}
const newColSpec: XmlNode = {
id: crypto.randomUUID(),
tagName: 'COLSPEC',
attributes: {
COLNAME: `col_temp_${Date.now()}`,
COLNUM: ''
COLNUM: '',
COLWIDTH: defaultWidth
},
children: [],
textContent: '',
......@@ -1008,7 +1042,22 @@ export function useTableEditor(props: { node: XmlNode }) {
allCells.push(...row.cells)
}
const matchedCell = allCells.find((c) => c.id === newId || c.paragraphs.some((p) => p.id === newId))
// 优先通过 nodeMap 向上回溯祖先链寻找最近的 ENTRY 节点,以支持嵌套复杂子节点时的精确定位
let entryId: string | null = null
if (allCells.some((c) => c.id === newId)) {
entryId = newId
} else {
let curr = store.nodeMap.get(newId)
while (curr) {
if (curr.node.tagName === 'ENTRY') {
entryId = curr.node.id
break
}
curr = curr.parent ? store.nodeMap.get(curr.parent.id) : undefined
}
}
const matchedCell = entryId ? allCells.find((c) => c.id === entryId) : null
if (matchedCell) {
if (selectedCellIds.value.includes(matchedCell.id)) {
return
......@@ -1078,31 +1127,7 @@ export function useTableEditor(props: { node: XmlNode }) {
}
const expectedArea = (maxCol - minCol + 1) * (maxRow - minRow + 1)
if (actualArea !== expectedArea) return false
const mask = Array.from({ length: rows }, () => Array(cols).fill(false))
for (const cell of cells) {
const cIdx = cell.colIdx ?? 0
const rIdx = cell.rowIdx ?? 0
const cSpan = cell.colspan ?? 1
const rSpan = cell.rowspan ?? 1
for (let dr = 0; dr < rSpan; dr++) {
for (let dc = 0; dc < cSpan; dc++) {
if (rIdx + dr < rows && cIdx + dc < cols) {
mask[rIdx + dr][cIdx + dc] = true
}
}
}
}
for (let r = minRow; r <= maxRow; r++) {
for (let c = minCol; c <= maxCol; c++) {
if (!mask[r][c]) return false
}
}
return true
return actualArea === expectedArea
})
const canSplitSelected = computed(() => {
......@@ -1122,6 +1147,8 @@ export function useTableEditor(props: { node: XmlNode }) {
}
if (selectedCellIds.value.length > 0) {
store.setSelectedNodeId(selectedCellIds.value[selectedCellIds.value.length - 1])
} else {
store.setSelectedNodeId(props.node.id)
}
} else {
selectedCellIds.value = [cell.id]
......@@ -1130,14 +1157,41 @@ export function useTableEditor(props: { node: XmlNode }) {
}
const handleCellClick = (cell: TableCellModel, e: MouseEvent) => {
const isCtrl = e.ctrlKey || e.metaKey
if (preventCellClick.value) return
// 如果是 Ctrl 键点击,多选逻辑已在捕获阶段处理,冒泡阶段直接忽略
if (isCtrl) return
selectCellLogic(cell, e)
}
const handleCellClickCapture = (cell: TableCellModel, e: MouseEvent) => {
const isCtrl = e.ctrlKey || e.metaKey
if (isDiffMode.value) return
if (preventCellClick.value) return
if (isCtrl) {
e.stopImmediatePropagation()
e.stopPropagation()
e.preventDefault()
selectCellLogic(cell, e)
}
}
const handleCellMousedownCapture = (e: MouseEvent) => {
const isCtrl = e.ctrlKey || e.metaKey
if (isDiffMode.value) return
if (isCtrl) {
e.preventDefault()
}
}
const handleParaClick = (para: any, cell: TableCellModel, e: MouseEvent) => {
const isCtrl = e.ctrlKey || e.metaKey
if (preventCellClick.value) return
store.setSelectedNodeId(para.id)
selectCellLogic(cell, e)
// 只有非 Ctrl 点击(单选)且单元格已被选中时,才将全局选中节点联动为该段落,避免多选时被段落选中状态干扰
if (!isCtrl && selectedCellIds.value.includes(cell.id)) {
store.setSelectedNodeId(para.id)
}
}
const handleCellBlur = (cellId: string, e: FocusEvent) => {
......@@ -1815,6 +1869,8 @@ export function useTableEditor(props: { node: XmlNode }) {
canSplitSelected,
selectedCells,
handleCellClick,
handleCellClickCapture,
handleCellMousedownCapture,
handleParaClick,
handleCellBlur,
handleRowAddSelect,
......
......@@ -81,7 +81,7 @@
:options="attrDef.enumValues.map((v: any) => ({ label: v, value: v }))"
size="tiny"
class="flex-1"
@update:value="syncMixedContent"
@change="syncMixedContent"
/>
<n-input v-else v-model:value="item.attributes[attrName]" size="tiny" class="flex-1" @input="syncMixedContent" />
</div>
......
......@@ -5,7 +5,7 @@ import { DEFAULT_FILE_NAME } from '../constants'
// 静态导入 DTD JSON
import dtdJson from '@/assets/json/dtd.json'
// 导入原始 XML 文本 (?raw)
import xmlText from '@/assets/file/AMEA-A282400-02-1_0_0.xml?raw'
import xmlText from '@/assets/file/AMEA-A282400-02-1_0_0_updated.xml?raw'
/**
* 工卡 XML 编辑器核心业务逻辑 Hook
......
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