@wanghe1995/docx-editor-ui-lite 1.0.3 → 1.0.4

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,23 @@
1
+ /* Material Icons Local Font */
2
+ @font-face {
3
+ font-family: 'Material Icons';
4
+ font-style: normal;
5
+ font-weight: 400;
6
+ src: url('./material-icons.woff2') format('woff2');
7
+ }
8
+
9
+ .material-icons {
10
+ font-family: 'Material Icons';
11
+ font-weight: normal;
12
+ font-style: normal;
13
+ font-size: 0.9em;
14
+ line-height: 1;
15
+ letter-spacing: normal;
16
+ text-transform: none;
17
+ display: inline-block;
18
+ white-space: nowrap;
19
+ word-wrap: normal;
20
+ direction: ltr;
21
+ -webkit-font-feature-settings: 'liga';
22
+ -webkit-font-smoothing: antialiased;
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wanghe1995/docx-editor-ui-lite",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "DocxEditor 精简版UI库(Lite)",
5
5
  "author": "wanghe",
6
6
  "license": "MIT",
@@ -20,6 +20,8 @@
20
20
  },
21
21
  "files": [
22
22
  "dist",
23
+ "static",
24
+ "fonts",
23
25
  "README.md",
24
26
  "package.json"
25
27
  ],
@@ -27,7 +29,7 @@
27
29
  "dev": "vite --config vite.config.ts",
28
30
  "clean": "node -e \"const fs = require('fs'); if(fs.existsSync('dist')) fs.rmSync('dist', {recursive: true})\"",
29
31
  "build": "npm run clean && tsc && vite build --config vite.config.ts --mode lib",
30
- "build:ui": "vite build --config vite.config.deploy.ts",
32
+ "build:ui": "vite build --config vite.config.ts --mode ui",
31
33
  "lint": "eslint .",
32
34
  "type:check": "tsc --noEmit"
33
35
  },
@@ -0,0 +1,115 @@
1
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2
+ html, body { height: 100%; overflow: auto; }
3
+ body { font-family: 'Microsoft YaHei', '微软雅黑', 'Google Sans', Roboto, Arial, sans-serif; background: #f1f3f4; display: flex; flex-direction: column; min-height: 100vh; }
4
+
5
+ .top-fixed-area { position: sticky; top: 0; z-index: 100; background: #fff; }
6
+
7
+ .menu-bar { display: flex; align-items: center; background: #fff; padding: 8px 12px; border-bottom: 1px solid #dadce0; }
8
+ .menu-bar .logo { width: 40px; height: 40px; margin-right: 12px; display: flex; align-items: center; justify-content: center; }
9
+ .menu-bar .doc-info { flex: 1; min-width: 0; }
10
+ .doc-title-row { display: flex; align-items: center; width: fit-content; max-width: 100%; }
11
+ .menu-bar .doc-title { font-size: 18px; font-weight: 500; color: #202124; border: none; outline: none; padding: 4px 8px; border-radius: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
12
+ .menu-bar .doc-title:hover { background: #f1f3f4; }
13
+ .menu-bar .menu-items { display: flex; gap: 4px; margin-top: 4px; }
14
+ .menu-bar .menu-item { padding: 4px 8px; font-size: 13px; color: #5f6368; cursor: pointer; border-radius: 4px; position: relative; }
15
+ .menu-bar .menu-item:hover { background: #f1f3f4; }
16
+
17
+ .dropdown-menu { display: none; position: absolute; top: 100%; left: 0; background: #fff; border: 1px solid #dadce0; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); min-width: 180px; z-index: 200; padding: 4px 0; }
18
+ .dropdown-menu.show { display: block; }
19
+ .dropdown-item { display: flex; align-items: center; justify-content: space-between; padding: 6px 16px; font-size: 13px; color: #3c4043; cursor: pointer; white-space: nowrap; }
20
+ .dropdown-item:hover { background: #edf2fa; }
21
+ .dropdown-item .shortcut-hint { font-size: 12px; color: #80868b; margin-left: 24px; }
22
+ .dropdown-separator { height: 1px; background: #dadce0; margin: 4px 0; }
23
+ .dropdown-item .sub-arrow { margin-left: auto; font-size: 12px; color: #80868b; }
24
+ .dropdown-submenu { display: none; position: absolute; left: 100%; top: 0; background: #fff; border: 1px solid #dadce0; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); min-width: 200px; z-index: 201; padding: 4px 0; }
25
+ .dropdown-item:hover > .dropdown-submenu { display: block; }
26
+
27
+ .toolbar { display: flex; align-items: center; background: #edf2fa; padding: 4px 12px; gap: 2px; flex-wrap: wrap; border-bottom: 1px solid #dadce0; }
28
+ .toolbar-divider { width: 1px; height: 24px; background: #dadce0; margin: 0 4px; }
29
+ .toolbar-btn { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; border: none; background: transparent; border-radius: 4px; cursor: pointer; font-size: 18px; color: #444746; position: relative; }
30
+ .toolbar-btn:hover { background: #d3e3fd; }
31
+ .toolbar-btn[title]:hover::after { content: attr(title); position: absolute; bottom: -28px; left: 50%; transform: translateX(-50%); background: #3c4043; color: #fff; font-size: 11px; padding: 4px 8px; border-radius: 4px; white-space: nowrap; z-index: 1000; }
32
+ .toolbar-select { height: 32px; padding: 0 8px; border: none; background: transparent; border-radius: 4px; font-size: 13px; color: #444746; cursor: pointer; outline: none; }
33
+ .toolbar-select:hover { background: #d3e3fd; }
34
+ .color-picker { width: 32px; height: 32px; padding: 4px; border: none; background: transparent; border-radius: 4px; cursor: pointer; }
35
+ .color-picker:hover { background: #d3e3fd; }
36
+
37
+ .body-area { display: flex; flex: 1; min-height: 0; }
38
+
39
+ .catalog-sidebar { width: 0; overflow: hidden; background: #fff; border-right: 1px solid #dadce0; transition: width 0.2s ease; flex-shrink: 0; margin-left: 8px; }
40
+ .catalog-sidebar.open { width: 300px; }
41
+ .catalog-sidebar-inner { height: 100%; display: flex; flex-direction: column; }
42
+ .catalog-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid #e8eaed; }
43
+ .catalog-header h3 { font-size: 14px; font-weight: 500; color: #202124; }
44
+ .catalog-close { background: none; border: none; cursor: pointer; color: #5f6368; font-size: 18px; padding: 2px; border-radius: 4px; }
45
+ .catalog-close:hover { background: #f1f3f4; }
46
+ .catalog-tree { flex: 1; overflow-y: auto; padding: 8px 0; }
47
+ .catalog-empty { padding: 20px 12px; font-size: 13px; color: #80868b; text-align: center; }
48
+
49
+ .catalog-node { padding: 4px 8px; cursor: pointer; border-radius: 4px; margin: 1px 8px; font-size: 13px; color: #3c4043; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
50
+ .catalog-node:hover { background: #edf2fa; }
51
+ .catalog-node.level-1 { font-weight: 500; font-size: 14px; }
52
+ .catalog-node.level-2 { padding-left: 20px; }
53
+ .catalog-node.level-3 { padding-left: 32px; }
54
+ .catalog-node.level-4 { padding-left: 44px; }
55
+ .catalog-node.level-5 { padding-left: 56px; }
56
+ .catalog-node.level-6 { padding-left: 68px; }
57
+
58
+ .editor-wrapper { display: flex; justify-content: center; padding: 20px; flex: 1; overflow-y: auto; min-height: 0; }
59
+ #editor-container { background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
60
+
61
+ .status-bar { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; border-top: 1px solid #dadce0; padding: 4px 12px; font-size: 12px; color: #5f6368; display: flex; justify-content: space-between; z-index: 50; align-items: center; }
62
+ .status-left { display: flex; align-items: center; gap: 12px; }
63
+ .status-right { display: flex; align-items: center; gap: 4px; }
64
+ .status-btn { background: none; border: none; color: #5f6368; font-size: 12px; cursor: pointer; padding: 2px 6px; border-radius: 3px; }
65
+ .status-btn:hover { background: #f1f3f4; }
66
+ .status-icon-btn { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; }
67
+ .status-divider { width: 1px; height: 14px; background: #dadce0; }
68
+ .zoom-display { min-width: 36px; text-align: center; font-size: 12px; color: #444746; }
69
+ .catalog-toggle-label { display: inline-flex; align-items: center; gap: 4px; cursor: pointer; user-select: none; }
70
+ .catalog-toggle-label input[type="checkbox"] { margin: 0; cursor: pointer; }
71
+ .catalog-toggle-label span { font-size: 12px; }
72
+ .paper-size-wrap { position: relative; }
73
+ .paper-size-dropdown { display: none; position: absolute; bottom: 100%; left: 0; background: #fff; border: 1px solid #dadce0; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); min-width: 180px; max-height: 240px; overflow-y: auto; z-index: 200; padding: 4px 0; margin-bottom: 4px; }
74
+ .paper-size-dropdown.show { display: block; }
75
+ .paper-size-item { padding: 6px 12px; font-size: 12px; color: #3c4043; cursor: pointer; white-space: nowrap; }
76
+ .paper-size-item:hover { background: #edf2fa; }
77
+ .paper-size-item.active { background: #d3e3fd; color: #1a73e8; font-weight: 500; }
78
+
79
+ .popup-panel { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.2); padding: 20px; z-index: 1000; min-width: 320px; }
80
+ .popup-panel.show { display: block; }
81
+ .popup-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.3); z-index: 999; }
82
+ .popup-overlay.show { display: block; }
83
+ .popup-title { font-size: 16px; font-weight: 500; margin-bottom: 16px; }
84
+ .popup-row { margin-bottom: 12px; }
85
+ .popup-row label { display: block; font-size: 13px; color: #5f6368; margin-bottom: 4px; }
86
+ .popup-row input, .popup-row select { width: 100%; padding: 8px 12px; border: 1px solid #dadce0; border-radius: 4px; font-size: 14px; }
87
+ .popup-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
88
+ .popup-btn { padding: 8px 20px; border: none; border-radius: 4px; font-size: 14px; cursor: pointer; }
89
+ .popup-btn-primary { background: #1a73e8; color: #fff; }
90
+ .popup-btn-primary:hover { background: #1557b0; }
91
+ .popup-btn-secondary { background: #f1f3f4; color: #3c4043; }
92
+ .popup-btn-secondary:hover { background: #e8eaed; }
93
+
94
+ .zoom-control { display: flex; align-items: center; gap: 4px; }
95
+ .zoom-value { font-size: 13px; color: #444746; min-width: 40px; text-align: center; }
96
+
97
+ .shortcuts-content { max-height: 60vh; overflow-y: auto; padding: 0 8px; }
98
+ .shortcut-group { margin-bottom: 20px; }
99
+ .shortcut-group h3 { margin: 0 0 10px 0; font-size: 15px; font-weight: 600; color: #202124; padding-bottom: 8px; border-bottom: 1px solid #e8eaed; }
100
+ .shortcut-row { display: flex; align-items: center; justify-content: space-between; font-size: 13px; color: #5f6368; padding: 3px 0; }
101
+ .shortcut-row kbd { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 22px; padding: 0 6px; background: #f1f3f4; border: 1px solid #dadce0; border-radius: 4px; font-size: 12px; font-family: inherit; color: #202124; margin: 0 2px; }
102
+
103
+ .save-indicator { display: inline-flex; align-items: center; font-size: 12px; color: #80868b; margin-left: 12px; }
104
+ .save-indicator.saving { color: #1a73e8; }
105
+
106
+ .import-notification { position: fixed; bottom: 60px; right: 20px; background: #fff; border: 1px solid #dadce0; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.15); padding: 16px; z-index: 500; min-width: 280px; display: none; }
107
+ .import-notification.show { display: block; }
108
+ .import-notification h4 { font-size: 14px; font-weight: 500; margin-bottom: 8px; color: #202124; }
109
+ .import-notification p { font-size: 12px; color: #5f6368; margin-bottom: 12px; }
110
+ .import-notification .import-actions { display: flex; gap: 8px; justify-content: flex-end; }
111
+ .import-notification .import-btn { padding: 6px 16px; border: none; border-radius: 4px; font-size: 13px; cursor: pointer; min-width: 64px; }
112
+ .import-notification .import-btn-primary { background: #1a73e8; color: #fff; }
113
+ .import-notification .import-btn-primary:hover { background: #1557b0; }
114
+ .import-notification .import-btn-secondary { background: #f1f3f4; color: #3c4043; }
115
+ .import-notification .import-btn-secondary:hover { background: #e8eaed; }
@@ -0,0 +1,418 @@
1
+ import { WordEditor, RowFlex, TitleLevel, ListType, ListStyle, parseDocx } from '../../src/editor/index.ts'
2
+
3
+ const testData = {
4
+ main: []
5
+ }
6
+
7
+ let documentMeta = {
8
+ id: 'DEU' + Array.from({length: 4}, () => Math.random().toString(16).slice(2).toUpperCase().padEnd(4, '0').slice(0, 4)).join(''),
9
+ name: '新建文档',
10
+ createdAt: new Date().toISOString(),
11
+ submittedAt: ''
12
+ }
13
+
14
+ let importModeResolver = null
15
+
16
+ const editor = new WordEditor({
17
+ container: '#editor-container',
18
+ data: testData,
19
+ options: {
20
+ width: 794,
21
+ height: 1123,
22
+ margins: [96, 120, 96, 120],
23
+ defaultFont: '微软雅黑',
24
+ defaultSize: 14,
25
+ marginIndicatorDisabled: true,
26
+ lineBreak: { disabled: false, color: '#AAAAAA' }
27
+ },
28
+ onPageChange: (pageNo) => {
29
+ const el = document.getElementById('status-page')
30
+ if (el) el.textContent = `第 ${pageNo} 页`
31
+ },
32
+ onChange: () => {
33
+ updateWordCount()
34
+ debounceRefreshCatalog()
35
+ },
36
+ onScaleChange: (scale) => {
37
+ const pct = `${Math.round(scale * 100)}%`
38
+ document.getElementById('zoom-value').textContent = pct
39
+ const statusZoom = document.getElementById('status-zoom')
40
+ if (statusZoom) statusZoom.textContent = pct
41
+ }
42
+ })
43
+
44
+ function updateWordCount() {
45
+ try {
46
+ const result = editor.command.getWordCount()
47
+ const count = result?.count ?? 0
48
+ document.getElementById('status-words').textContent = `${count} 字`
49
+ } catch (e) {
50
+ // ignore
51
+ }
52
+ }
53
+
54
+ setTimeout(() => updateWordCount(), 500)
55
+
56
+ window.editor = editor
57
+ window.RowFlex = RowFlex
58
+ window.TitleLevel = TitleLevel
59
+ window.ListType = ListType
60
+ window.ListStyle = ListStyle
61
+
62
+ window.execCmd = (cmd, ...args) => {
63
+ try {
64
+ focusEditorAgent()
65
+ editor.command[cmd](...args)
66
+ } catch (e) { console.error(`执行失败: ${cmd}`, e) }
67
+ }
68
+
69
+ function focusEditorAgent() {
70
+ try {
71
+ const container = document.getElementById('editor-container')
72
+ const agentDom = container?.querySelector('textarea')
73
+ if (agentDom && typeof agentDom.focus === 'function') {
74
+ agentDom.focus()
75
+ }
76
+ } catch (e) { /* ignore */ }
77
+ }
78
+
79
+ window.setRowFlex = (flex) => { editor.command.executeRowFlex(RowFlex[flex]) }
80
+ window.setTitle = (level) => {
81
+ if (!level) { editor.command.executeTitle(null); return }
82
+ const levels = {
83
+ '1': TitleLevel.FIRST, '2': TitleLevel.SECOND, '3': TitleLevel.THIRD,
84
+ '4': TitleLevel.FOURTH, '5': TitleLevel.FIFTH, '6': TitleLevel.SIXTH
85
+ }
86
+ editor.command.executeTitle(levels[level])
87
+ }
88
+ window.setList = (type) => {
89
+ if (type === 'OL') { editor.command.executeList(ListType.OL, ListStyle.DECIMAL) }
90
+ else { editor.command.executeList(ListType.UL, ListStyle.DISC) }
91
+ }
92
+ window.setLineHeight = (value) => { editor.command.executeLineHeight(parseFloat(value)) }
93
+
94
+ window.showPopup = (id) => { document.getElementById('popup-overlay').classList.add('show'); document.getElementById(id).classList.add('show') }
95
+ window.closePopup = () => { document.getElementById('popup-overlay').classList.remove('show'); document.querySelectorAll('.popup-panel').forEach(p => p.classList.remove('show')) }
96
+
97
+ window.insertTable = () => showPopup('table-popup')
98
+ window.confirmInsertTable = () => { const rows = parseInt(document.getElementById('table-rows').value) || 3; const cols = parseInt(document.getElementById('table-cols').value) || 4; editor.command.executeInsertTable(rows, cols); closePopup() }
99
+
100
+ window.insertHyperlink = () => showPopup('link-popup')
101
+ window.confirmInsertLink = () => { const text = document.getElementById('link-text').value; const url = document.getElementById('link-url').value; if (url) { editor.command.executeHyperlink({ type: 'hyperlink', value: text || url, url }); } closePopup() }
102
+
103
+ window.insertImage = () => {
104
+ const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'
105
+ input.onchange = (e) => {
106
+ const file = e.target.files[0]; if (!file) return
107
+ const reader = new FileReader()
108
+ reader.onload = (event) => {
109
+ const img = new Image(); img.onload = () => { const maxWidth = 400; const scale = img.width > maxWidth ? maxWidth / img.width : 1; editor.command.executeImage({ value: event.target.result, width: img.width * scale, height: img.height * scale }) }; img.src = event.target.result
110
+ }
111
+ reader.readAsDataURL(file)
112
+ }
113
+ input.click()
114
+ }
115
+
116
+ window.searchReplace = () => showPopup('search-popup')
117
+ window.doSearch = () => { const text = document.getElementById('search-text').value; if (text) { const result = editor.command.executeSearch(text); alert(`找到 ${result?.count || 0} 处匹配`) } }
118
+ window.doReplace = () => { const search = document.getElementById('search-text').value; const replace = document.getElementById('replace-text').value; if (search) { editor.command.executeReplace(replace) } }
119
+ window.doReplaceAll = () => { const search = document.getElementById('search-text').value; const replace = document.getElementById('replace-text').value; if (search) { editor.command.executeSearch(search); let count = 0; while (editor.command.executeReplace(replace)) { count++; if (count > 1000) break; } alert(`已替换 ${count} 处`) } }
120
+
121
+ window.showShortcutsDialog = () => showPopup('shortcuts-popup')
122
+
123
+ // 菜单下拉交互
124
+ let activeDropdown = null
125
+ document.addEventListener('click', (e) => {
126
+ if (activeDropdown) {
127
+ activeDropdown.classList.remove('show')
128
+ activeDropdown = null
129
+ }
130
+ const menuItem = e.target.closest('.menu-item')
131
+ if (menuItem) {
132
+ const dropdown = menuItem.querySelector('.dropdown-menu')
133
+ if (dropdown) {
134
+ e.stopPropagation()
135
+ if (dropdown !== activeDropdown) {
136
+ if (activeDropdown) activeDropdown.classList.remove('show')
137
+ dropdown.classList.add('show')
138
+ activeDropdown = dropdown
139
+ }
140
+ }
141
+ }
142
+ })
143
+
144
+ // 确保编辑器保持焦点(core快捷键监听在agentDom上)
145
+ const editorContainer = document.getElementById('editor-container')
146
+ if (editorContainer) {
147
+ const focusEditor = () => {
148
+ try {
149
+ const agentDom = editor.instance?.command?.__proxyGetAgentDom?.()
150
+ || editorContainer.querySelector('textarea')
151
+ if (agentDom && typeof agentDom.focus === 'function') {
152
+ agentDom.focus()
153
+ }
154
+ } catch (e) { /* ignore */ }
155
+ }
156
+
157
+ document.addEventListener('mousedown', (e) => {
158
+ const target = e.target
159
+ if (target && !editorContainer.contains(target) && !target.closest('.toolbar') && !target.closest('.menu-bar') && !target.closest('.popup-panel') && !target.closest('.status-bar') && !target.closest('.catalog-sidebar')) {
160
+ setTimeout(focusEditor, 0)
161
+ }
162
+ })
163
+ }
164
+
165
+ // 快捷键全局监听
166
+ const editorContainerEl = document.getElementById('editor-container')
167
+ document.addEventListener('keydown', (e) => {
168
+ // 如果事件已被core库处理(焦点在编辑器内),则跳过编辑器内置快捷键
169
+ const inEditor = editorContainerEl && editorContainerEl.contains(e.target)
170
+ const ctrl = e.ctrlKey || e.metaKey
171
+ const shift = e.shiftKey
172
+ const key = e.key.toLowerCase()
173
+
174
+ // 应用级快捷键(始终拦截)
175
+ if (ctrl && key === 's') { e.preventDefault(); handleSave(); return }
176
+ if (ctrl && key === 'p') { e.preventDefault(); execCmd('executePrint'); return }
177
+ if (ctrl && key === 'f') { e.preventDefault(); searchReplace(); return }
178
+ if (ctrl && key === 'h') { e.preventDefault(); searchReplace(); return }
179
+
180
+ // 编辑器内置快捷键:仅焦点不在编辑器时兜底处理
181
+ if (inEditor) return
182
+ if (ctrl && !shift && key === 'z') { e.preventDefault(); execCmd('executeUndo'); return }
183
+ if (ctrl && shift && key === 'z') { e.preventDefault(); execCmd('executeRedo'); return }
184
+ if (ctrl && key === 'y') { e.preventDefault(); execCmd('executeRedo'); return }
185
+ if (ctrl && key === 'b') { e.preventDefault(); execCmd('executeBold'); return }
186
+ if (ctrl && key === 'i') { e.preventDefault(); execCmd('executeItalic'); return }
187
+ if (ctrl && key === 'u') { e.preventDefault(); execCmd('executeUnderline'); return }
188
+ if (ctrl && key === 'k') { e.preventDefault(); insertHyperlink(); return }
189
+ if (ctrl && key === '\\') { e.preventDefault(); execCmd('executeFormat'); return }
190
+ if (ctrl && key === 'enter') { e.preventDefault(); execCmd('executePageBreak'); return }
191
+ })
192
+
193
+ // 保存功能
194
+ window.handleSave = () => {
195
+ const indicator = document.getElementById('save-indicator')
196
+ try {
197
+ const content = editor.command.getValue()
198
+ if (!content) return
199
+ const snapshot = { meta: { ...documentMeta }, content }
200
+ documentMeta.submittedAt = new Date().toISOString()
201
+ indicator.textContent = '保存中...'
202
+ indicator.classList.add('saving')
203
+ setTimeout(() => {
204
+ indicator.textContent = `已保存 ${formatTime(new Date())}`
205
+ indicator.classList.remove('saving')
206
+ console.log('[save] 保存快照:', snapshot)
207
+ }, 300)
208
+ } catch (e) {
209
+ indicator.textContent = '保存失败'
210
+ indicator.classList.remove('saving')
211
+ console.error('保存失败:', e)
212
+ }
213
+ }
214
+
215
+ function formatTime(date) {
216
+ return `${String(date.getHours()).padStart(2,'0')}:${String(date.getMinutes()).padStart(2,'0')}:${String(date.getSeconds()).padStart(2,'0')}`
217
+ }
218
+
219
+ // 导入文档功能
220
+ window.handleImportDoc = () => {
221
+ const input = document.createElement('input')
222
+ input.type = 'file'
223
+ input.accept = '.docx,.doc'
224
+ input.onchange = (e) => {
225
+ const file = e.target.files[0]
226
+ if (!file) return
227
+ const fileSize = file.size < 1024 * 1024
228
+ ? `${(file.size / 1024).toFixed(1)} KB`
229
+ : `${(file.size / 1024 / 1024).toFixed(1)} MB`
230
+
231
+ const notification = document.getElementById('import-notification')
232
+ document.getElementById('import-info').textContent = `${file.name} (${fileSize})`
233
+ notification.classList.add('show')
234
+
235
+ importModeResolver = (mode) => {
236
+ notification.classList.remove('show')
237
+ if (mode === 'cancel') return
238
+
239
+ const hex = () => Math.random().toString(16).slice(2).toUpperCase().padEnd(4, '0').slice(0, 4)
240
+ const newId = 'DEU' + hex() + hex() + hex() + hex()
241
+ documentMeta.id = newId
242
+ documentMeta.name = file.name.replace(/\.\w+$/, '')
243
+ const titleInput = document.getElementById('doc-title')
244
+ titleInput.value = documentMeta.name
245
+ titleInput.title = documentMeta.name
246
+
247
+ const reader = new FileReader()
248
+ reader.onload = async (event) => {
249
+ try {
250
+ const arrayBuffer = event.target.result
251
+ const result = await parseDocx(arrayBuffer)
252
+
253
+ if (!result.success || !result.elements || result.elements.length === 0) {
254
+ alert('文档解析失败: ' + (result.error || '未知错误'))
255
+ return
256
+ }
257
+
258
+ if (mode === 'overwrite') {
259
+ editor.command.executeSetValue({ main: result.elements })
260
+ } else {
261
+ const current = editor.command.getValue()
262
+ const currentElements = current?.data?.main || current?.main || []
263
+ const merged = [...currentElements, ...result.elements]
264
+ editor.command.executeSetValue({ main: merged })
265
+ }
266
+
267
+ updateWordCount()
268
+ refreshCatalog()
269
+ handleSave()
270
+ } catch (err) {
271
+ console.error('导入失败:', err)
272
+ alert('导入失败: ' + (err.message || '未知错误'))
273
+ }
274
+ }
275
+ reader.readAsArrayBuffer(file)
276
+ }
277
+ }
278
+ input.click()
279
+ }
280
+
281
+ window.handleImportAction = (mode) => {
282
+ if (importModeResolver) {
283
+ const resolver = importModeResolver
284
+ importModeResolver = null
285
+ resolver(mode)
286
+ }
287
+ }
288
+
289
+ // 文档标题同步
290
+ document.getElementById('doc-title').addEventListener('input', (e) => {
291
+ documentMeta.name = e.target.value
292
+ e.target.title = e.target.value
293
+ })
294
+
295
+ // 目录功能
296
+ const levelOrderMap = {
297
+ first: 1, second: 2, third: 3, fourth: 4, fifth: 5, sixth: 6
298
+ }
299
+
300
+ let catalogOpen = true
301
+ let catalogRefreshTimer = null
302
+
303
+ const debounceRefreshCatalog = () => {
304
+ if (catalogRefreshTimer) clearTimeout(catalogRefreshTimer)
305
+ catalogRefreshTimer = setTimeout(() => refreshCatalog(), 1000)
306
+ }
307
+
308
+ async function refreshCatalog() {
309
+ try {
310
+ const catalog = await editor.command.getCatalog()
311
+ renderCatalog(Array.isArray(catalog) ? catalog : [])
312
+ } catch (e) {
313
+ // ignore
314
+ }
315
+ }
316
+
317
+ function getLevelNumber(level) {
318
+ if (typeof level === 'number') return level
319
+ return levelOrderMap[level] || 1
320
+ }
321
+
322
+ function renderCatalog(catalog) {
323
+ const treeEl = document.getElementById('catalog-tree')
324
+ const emptyEl = document.getElementById('catalog-empty')
325
+
326
+ treeEl.querySelectorAll('.catalog-node').forEach(n => n.remove())
327
+
328
+ if (!catalog || catalog.length === 0) {
329
+ emptyEl.style.display = 'block'
330
+ return
331
+ }
332
+ emptyEl.style.display = 'none'
333
+
334
+ const renderItems = (items) => {
335
+ for (const item of items) {
336
+ const node = document.createElement('div')
337
+ node.className = `catalog-node level-${getLevelNumber(item.level)}`
338
+ node.textContent = item.name || ''
339
+ node.title = item.name || ''
340
+ if (item.id) {
341
+ node.addEventListener('click', () => {
342
+ try { editor.command.executeLocationCatalog(item.id) } catch (e) { console.error('定位失败:', e) }
343
+ })
344
+ }
345
+ treeEl.appendChild(node)
346
+ if (item.subCatalog && item.subCatalog.length > 0) {
347
+ renderItems(item.subCatalog)
348
+ }
349
+ }
350
+ }
351
+ renderItems(catalog)
352
+ }
353
+
354
+ window.toggleCatalog = (checked) => {
355
+ catalogOpen = typeof checked === 'boolean' ? checked : !catalogOpen
356
+ const sidebar = document.getElementById('catalog-sidebar')
357
+ const cb = document.getElementById('catalog-toggle-cb')
358
+ if (catalogOpen) {
359
+ sidebar.classList.add('open')
360
+ if (cb) cb.checked = true
361
+ refreshCatalog()
362
+ } else {
363
+ sidebar.classList.remove('open')
364
+ if (cb) cb.checked = false
365
+ }
366
+ }
367
+
368
+ // 初始化时打开目录并刷新
369
+ setTimeout(() => {
370
+ const sidebar = document.getElementById('catalog-sidebar')
371
+ if (sidebar) sidebar.classList.add('open')
372
+ refreshCatalog()
373
+ }, 800)
374
+
375
+ // 纸张方向切换
376
+ let paperDirection = 'vertical'
377
+ window.togglePaperDirection = () => {
378
+ paperDirection = paperDirection === 'vertical' ? 'horizontal' : 'vertical'
379
+ document.getElementById('paper-direction-btn').textContent = paperDirection === 'vertical' ? '纵向' : '横向'
380
+ try {
381
+ editor.command.executePaperDirection(paperDirection)
382
+ } catch (e) { console.error('切换纸张方向失败:', e) }
383
+ }
384
+
385
+ // 纸张大小选择
386
+ let paperSizeMenuOpen = false
387
+ window.togglePaperSizeMenu = () => {
388
+ paperSizeMenuOpen = !paperSizeMenuOpen
389
+ const dropdown = document.getElementById('paper-size-dropdown')
390
+ if (paperSizeMenuOpen) {
391
+ dropdown.classList.add('show')
392
+ } else {
393
+ dropdown.classList.remove('show')
394
+ }
395
+ }
396
+
397
+ document.addEventListener('click', (e) => {
398
+ if (paperSizeMenuOpen && !e.target.closest('.paper-size-wrap')) {
399
+ paperSizeMenuOpen = false
400
+ document.getElementById('paper-size-dropdown').classList.remove('show')
401
+ }
402
+ })
403
+
404
+ window.setPaperSize = (width, height, label) => {
405
+ document.getElementById('paper-size-btn').textContent = label
406
+ paperSizeMenuOpen = false
407
+ document.getElementById('paper-size-dropdown').classList.remove('show')
408
+ document.querySelectorAll('.paper-size-item').forEach(item => {
409
+ item.classList.toggle('active', item.dataset.label === label)
410
+ })
411
+ try {
412
+ const w = paperDirection === 'horizontal' ? height : width
413
+ const h = paperDirection === 'horizontal' ? width : height
414
+ editor.command.executePaperSize(w, h)
415
+ } catch (e) { console.error('设置纸张大小失败:', e) }
416
+ }
417
+
418
+ console.log('DocxEditor UI Lite 初始化成功!')