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'] | ||||
|     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'] | ||||
|     MarkdownToWord: typeof import('./src/tools/markdown-to-word/markdown-to-word.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'] | ||||
|     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'] | ||||
|     MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] | ||||
|     NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] | ||||
|     NButton: typeof import('naive-ui')['NButton'] | ||||
|     NCheckbox: typeof import('naive-ui')['NCheckbox'] | ||||
|     NCode: typeof import('naive-ui')['NCode'] | ||||
|     NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] | ||||
|     NConfigProvider: typeof import('naive-ui')['NConfigProvider'] | ||||
|     NDivider: typeof import('naive-ui')['NDivider'] | ||||
|     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'] | ||||
|     NH3: typeof import('naive-ui')['NH3'] | ||||
|     NIcon: typeof import('naive-ui')['NIcon'] | ||||
|     NInputGroup: typeof import('naive-ui')['NInputGroup'] | ||||
|     NLayout: typeof import('naive-ui')['NLayout'] | ||||
|     NLayoutSider: typeof import('naive-ui')['NLayoutSider'] | ||||
|     NMenu: typeof import('naive-ui')['NMenu'] | ||||
|     NScrollbar: typeof import('naive-ui')['NScrollbar'] | ||||
|     NSlider: typeof import('naive-ui')['NSlider'] | ||||
|     NSpace: typeof import('naive-ui')['NSpace'] | ||||
|     NSwitch: typeof import('naive-ui')['NSwitch'] | ||||
|     NTable: typeof import('naive-ui')['NTable'] | ||||
|     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'] | ||||
|  | ||||
| @ -392,3 +392,18 @@ tools: | ||||
|   text-to-binary: | ||||
|     title: Text to ASCII binary | ||||
|     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: | ||||
|     title: 随机端口生成 | ||||
|     description: 生成“已知”端口范围(0-1023)之外的随机端口号。 | ||||
|     description: 生成"已知"端口范围(0-1023)之外的随机端口号。 | ||||
| 
 | ||||
|   yaml-prettify: | ||||
|     title: YAML美化和格式化 | ||||
| @ -212,7 +212,7 @@ tools: | ||||
| 
 | ||||
|   numeronym-generator: | ||||
|     title: 数字名称生成器 | ||||
|     description: 数字名是一个用数字构成缩写的词。例如,“i18n”是“国际化”的名词,其中18表示单词中第一个i和最后一个n之间的字母数。 | ||||
|     description: 数字名是一个用数字构成缩写的词。例如,"i18n"是"国际化"的名词,其中18表示单词中第一个i和最后一个n之间的字母数。 | ||||
| 
 | ||||
|   case-converter: | ||||
|     title: 大小写转换 | ||||
| @ -220,7 +220,7 @@ tools: | ||||
| 
 | ||||
|   html-entities: | ||||
|     title: 转义html实体 | ||||
|     description: 转义或unescape html实体(将<、>、&、“和\'替换为其html版本) | ||||
|     description: 转义或unescape html实体(将<、>、&、"和'替换为其html版本) | ||||
| 
 | ||||
|   json-prettify: | ||||
|     title: JSON美化和格式化 | ||||
| @ -383,8 +383,23 @@ tools: | ||||
| 
 | ||||
|   url-encoder: | ||||
|     title: 编码/解码url格式的字符串 | ||||
|     description: 编码为url编码格式(也称为“百分比编码”)或从中解码。 | ||||
|     description: 编码为url编码格式(也称为"百分比编码")或从中解码。 | ||||
| 
 | ||||
|   text-to-binary: | ||||
|     title: 文本到 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", | ||||
|     "@types/figlet": "^1.5.8", | ||||
|     "@types/markdown-it": "^13.0.7", | ||||
|     "@types/marked": "^6.0.0", | ||||
|     "@vicons/material": "^0.12.0", | ||||
|     "@vicons/tabler": "^0.12.0", | ||||
|     "@vueuse/core": "^10.3.0", | ||||
| @ -61,11 +62,13 @@ | ||||
|     "cronstrue": "^2.26.0", | ||||
|     "crypto-js": "^4.1.1", | ||||
|     "date-fns": "^2.29.3", | ||||
|     "docx": "^9.5.0", | ||||
|     "dompurify": "^3.0.6", | ||||
|     "email-normalizer": "^1.0.0", | ||||
|     "emojilib": "^3.0.10", | ||||
|     "figlet": "^1.7.0", | ||||
|     "figue": "^1.2.0", | ||||
|     "file-saver": "^2.0.5", | ||||
|     "fuse.js": "^6.6.2", | ||||
|     "highlight.js": "^11.7.0", | ||||
|     "iarna-toml-esm": "^3.0.5", | ||||
| @ -75,6 +78,7 @@ | ||||
|     "jwt-decode": "^3.1.2", | ||||
|     "libphonenumber-js": "^1.10.28", | ||||
|     "lodash": "^4.17.21", | ||||
|     "markdown-docx": "^1.1.2", | ||||
|     "markdown-it": "^14.0.0", | ||||
|     "marked": "^10.0.0", | ||||
|     "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 xmlFormatter } from './xml-formatter'; | ||||
| import { tool as yamlViewer } from './yaml-viewer'; | ||||
| import { tool as markdownWord } from './markdown-to-word'; | ||||
| 
 | ||||
| export const toolsByCategory: ToolCategory[] = [ | ||||
|   { | ||||
| @ -116,6 +117,7 @@ export const toolsByCategory: ToolCategory[] = [ | ||||
|       xmlToJson, | ||||
|       jsonToXml, | ||||
|       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