feat(new tool): markdown to word
This commit is contained in:
		
							parent
							
								
									8b807a8968
								
							
						
					
					
						commit
						1fd83e1ca9
					
				
							
								
								
									
										11
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -122,6 +122,7 @@ declare module '@vue/runtime-core' { | |||||||
|     MacAddressGenerator: typeof import('./src/tools/mac-address-generator/mac-address-generator.vue')['default'] |     MacAddressGenerator: typeof import('./src/tools/mac-address-generator/mac-address-generator.vue')['default'] | ||||||
|     MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default'] |     MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default'] | ||||||
|     MarkdownToHtml: typeof import('./src/tools/markdown-to-html/markdown-to-html.vue')['default'] |     MarkdownToHtml: typeof import('./src/tools/markdown-to-html/markdown-to-html.vue')['default'] | ||||||
|  |     MarkdownToWord: typeof import('./src/tools/markdown-to-word/markdown-to-word.vue')['default'] | ||||||
|     MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default'] |     MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default'] | ||||||
|     MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default'] |     MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default'] | ||||||
|     MenuBarItem: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue')['default'] |     MenuBarItem: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue')['default'] | ||||||
| @ -130,18 +131,28 @@ declare module '@vue/runtime-core' { | |||||||
|     MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default'] |     MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default'] | ||||||
|     MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] |     MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] | ||||||
|     NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] |     NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] | ||||||
|  |     NButton: typeof import('naive-ui')['NButton'] | ||||||
|     NCheckbox: typeof import('naive-ui')['NCheckbox'] |     NCheckbox: typeof import('naive-ui')['NCheckbox'] | ||||||
|  |     NCode: typeof import('naive-ui')['NCode'] | ||||||
|     NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] |     NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] | ||||||
|     NConfigProvider: typeof import('naive-ui')['NConfigProvider'] |     NConfigProvider: typeof import('naive-ui')['NConfigProvider'] | ||||||
|     NDivider: typeof import('naive-ui')['NDivider'] |     NDivider: typeof import('naive-ui')['NDivider'] | ||||||
|     NEllipsis: typeof import('naive-ui')['NEllipsis'] |     NEllipsis: typeof import('naive-ui')['NEllipsis'] | ||||||
|  |     NForm: typeof import('naive-ui')['NForm'] | ||||||
|  |     NFormItem: typeof import('naive-ui')['NFormItem'] | ||||||
|  |     NGi: typeof import('naive-ui')['NGi'] | ||||||
|  |     NGrid: typeof import('naive-ui')['NGrid'] | ||||||
|     NH1: typeof import('naive-ui')['NH1'] |     NH1: typeof import('naive-ui')['NH1'] | ||||||
|     NH3: typeof import('naive-ui')['NH3'] |     NH3: typeof import('naive-ui')['NH3'] | ||||||
|     NIcon: typeof import('naive-ui')['NIcon'] |     NIcon: typeof import('naive-ui')['NIcon'] | ||||||
|  |     NInputGroup: typeof import('naive-ui')['NInputGroup'] | ||||||
|     NLayout: typeof import('naive-ui')['NLayout'] |     NLayout: typeof import('naive-ui')['NLayout'] | ||||||
|     NLayoutSider: typeof import('naive-ui')['NLayoutSider'] |     NLayoutSider: typeof import('naive-ui')['NLayoutSider'] | ||||||
|     NMenu: typeof import('naive-ui')['NMenu'] |     NMenu: typeof import('naive-ui')['NMenu'] | ||||||
|  |     NScrollbar: typeof import('naive-ui')['NScrollbar'] | ||||||
|  |     NSlider: typeof import('naive-ui')['NSlider'] | ||||||
|     NSpace: typeof import('naive-ui')['NSpace'] |     NSpace: typeof import('naive-ui')['NSpace'] | ||||||
|  |     NSwitch: typeof import('naive-ui')['NSwitch'] | ||||||
|     NTable: typeof import('naive-ui')['NTable'] |     NTable: typeof import('naive-ui')['NTable'] | ||||||
|     NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] |     NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] | ||||||
|     OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] |     OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] | ||||||
|  | |||||||
| @ -392,3 +392,18 @@ tools: | |||||||
|   text-to-binary: |   text-to-binary: | ||||||
|     title: Text to ASCII binary |     title: Text to ASCII binary | ||||||
|     description: Convert text to its ASCII binary representation and vice-versa. |     description: Convert text to its ASCII binary representation and vice-versa. | ||||||
|  | 
 | ||||||
|  |   markdown-to-word: | ||||||
|  |     title: Markdown to Word | ||||||
|  |     description: Convert Markdown documents to Word document format with live preview and custom styling support. | ||||||
|  |     input: | ||||||
|  |       placeholder: Enter your Markdown text here... | ||||||
|  |       label: Markdown Input | ||||||
|  |     preview: | ||||||
|  |       label: Preview | ||||||
|  |     export: | ||||||
|  |       button: Export to Word | ||||||
|  |       disabled: Please enter Markdown content first | ||||||
|  |     notification: | ||||||
|  |       success: Export successful! | ||||||
|  |       error: Export failed, please try again. | ||||||
|  | |||||||
| @ -153,7 +153,7 @@ tools: | |||||||
| 
 | 
 | ||||||
|   random-port-generator: |   random-port-generator: | ||||||
|     title: 随机端口生成 |     title: 随机端口生成 | ||||||
|     description: 生成“已知”端口范围(0-1023)之外的随机端口号。 |     description: 生成"已知"端口范围(0-1023)之外的随机端口号。 | ||||||
| 
 | 
 | ||||||
|   yaml-prettify: |   yaml-prettify: | ||||||
|     title: YAML美化和格式化 |     title: YAML美化和格式化 | ||||||
| @ -212,7 +212,7 @@ tools: | |||||||
| 
 | 
 | ||||||
|   numeronym-generator: |   numeronym-generator: | ||||||
|     title: 数字名称生成器 |     title: 数字名称生成器 | ||||||
|     description: 数字名是一个用数字构成缩写的词。例如,“i18n”是“国际化”的名词,其中18表示单词中第一个i和最后一个n之间的字母数。 |     description: 数字名是一个用数字构成缩写的词。例如,"i18n"是"国际化"的名词,其中18表示单词中第一个i和最后一个n之间的字母数。 | ||||||
| 
 | 
 | ||||||
|   case-converter: |   case-converter: | ||||||
|     title: 大小写转换 |     title: 大小写转换 | ||||||
| @ -220,7 +220,7 @@ tools: | |||||||
| 
 | 
 | ||||||
|   html-entities: |   html-entities: | ||||||
|     title: 转义html实体 |     title: 转义html实体 | ||||||
|     description: 转义或unescape html实体(将<、>、&、“和\'替换为其html版本) |     description: 转义或unescape html实体(将<、>、&、"和'替换为其html版本) | ||||||
| 
 | 
 | ||||||
|   json-prettify: |   json-prettify: | ||||||
|     title: JSON美化和格式化 |     title: JSON美化和格式化 | ||||||
| @ -383,8 +383,23 @@ tools: | |||||||
| 
 | 
 | ||||||
|   url-encoder: |   url-encoder: | ||||||
|     title: 编码/解码url格式的字符串 |     title: 编码/解码url格式的字符串 | ||||||
|     description: 编码为url编码格式(也称为“百分比编码”)或从中解码。 |     description: 编码为url编码格式(也称为"百分比编码")或从中解码。 | ||||||
| 
 | 
 | ||||||
|   text-to-binary: |   text-to-binary: | ||||||
|     title: 文本到 ASCII 二进制 |     title: 文本到 ASCII 二进制 | ||||||
|     description: 将文本转换为其 ASCII 二进制表示形式,反之亦然。 |     description: 将文本转换为其 ASCII 二进制表示形式,反之亦然。 | ||||||
|  | 
 | ||||||
|  |   markdown-to-word: | ||||||
|  |     title: Markdown 转 Word | ||||||
|  |     description: 将 Markdown 文档转换为 Word 文档格式,支持实时预览和自定义样式。 | ||||||
|  |     input: | ||||||
|  |       placeholder: 在此输入 Markdown 文本... | ||||||
|  |       label: Markdown 输入 | ||||||
|  |     preview: | ||||||
|  |       label: 预览 | ||||||
|  |     export: | ||||||
|  |       button: 导出为 Word | ||||||
|  |       disabled: 请先输入 Markdown 内容 | ||||||
|  |     notification: | ||||||
|  |       success: 导出成功! | ||||||
|  |       error: 导出失败,请重试。 | ||||||
|  | |||||||
| @ -47,6 +47,7 @@ | |||||||
|     "@tiptap/vue-3": "2.0.3", |     "@tiptap/vue-3": "2.0.3", | ||||||
|     "@types/figlet": "^1.5.8", |     "@types/figlet": "^1.5.8", | ||||||
|     "@types/markdown-it": "^13.0.7", |     "@types/markdown-it": "^13.0.7", | ||||||
|  |     "@types/marked": "^6.0.0", | ||||||
|     "@vicons/material": "^0.12.0", |     "@vicons/material": "^0.12.0", | ||||||
|     "@vicons/tabler": "^0.12.0", |     "@vicons/tabler": "^0.12.0", | ||||||
|     "@vueuse/core": "^10.3.0", |     "@vueuse/core": "^10.3.0", | ||||||
| @ -61,11 +62,13 @@ | |||||||
|     "cronstrue": "^2.26.0", |     "cronstrue": "^2.26.0", | ||||||
|     "crypto-js": "^4.1.1", |     "crypto-js": "^4.1.1", | ||||||
|     "date-fns": "^2.29.3", |     "date-fns": "^2.29.3", | ||||||
|  |     "docx": "^9.5.0", | ||||||
|     "dompurify": "^3.0.6", |     "dompurify": "^3.0.6", | ||||||
|     "email-normalizer": "^1.0.0", |     "email-normalizer": "^1.0.0", | ||||||
|     "emojilib": "^3.0.10", |     "emojilib": "^3.0.10", | ||||||
|     "figlet": "^1.7.0", |     "figlet": "^1.7.0", | ||||||
|     "figue": "^1.2.0", |     "figue": "^1.2.0", | ||||||
|  |     "file-saver": "^2.0.5", | ||||||
|     "fuse.js": "^6.6.2", |     "fuse.js": "^6.6.2", | ||||||
|     "highlight.js": "^11.7.0", |     "highlight.js": "^11.7.0", | ||||||
|     "iarna-toml-esm": "^3.0.5", |     "iarna-toml-esm": "^3.0.5", | ||||||
| @ -75,6 +78,7 @@ | |||||||
|     "jwt-decode": "^3.1.2", |     "jwt-decode": "^3.1.2", | ||||||
|     "libphonenumber-js": "^1.10.28", |     "libphonenumber-js": "^1.10.28", | ||||||
|     "lodash": "^4.17.21", |     "lodash": "^4.17.21", | ||||||
|  |     "markdown-docx": "^1.1.2", | ||||||
|     "markdown-it": "^14.0.0", |     "markdown-it": "^14.0.0", | ||||||
|     "marked": "^10.0.0", |     "marked": "^10.0.0", | ||||||
|     "mathjs": "^11.9.1", |     "mathjs": "^11.9.1", | ||||||
|  | |||||||
							
								
								
									
										811
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										811
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -87,6 +87,7 @@ import { tool as uuidGenerator } from './uuid-generator'; | |||||||
| import { tool as macAddressLookup } from './mac-address-lookup'; | import { tool as macAddressLookup } from './mac-address-lookup'; | ||||||
| import { tool as xmlFormatter } from './xml-formatter'; | import { tool as xmlFormatter } from './xml-formatter'; | ||||||
| import { tool as yamlViewer } from './yaml-viewer'; | import { tool as yamlViewer } from './yaml-viewer'; | ||||||
|  | import { tool as markdownWord } from './markdown-to-word'; | ||||||
| 
 | 
 | ||||||
| export const toolsByCategory: ToolCategory[] = [ | export const toolsByCategory: ToolCategory[] = [ | ||||||
|   { |   { | ||||||
| @ -116,6 +117,7 @@ export const toolsByCategory: ToolCategory[] = [ | |||||||
|       xmlToJson, |       xmlToJson, | ||||||
|       jsonToXml, |       jsonToXml, | ||||||
|       markdownToHtml, |       markdownToHtml, | ||||||
|  |       markdownWord, | ||||||
|     ], |     ], | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|  | |||||||
							
								
								
									
										13
									
								
								src/tools/markdown-to-word/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/tools/markdown-to-word/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | import { Markdown } from '@vicons/tabler'; | ||||||
|  | import { defineTool } from '../tool'; | ||||||
|  | import { translate } from '@/plugins/i18n.plugin'; | ||||||
|  | 
 | ||||||
|  | export const tool = defineTool({ | ||||||
|  |   name: translate('tools.markdown-to-word.title'), | ||||||
|  |   path: '/markdown-to-word', | ||||||
|  |   description: translate('tools.markdown-to-word.description'), | ||||||
|  |   keywords: ['markdown', 'word', 'docx', 'converter'], | ||||||
|  |   component: () => import('./markdown-to-word.vue'), | ||||||
|  |   icon: Markdown, | ||||||
|  |   createdAt: new Date('2024-08-25'), | ||||||
|  | }); | ||||||
							
								
								
									
										372
									
								
								src/tools/markdown-to-word/markdown-to-word.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										372
									
								
								src/tools/markdown-to-word/markdown-to-word.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,372 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { computed, ref } from 'vue'; | ||||||
|  | import markdownDocx, { Packer } from 'markdown-docx'; | ||||||
|  | import { marked } from 'marked'; | ||||||
|  | import DOMPurify from 'dompurify'; | ||||||
|  | import { useI18n } from 'vue-i18n'; | ||||||
|  | 
 | ||||||
|  | const { t } = useI18n(); | ||||||
|  | const inputMarkdown = ref(''); | ||||||
|  | const dataUrl = ref(''); | ||||||
|  | const isLoading = ref(false); | ||||||
|  | const lastExportTime = ref<Date | null>(null); | ||||||
|  | 
 | ||||||
|  | // 计算按钮是否禁用 | ||||||
|  | const isExportDisabled = computed(() => { | ||||||
|  |   return isLoading.value || !inputMarkdown.value.trim(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | // Markdown 预览 | ||||||
|  | const previewHtml = computed(() => { | ||||||
|  |   if (!inputMarkdown.value) { | ||||||
|  |     return ''; | ||||||
|  |   } | ||||||
|  |   const rawHtml = marked(inputMarkdown.value); | ||||||
|  |   return DOMPurify.sanitize(rawHtml); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | async function convertMarkdownToDocx() { | ||||||
|  |   if (!inputMarkdown.value.trim()) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   isLoading.value = true; | ||||||
|  |   try { | ||||||
|  |     const doc = await markdownDocx(inputMarkdown.value); | ||||||
|  |     const blob = await Packer.toBlob(doc); | ||||||
|  |     const url = URL.createObjectURL(blob); | ||||||
|  |     dataUrl.value = url; | ||||||
|  | 
 | ||||||
|  |     // 自动下载 | ||||||
|  |     const a = document.createElement('a'); | ||||||
|  |     a.href = url; | ||||||
|  |     a.download = 'document.docx'; | ||||||
|  |     a.click(); | ||||||
|  |     URL.revokeObjectURL(url); | ||||||
|  |     lastExportTime.value = new Date(); | ||||||
|  |   } | ||||||
|  |   catch (error) { | ||||||
|  |     console.error('转换失败:', error); | ||||||
|  |     dataUrl.value = ''; | ||||||
|  |   } | ||||||
|  |   finally { | ||||||
|  |     isLoading.value = false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <div class="markdown-to-word-container"> | ||||||
|  |     <!-- 顶部标题和按钮区 --> | ||||||
|  |     <div class="header"> | ||||||
|  |       <h1 class="title">{{ t('tools.markdown-to-word.title') }}</h1> | ||||||
|  |       <p class="subtitle">{{ t('tools.markdown-to-word.description') }}</p> | ||||||
|  |       <button | ||||||
|  |         class="export-button" | ||||||
|  |         :disabled="isExportDisabled" | ||||||
|  |         @click="convertMarkdownToDocx" | ||||||
|  |       > | ||||||
|  |         {{ isLoading ? '生成中...' : t('tools.markdown-to-word.export.button') }} | ||||||
|  |       </button> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <!-- 主内容区 --> | ||||||
|  |     <div class="content"> | ||||||
|  |       <!-- 左侧输入区 --> | ||||||
|  |       <section class="input-section"> | ||||||
|  |         <textarea | ||||||
|  |           v-model="inputMarkdown" | ||||||
|  |           :placeholder="t('tools.markdown-to-word.input.placeholder')" | ||||||
|  |           class="markdown-input" | ||||||
|  |         /> | ||||||
|  |       </section> | ||||||
|  | 
 | ||||||
|  |       <!-- 右侧预览区 --> | ||||||
|  |       <section class="preview-section"> | ||||||
|  |         <!-- Markdown 预览 --> | ||||||
|  |         <div class="markdown-preview"> | ||||||
|  |           <div v-if="isLoading" class="loading"> | ||||||
|  |             <div class="loading-spinner" /> | ||||||
|  |             <div class="loading-text">{{ t('tools.markdown-to-word.export.disabled') }}</div> | ||||||
|  |           </div> | ||||||
|  |           <div v-else-if="inputMarkdown" v-html="previewHtml" class="preview-content" /> | ||||||
|  |           <div v-else class="empty-state"> | ||||||
|  |             {{ t('tools.markdown-to-word.input.placeholder') }}<br> | ||||||
|  |             {{ t('tools.markdown-to-word.export.disabled') }} | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         <!-- 导出成功提示 --> | ||||||
|  |         <div v-if="lastExportTime" class="export-success"> | ||||||
|  |           <div class="success-icon">✓</div> | ||||||
|  |           {{ t('tools.markdown-to-word.notification.success') }} | ||||||
|  |         </div> | ||||||
|  |       </section> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | .markdown-to-word-container { | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  |   height: 100vh; | ||||||
|  |   width: 100%; | ||||||
|  |   min-width: 100%; | ||||||
|  |   background: #fafbfc; | ||||||
|  |   position: relative; | ||||||
|  |   overflow: hidden; | ||||||
|  |   box-sizing: border-box; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .header { | ||||||
|  |   padding: 20px 24px; | ||||||
|  |   background: #fff; | ||||||
|  |   border-bottom: 1px solid #e5e7eb; | ||||||
|  |   position: relative; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .title { | ||||||
|  |   font-size: 24px; | ||||||
|  |   font-weight: 600; | ||||||
|  |   color: #111827; | ||||||
|  |   margin: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .subtitle { | ||||||
|  |   font-size: 14px; | ||||||
|  |   color: #6b7280; | ||||||
|  |   margin: 4px 0 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .export-button { | ||||||
|  |   position: absolute; | ||||||
|  |   right: 24px; | ||||||
|  |   top: 50%; | ||||||
|  |   transform: translateY(-50%); | ||||||
|  |   padding: 8px 20px; | ||||||
|  |   background: #42b983; | ||||||
|  |   color: #fff; | ||||||
|  |   border: none; | ||||||
|  |   border-radius: 4px; | ||||||
|  |   font-size: 16px; | ||||||
|  |   font-weight: 500; | ||||||
|  |   cursor: pointer; | ||||||
|  |   transition: all 0.2s; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .export-button:disabled { | ||||||
|  |   background: #d1d5db; | ||||||
|  |   cursor: not-allowed; | ||||||
|  |   opacity: 0.7; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .content { | ||||||
|  |   display: flex; | ||||||
|  |   flex: 1; | ||||||
|  |   min-height: 0; | ||||||
|  |   height: calc(100vh - 85px); /* 减去header的高度 */ | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .input-section { | ||||||
|  |   width: 48%; | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  |   padding: 20px; | ||||||
|  |   border-right: 1px solid #e5e7eb; | ||||||
|  |   background: #fff; | ||||||
|  |   min-width: 500px; | ||||||
|  |   height: 100%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .markdown-input { | ||||||
|  |   flex: 1; | ||||||
|  |   width: 100%; | ||||||
|  |   height: 100%; | ||||||
|  |   min-height: 0; | ||||||
|  |   resize: none; | ||||||
|  |   font-family: 'JetBrains Mono', 'Menlo', 'Monaco', 'Consolas', monospace; | ||||||
|  |   font-size: 15px; | ||||||
|  |   padding: 24px 32px; | ||||||
|  |   border: 1px solid #e5e7eb; | ||||||
|  |   border-radius: 4px; | ||||||
|  |   background: #f7f7fa; | ||||||
|  |   box-sizing: border-box; | ||||||
|  |   line-height: 1.6; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .preview-section { | ||||||
|  |   flex: 1; | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  |   padding: 20px; | ||||||
|  |   background: #fff; | ||||||
|  |   min-width: 500px; | ||||||
|  |   position: relative; | ||||||
|  |   height: 100%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .markdown-preview { | ||||||
|  |   flex: 1; | ||||||
|  |   height: 100%; | ||||||
|  |   padding: 24px 32px; | ||||||
|  |   overflow-y: auto; | ||||||
|  |   background: #fff; | ||||||
|  |   border: 1px solid #e5e7eb; | ||||||
|  |   border-radius: 4px; | ||||||
|  |   line-height: 1.6; | ||||||
|  |   font-size: 16px; | ||||||
|  |   min-height: 0; | ||||||
|  |   position: relative; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .preview-content { | ||||||
|  |   width: 100%; | ||||||
|  |   height: 100%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .empty-state { | ||||||
|  |   position: absolute; | ||||||
|  |   top: 50%; | ||||||
|  |   left: 50%; | ||||||
|  |   transform: translate(-50%, -50%); | ||||||
|  |   text-align: center; | ||||||
|  |   color: #666; | ||||||
|  |   font-size: 16px; | ||||||
|  |   line-height: 1.6; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .loading { | ||||||
|  |   position: absolute; | ||||||
|  |   top: 50%; | ||||||
|  |   left: 50%; | ||||||
|  |   transform: translate(-50%, -50%); | ||||||
|  |   text-align: center; | ||||||
|  |   z-index: 1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .loading-spinner { | ||||||
|  |   width: 40px; | ||||||
|  |   height: 40px; | ||||||
|  |   margin: 0 auto 16px; | ||||||
|  |   border: 3px solid #f3f3f3; | ||||||
|  |   border-top: 3px solid #42b983; | ||||||
|  |   border-radius: 50%; | ||||||
|  |   animation: spin 1s linear infinite; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @keyframes spin { | ||||||
|  |   0% { transform: rotate(0deg); } | ||||||
|  |   100% { transform: rotate(360deg); } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .loading-text { | ||||||
|  |   color: #666; | ||||||
|  |   font-size: 16px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .export-success { | ||||||
|  |   position: fixed; | ||||||
|  |   bottom: 24px; | ||||||
|  |   right: 24px; | ||||||
|  |   background: #f0fdf4; | ||||||
|  |   border: 1px solid #86efac; | ||||||
|  |   border-radius: 4px; | ||||||
|  |   padding: 12px 16px; | ||||||
|  |   display: flex; | ||||||
|  |   align-items: center; | ||||||
|  |   gap: 8px; | ||||||
|  |   color: #166534; | ||||||
|  |   font-size: 14px; | ||||||
|  |   animation: fadeIn 0.3s ease; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .success-icon { | ||||||
|  |   color: #22c55e; | ||||||
|  |   font-size: 18px; | ||||||
|  |   font-weight: bold; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @keyframes fadeIn { | ||||||
|  |   from { opacity: 0; transform: translateY(10px); } | ||||||
|  |   to { opacity: 1; transform: translateY(0); } | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | /* Markdown 预览样式 */ | ||||||
|  | .markdown-preview h1, | ||||||
|  | .markdown-preview h2, | ||||||
|  | .markdown-preview h3, | ||||||
|  | .markdown-preview h4, | ||||||
|  | .markdown-preview h5, | ||||||
|  | .markdown-preview h6 { | ||||||
|  |   margin-top: 24px; | ||||||
|  |   margin-bottom: 16px; | ||||||
|  |   font-weight: 600; | ||||||
|  |   line-height: 1.25; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .markdown-preview h1 { font-size: 2em; } | ||||||
|  | .markdown-preview h2 { font-size: 1.5em; } | ||||||
|  | .markdown-preview h3 { font-size: 1.25em; } | ||||||
|  | 
 | ||||||
|  | .markdown-preview p { | ||||||
|  |   margin-bottom: 16px; | ||||||
|  |   line-height: 1.6; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .markdown-preview code { | ||||||
|  |   padding: 0.2em 0.4em; | ||||||
|  |   background: #f1f5f9; | ||||||
|  |   border-radius: 3px; | ||||||
|  |   font-family: 'JetBrains Mono', monospace; | ||||||
|  |   font-size: 0.9em; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .markdown-preview pre { | ||||||
|  |   padding: 16px; | ||||||
|  |   overflow: auto; | ||||||
|  |   background: #f8fafc; | ||||||
|  |   border-radius: 6px; | ||||||
|  |   margin-bottom: 16px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .markdown-preview pre code { | ||||||
|  |   padding: 0; | ||||||
|  |   background: transparent; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .markdown-preview ul, | ||||||
|  | .markdown-preview ol { | ||||||
|  |   padding-left: 2em; | ||||||
|  |   margin-bottom: 16px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .markdown-preview blockquote { | ||||||
|  |   padding: 0 1em; | ||||||
|  |   color: #666; | ||||||
|  |   border-left: 0.25em solid #ddd; | ||||||
|  |   margin: 0 0 16px 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .markdown-preview img { | ||||||
|  |   max-width: 100%; | ||||||
|  |   height: auto; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .markdown-preview table { | ||||||
|  |   border-collapse: collapse; | ||||||
|  |   width: 100%; | ||||||
|  |   margin-bottom: 16px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .markdown-preview th, | ||||||
|  | .markdown-preview td { | ||||||
|  |   padding: 6px 13px; | ||||||
|  |   border: 1px solid #ddd; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .markdown-preview tr:nth-child(2n) { | ||||||
|  |   background: #f8fafc; | ||||||
|  | } | ||||||
|  | </style> | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user