Merge remote changes with local modifications
- Add docx and html2pdf.js export functionality (from remote) - Update backend with new API endpoints - Sync local configuration changes
This commit is contained in:
@@ -2,6 +2,8 @@ import asyncio
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ from pydantic import BaseModel
|
|||||||
from geoip import get_ip_location_text
|
from geoip import get_ip_location_text
|
||||||
from llm import call_ollama, call_vlm_ocr
|
from llm import call_ollama, call_vlm_ocr
|
||||||
from prompt import build_completion_prompts, prepare_prompt_context
|
from prompt import build_completion_prompts, prepare_prompt_context
|
||||||
|
import markitdown
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
@@ -73,6 +76,11 @@ class OCRRequest(BaseModel):
|
|||||||
language: str = "auto"
|
language: str = "auto"
|
||||||
|
|
||||||
|
|
||||||
|
class ConvertRequest(BaseModel):
|
||||||
|
file: str
|
||||||
|
filename: str = "document.pdf"
|
||||||
|
|
||||||
|
|
||||||
def _preview(text: str, limit: int = 80) -> str:
|
def _preview(text: str, limit: int = 80) -> str:
|
||||||
value = (text or "").replace("\n", "\\n")
|
value = (text or "").replace("\n", "\\n")
|
||||||
if len(value) <= limit:
|
if len(value) <= limit:
|
||||||
@@ -243,6 +251,58 @@ async def ocr_image(request: OCRRequest, api_key: str = Security(get_api_key)):
|
|||||||
return JSONResponse(content={"error": str(e)}, status_code=500)
|
return JSONResponse(content={"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/v1/convert")
|
||||||
|
async def convert_to_markdown(request: ConvertRequest, api_key: str = Security(get_api_key)):
|
||||||
|
"""将文件转换为Markdown格式"""
|
||||||
|
request_id = str(uuid.uuid4())[:8]
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(
|
||||||
|
"[%s] /v1/convert filename=%s file_base64_chars=%d",
|
||||||
|
request_id,
|
||||||
|
request.filename,
|
||||||
|
len(request.file or ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 解码Base64文件内容
|
||||||
|
file_bytes = base64.b64decode(request.file)
|
||||||
|
logger.info("[%s] /v1/convert decoded file_bytes=%d", request_id, len(file_bytes))
|
||||||
|
|
||||||
|
# 获取文件扩展名
|
||||||
|
ext = os.path.splitext(request.filename)[1].lower()
|
||||||
|
|
||||||
|
# 创建临时文件
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp:
|
||||||
|
tmp.write(file_bytes)
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 使用MarkItDown转换为Markdown
|
||||||
|
md = markitdown.MarkItDown()
|
||||||
|
result = md.convert(tmp_path)
|
||||||
|
markdown_text = result.text_content
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[%s] /v1/convert success text_chars=%d text_preview='%s'",
|
||||||
|
request_id,
|
||||||
|
len(markdown_text or ""),
|
||||||
|
_preview(markdown_text, 120),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"markdown": markdown_text,
|
||||||
|
"filename": request.filename
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
# 清理临时文件
|
||||||
|
if os.path.exists(tmp_path):
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("[%s] /v1/convert failed: %s", request_id, e)
|
||||||
|
return JSONResponse(content={"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,8 @@ pydantic
|
|||||||
python-dotenv
|
python-dotenv
|
||||||
httpx
|
httpx
|
||||||
geoip2
|
geoip2
|
||||||
|
markitdown
|
||||||
|
python-docx
|
||||||
|
python-pptx
|
||||||
|
openpyxl
|
||||||
|
pypdf
|
||||||
|
|||||||
Generated
-12
@@ -424,7 +424,6 @@
|
|||||||
"version": "6.12.1",
|
"version": "6.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
|
||||||
"integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==",
|
"integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/state": "^6.0.0",
|
"@codemirror/state": "^6.0.0",
|
||||||
"@codemirror/view": "^6.23.0",
|
"@codemirror/view": "^6.23.0",
|
||||||
@@ -496,7 +495,6 @@
|
|||||||
"version": "6.5.4",
|
"version": "6.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz",
|
||||||
"integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==",
|
"integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@marijn/find-cluster-break": "^1.0.0"
|
"@marijn/find-cluster-break": "^1.0.0"
|
||||||
}
|
}
|
||||||
@@ -516,7 +514,6 @@
|
|||||||
"version": "6.39.11",
|
"version": "6.39.11",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.11.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.11.tgz",
|
||||||
"integrity": "sha512-bWdeR8gWM87l4DB/kYSF9A+dVackzDb/V56Tq7QVrQ7rn86W0rgZFtlL3g3pem6AeGcb9NQNoy3ao4WpW4h5tQ==",
|
"integrity": "sha512-bWdeR8gWM87l4DB/kYSF9A+dVackzDb/V56Tq7QVrQ7rn86W0rgZFtlL3g3pem6AeGcb9NQNoy3ao4WpW4h5tQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/state": "^6.5.0",
|
"@codemirror/state": "^6.5.0",
|
||||||
"crelt": "^1.0.6",
|
"crelt": "^1.0.6",
|
||||||
@@ -3073,7 +3070,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz",
|
||||||
"integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==",
|
"integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chevrotain/cst-dts-gen": "11.1.1",
|
"@chevrotain/cst-dts-gen": "11.1.1",
|
||||||
"@chevrotain/gast": "11.1.1",
|
"@chevrotain/gast": "11.1.1",
|
||||||
@@ -3402,7 +3398,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
|
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
|
||||||
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
|
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
@@ -3824,7 +3819,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@@ -6578,7 +6572,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -6794,7 +6787,6 @@
|
|||||||
"version": "1.25.4",
|
"version": "1.25.4",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
||||||
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"orderedmap": "^2.0.0"
|
"orderedmap": "^2.0.0"
|
||||||
}
|
}
|
||||||
@@ -6825,7 +6817,6 @@
|
|||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||||
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-model": "^1.0.0",
|
"prosemirror-model": "^1.0.0",
|
||||||
"prosemirror-transform": "^1.0.0",
|
"prosemirror-transform": "^1.0.0",
|
||||||
@@ -6856,7 +6847,6 @@
|
|||||||
"version": "1.41.5",
|
"version": "1.41.5",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.5.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.5.tgz",
|
||||||
"integrity": "sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==",
|
"integrity": "sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-model": "^1.20.0",
|
"prosemirror-model": "^1.20.0",
|
||||||
"prosemirror-state": "^1.0.0",
|
"prosemirror-state": "^1.0.0",
|
||||||
@@ -8498,7 +8488,6 @@
|
|||||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -8622,7 +8611,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
|
||||||
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
|
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.26",
|
"@vue/compiler-dom": "3.5.26",
|
||||||
"@vue/compiler-sfc": "3.5.26",
|
"@vue/compiler-sfc": "3.5.26",
|
||||||
|
|||||||
@@ -48,27 +48,20 @@
|
|||||||
</button>
|
</button>
|
||||||
<input type="file" ref="fileInputRef" @change="handleFileUpload" accept=".md,text/markdown,text/x-markdown" style="display:none">
|
<input type="file" ref="fileInputRef" @change="handleFileUpload" accept=".md,text/markdown,text/x-markdown" style="display:none">
|
||||||
|
|
||||||
<div class="export-btn-wrapper">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
class="action-btn"
|
||||||
class="action-btn"
|
:aria-label="t('exportMd')"
|
||||||
:aria-label="t('exportMd')"
|
:title="t('exportMd')"
|
||||||
:title="t('exportMd')"
|
@click="exportMarkdown"
|
||||||
@click="toggleExportDropdown"
|
>
|
||||||
>
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
<polyline points="7 10 12 15 17 10"/>
|
||||||
<polyline points="7 10 12 15 17 10"/>
|
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
</svg>
|
||||||
</svg>
|
<span class="btn-tooltip">{{ t('exportMd') }}</span>
|
||||||
<span class="btn-tooltip">{{ t('exportMd') }}</span>
|
</button>
|
||||||
</button>
|
|
||||||
<div v-if="showExportDropdown" class="export-dropdown">
|
|
||||||
<button type="button" @click="exportMarkdown">{{ t('exportMd') }}</button>
|
|
||||||
<button type="button" @click="exportToDocx">{{ t('exportDocx') }}</button>
|
|
||||||
<button type="button" @click="exportToPdf">{{ t('exportPdf') }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="image-btn-wrapper">
|
<div class="image-btn-wrapper">
|
||||||
<button
|
<button
|
||||||
@@ -135,15 +128,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showMermaidPreview" class="mermaid-preview-overlay" @click.self="closeMermaidPreview">
|
|
||||||
<div class="mermaid-preview-dialog" role="dialog" aria-modal="true" aria-label="Mermaid Preview">
|
|
||||||
<button type="button" class="mermaid-preview-close" @click="closeMermaidPreview" aria-label="Close preview">✕</button>
|
|
||||||
<div class="mermaid-preview-scroll">
|
|
||||||
<img :src="mermaidPreviewSrc" alt="Mermaid diagram preview">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -151,19 +135,15 @@
|
|||||||
import { onMounted, onUnmounted, ref, computed, watch } from 'vue'
|
import { onMounted, onUnmounted, ref, computed, watch } from 'vue'
|
||||||
import { replaceAll } from '@milkdown/kit/utils'
|
import { replaceAll } from '@milkdown/kit/utils'
|
||||||
import { Crepe } from '@milkdown/crepe'
|
import { Crepe } from '@milkdown/crepe'
|
||||||
import { editorViewCtx } from '@milkdown/kit/core'
|
import { editorViewCtx, serializerCtx } from '@milkdown/kit/core'
|
||||||
import { Selection } from '@milkdown/prose/state'
|
import { Selection } from '@milkdown/prose/state'
|
||||||
import { undo, redo, undoDepth, redoDepth } from '@milkdown/prose/history'
|
import { undo, redo, undoDepth, redoDepth } from '@milkdown/prose/history'
|
||||||
import { copilotPlugin, copilotConfigCtx, copilotGhostMark, setCopilotEnabled, interruptCopilot, COPILOT_PLUGIN_KEY, SIZE_LIMIT, checkSizeLimit, clearGhostSuggestion } from '../plugins/copilotPlugin'
|
import { copilotPlugin, copilotConfigCtx, copilotGhostMark, setCopilotEnabled, interruptCopilot, COPILOT_PLUGIN_KEY, SIZE_LIMIT, checkSizeLimit, clearGhostSuggestion } from '../plugins/copilotPlugin'
|
||||||
import { mermaidRenderPreview, codeBlockConfig, refreshMermaidPreviews } from '../plugins/mermaidPlugin'
|
import { mermaidRenderPreview, codeBlockConfig } from '../plugins/mermaidPlugin'
|
||||||
import { fetchSuggestion } from '../utils/api.js'
|
import { fetchSuggestion } from '../utils/api.js'
|
||||||
import { useSettingsStore } from '../stores/settings'
|
import { useSettingsStore } from '../stores/settings'
|
||||||
import { OCR_URL } from '../utils/config.js'
|
import { OCR_URL } from '../utils/config.js'
|
||||||
import { setOcrCache, clearOcrCache, clearAllOcrCache, IMAGE_SIZE_LIMIT, calculateImageHash, getOcrByHash, setOcrByHash } from '../utils/ocrCache.js'
|
import { setOcrCache, clearOcrCache, clearAllOcrCache, IMAGE_SIZE_LIMIT, calculateImageHash, getOcrByHash, setOcrByHash } from '../utils/ocrCache.js'
|
||||||
import MarkdownIt from 'markdown-it'
|
|
||||||
import katex from 'katex'
|
|
||||||
import 'katex/dist/katex.min.css'
|
|
||||||
import html2pdf from 'html2pdf.js'
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:markdown'])
|
const emit = defineEmits(['update:markdown'])
|
||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
@@ -176,10 +156,7 @@ const cameraInputRef = ref(null)
|
|||||||
const aiEnabled = ref(true)
|
const aiEnabled = ref(true)
|
||||||
const contentSize = ref(0)
|
const contentSize = ref(0)
|
||||||
const showImageDropdown = ref(false)
|
const showImageDropdown = ref(false)
|
||||||
const showExportDropdown = ref(false)
|
|
||||||
const showUrlDialog = ref(false)
|
const showUrlDialog = ref(false)
|
||||||
const showMermaidPreview = ref(false)
|
|
||||||
const mermaidPreviewSrc = ref('')
|
|
||||||
const imageUrl = ref('')
|
const imageUrl = ref('')
|
||||||
const canUndo = ref(false)
|
const canUndo = ref(false)
|
||||||
const canRedo = ref(false)
|
const canRedo = ref(false)
|
||||||
@@ -201,8 +178,6 @@ const aiButtonLabel = computed(() => {
|
|||||||
let crepe = null
|
let crepe = null
|
||||||
let markdownSyncTimer = null
|
let markdownSyncTimer = null
|
||||||
let rootResizeObserver = null
|
let rootResizeObserver = null
|
||||||
let themeObserver = null
|
|
||||||
let mermaidResizeTimer = null
|
|
||||||
const objectUrls = new Set()
|
const objectUrls = new Set()
|
||||||
const IMAGE_NODE_TYPES = new Set(['image', 'image-block', 'imageBlock'])
|
const IMAGE_NODE_TYPES = new Set(['image', 'image-block', 'imageBlock'])
|
||||||
const MARKDOWN_EXT_RE = /\.md$/i
|
const MARKDOWN_EXT_RE = /\.md$/i
|
||||||
@@ -417,170 +392,6 @@ const prepareImageFile = async (file) => {
|
|||||||
return objectUrl
|
return objectUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeMermaidPreview = () => {
|
|
||||||
showMermaidPreview.value = false
|
|
||||||
mermaidPreviewSrc.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const openMermaidPreview = (url) => {
|
|
||||||
if (!url) return
|
|
||||||
mermaidPreviewSrc.value = url
|
|
||||||
showMermaidPreview.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeMermaidFilename = () => {
|
|
||||||
const now = new Date()
|
|
||||||
const pad = (n) => String(n).padStart(2, '0')
|
|
||||||
const datePart = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
|
|
||||||
const timePart = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
|
|
||||||
return `mermaid-${datePart}-${timePart}.png`
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeMermaidFilename = (filename = '') => {
|
|
||||||
if (!filename) return makeMermaidFilename()
|
|
||||||
if (/\.png$/i.test(filename)) return filename
|
|
||||||
if (/\.[^./\\]+$/.test(filename)) return filename.replace(/\.[^./\\]+$/, '.png')
|
|
||||||
return `${filename}.png`
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseSvgSize = (svg) => {
|
|
||||||
const viewBox = svg.match(/viewBox\s*=\s*["']\s*[-\d.]+\s+[-\d.]+\s+([-\d.]+)\s+([-\d.]+)\s*["']/i)
|
|
||||||
if (viewBox) {
|
|
||||||
const width = Number(viewBox[1])
|
|
||||||
const height = Number(viewBox[2])
|
|
||||||
if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
|
|
||||||
return { width, height }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const widthAttr = svg.match(/width\s*=\s*["']([-\d.]+)(px)?["']/i)
|
|
||||||
const heightAttr = svg.match(/height\s*=\s*["']([-\d.]+)(px)?["']/i)
|
|
||||||
const width = widthAttr ? Number(widthAttr[1]) : 960
|
|
||||||
const height = heightAttr ? Number(heightAttr[1]) : 540
|
|
||||||
return {
|
|
||||||
width: Number.isFinite(width) && width > 0 ? width : 960,
|
|
||||||
height: Number.isFinite(height) && height > 0 ? height : 540,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getRasterDpr = (width, height) => {
|
|
||||||
const rawDpr = typeof window !== 'undefined' ? (window.devicePixelRatio || 1) : 1
|
|
||||||
const baseDpr = Math.min(3, Math.max(1, rawDpr))
|
|
||||||
const maxCanvasEdge = 4096
|
|
||||||
const edgeLimitedDpr = maxCanvasEdge / Math.max(width, height, 1)
|
|
||||||
return Math.max(1, Math.min(baseDpr, edgeLimitedDpr))
|
|
||||||
}
|
|
||||||
|
|
||||||
const decodeSvgDataUrl = (url) => {
|
|
||||||
const commaIndex = url.indexOf(',')
|
|
||||||
if (commaIndex === -1) throw new Error('Invalid SVG data URL')
|
|
||||||
const header = url.slice(0, commaIndex)
|
|
||||||
const payload = url.slice(commaIndex + 1)
|
|
||||||
if (/;base64/i.test(header)) {
|
|
||||||
const binary = atob(payload)
|
|
||||||
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0))
|
|
||||||
return new TextDecoder().decode(bytes)
|
|
||||||
}
|
|
||||||
return decodeURIComponent(payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
const svgTextToPngDataUrl = async (svgText) => {
|
|
||||||
const fallback = parseSvgSize(svgText)
|
|
||||||
let width = fallback.width
|
|
||||||
let height = fallback.height
|
|
||||||
|
|
||||||
const maxEdge = 2400
|
|
||||||
const scale = Math.min(1, maxEdge / Math.max(width, height))
|
|
||||||
width = Math.max(1, Math.round(width * scale))
|
|
||||||
height = Math.max(1, Math.round(height * scale))
|
|
||||||
|
|
||||||
const image = new Image()
|
|
||||||
image.decoding = 'async'
|
|
||||||
image.crossOrigin = 'anonymous'
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
image.onload = () => resolve()
|
|
||||||
image.onerror = () => reject(new Error('Failed to load SVG image'))
|
|
||||||
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`
|
|
||||||
})
|
|
||||||
|
|
||||||
const dpr = getRasterDpr(width, height)
|
|
||||||
const canvas = document.createElement('canvas')
|
|
||||||
canvas.width = Math.max(1, Math.round(width * dpr))
|
|
||||||
canvas.height = Math.max(1, Math.round(height * dpr))
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
if (!ctx) throw new Error('Canvas context unavailable')
|
|
||||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
||||||
ctx.imageSmoothingEnabled = true
|
|
||||||
ctx.imageSmoothingQuality = 'high'
|
|
||||||
ctx.clearRect(0, 0, width, height)
|
|
||||||
ctx.drawImage(image, 0, 0, width, height)
|
|
||||||
|
|
||||||
return canvas.toDataURL('image/png')
|
|
||||||
}
|
|
||||||
|
|
||||||
const ensurePngDownloadUrl = async (url) => {
|
|
||||||
if (!url) return ''
|
|
||||||
if (/^data:image\/png/i.test(url)) return url
|
|
||||||
if (/^data:image\/svg\+xml/i.test(url)) {
|
|
||||||
const svgText = decodeSvgDataUrl(url)
|
|
||||||
return await svgTextToPngDataUrl(svgText)
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadMermaidImage = async (url, filename = '') => {
|
|
||||||
if (!url) return
|
|
||||||
let downloadUrl = url
|
|
||||||
try {
|
|
||||||
downloadUrl = await ensurePngDownloadUrl(url)
|
|
||||||
} catch (error) {
|
|
||||||
alert('PNG export failed for this diagram. Please remove external image resources and try again.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = downloadUrl
|
|
||||||
a.download = normalizeMermaidFilename(filename)
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
a.remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMermaidAction = async (event) => {
|
|
||||||
const target = event.target instanceof Element ? event.target.closest('[data-mermaid-action]') : null
|
|
||||||
if (!(target instanceof HTMLElement)) return
|
|
||||||
|
|
||||||
const action = target.getAttribute('data-mermaid-action')
|
|
||||||
if (!action) return
|
|
||||||
|
|
||||||
const block = target.closest('.mermaid-block')
|
|
||||||
const url = target.getAttribute('data-mermaid-url') || block?.getAttribute('data-mermaid-url') || ''
|
|
||||||
if (!url) return
|
|
||||||
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
|
|
||||||
if (action === 'zoom') {
|
|
||||||
openMermaidPreview(url)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === 'download') {
|
|
||||||
const filename = target.getAttribute('data-mermaid-filename') || ''
|
|
||||||
await downloadMermaidImage(url, filename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMermaidViewportResize = () => {
|
|
||||||
if (mermaidResizeTimer) {
|
|
||||||
clearTimeout(mermaidResizeTimer)
|
|
||||||
mermaidResizeTimer = null
|
|
||||||
}
|
|
||||||
mermaidResizeTimer = setTimeout(() => {
|
|
||||||
mermaidResizeTimer = null
|
|
||||||
refreshMermaidPreviews()
|
|
||||||
}, 140)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!root.value) throw new Error('root.value is null')
|
if (!root.value) throw new Error('root.value is null')
|
||||||
updateEditorTailSpace()
|
updateEditorTailSpace()
|
||||||
@@ -590,12 +401,10 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
rootResizeObserver.observe(root.value)
|
rootResizeObserver.observe(root.value)
|
||||||
}
|
}
|
||||||
root.value.addEventListener('click', handleMermaidAction, true)
|
|
||||||
window.addEventListener('resize', handleMermaidViewportResize)
|
|
||||||
|
|
||||||
crepe = new Crepe({
|
crepe = new Crepe({
|
||||||
root: root.value,
|
root: root.value,
|
||||||
defaultValue: '# 欢迎来到LLM-IN-TEXT\n\n一个即时LLM系统\n\n在下开始你的创作...',
|
defaultValue: '# Welcome to LLM-IN-TEXT\n\nA instant LLM system\n\nStart your creative work below...',
|
||||||
features: {
|
features: {
|
||||||
[Crepe.Feature.Latex]: true,
|
[Crepe.Feature.Latex]: true,
|
||||||
[Crepe.Feature.ImageBlock]: true,
|
[Crepe.Feature.ImageBlock]: true,
|
||||||
@@ -655,7 +464,6 @@ onMounted(async () => {
|
|||||||
|
|
||||||
|
|
||||||
await crepe.create()
|
await crepe.create()
|
||||||
refreshMermaidPreviews()
|
|
||||||
|
|
||||||
crepe.on((listener) => {
|
crepe.on((listener) => {
|
||||||
listener.updated((ctx, doc) => {
|
listener.updated((ctx, doc) => {
|
||||||
@@ -673,20 +481,6 @@ onMounted(async () => {
|
|||||||
refreshSizeAndLimit(ctx)
|
refreshSizeAndLimit(ctx)
|
||||||
updateHistoryState(view)
|
updateHistoryState(view)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (typeof MutationObserver !== 'undefined') {
|
|
||||||
themeObserver = new MutationObserver((mutations) => {
|
|
||||||
const changed = mutations.some((mutation) => mutation.type === 'attributes' && mutation.attributeName === 'data-theme')
|
|
||||||
if (changed) {
|
|
||||||
refreshMermaidPreviews()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
themeObserver.observe(document.documentElement, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['data-theme'],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleMarkdownSync()
|
scheduleMarkdownSync()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -714,233 +508,6 @@ const exportMarkdown = async () => {
|
|||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 棰勫鐞?LaTeX 鍏紡
|
|
||||||
const preprocessLatex = (text) => {
|
|
||||||
// 澶勭悊 $$...$$ 鍧楃骇鍏紡
|
|
||||||
text = text.replace(/\$\$([\s\S]*?)\$\$/g, (match, content) => {
|
|
||||||
try {
|
|
||||||
const html = katex.renderToString(content.trim(), {
|
|
||||||
displayMode: true,
|
|
||||||
throwOnError: false
|
|
||||||
})
|
|
||||||
return `<div class="math-block">${html}</div>`
|
|
||||||
} catch (e) {
|
|
||||||
return `<div class="math-error">$$${content}$$</div>`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 澶勭悊 $...$ 琛屽唴鍏紡
|
|
||||||
text = text.replace(/(?<!\$)\$(?!\$)([^\$\n]+?)\$(?!\$)/g, (match, content) => {
|
|
||||||
try {
|
|
||||||
const html = katex.renderToString(content.trim(), {
|
|
||||||
displayMode: false,
|
|
||||||
throwOnError: false
|
|
||||||
})
|
|
||||||
return `<span class="math-inline">${html}</span>`
|
|
||||||
} catch (e) {
|
|
||||||
return `<span class="math-error">${match}</span>`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
// 灏?markdown 杞崲涓?HTML 骞跺鐞?mermaid
|
|
||||||
const decodeHtmlEntities = (input) => {
|
|
||||||
if (!input) return ''
|
|
||||||
const textarea = document.createElement('textarea')
|
|
||||||
textarea.innerHTML = input
|
|
||||||
return textarea.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const collectRenderedMermaidImages = () => {
|
|
||||||
const imageMap = new Map()
|
|
||||||
const blocks = document.querySelectorAll('.mermaid-block[data-mermaid-code][data-mermaid-url]')
|
|
||||||
for (const block of blocks) {
|
|
||||||
const encodedCode = block.getAttribute('data-mermaid-code') || ''
|
|
||||||
const imageUrl = block.getAttribute('data-mermaid-url') || ''
|
|
||||||
if (!encodedCode || !imageUrl) continue
|
|
||||||
try {
|
|
||||||
const code = normalizeMermaidCodeForExport(decodeURIComponent(encodedCode))
|
|
||||||
if (code) imageMap.set(code, imageUrl)
|
|
||||||
} catch (error) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return imageMap
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeMermaidCodeForExport = (code) => {
|
|
||||||
return (code || '').replace(/\r\n/g, '\n').trim()
|
|
||||||
}
|
|
||||||
const markdownToHtml = async (markdown) => {
|
|
||||||
const md = new MarkdownIt({
|
|
||||||
html: true,
|
|
||||||
linkify: true,
|
|
||||||
typographer: true
|
|
||||||
})
|
|
||||||
|
|
||||||
// 棰勫鐞?LaTeX
|
|
||||||
let html = preprocessLatex(markdown)
|
|
||||||
|
|
||||||
// 娓叉煋 markdown
|
|
||||||
html = md.render(html)
|
|
||||||
|
|
||||||
// Mermaid code block -> rendered image
|
|
||||||
const mermaidImageMap = collectRenderedMermaidImages()
|
|
||||||
const template = document.createElement('template')
|
|
||||||
template.innerHTML = html
|
|
||||||
const mermaidNodes = template.content.querySelectorAll('pre > code.language-mermaid')
|
|
||||||
for (const node of mermaidNodes) {
|
|
||||||
const pre = node.parentElement
|
|
||||||
if (!(pre instanceof HTMLElement)) continue
|
|
||||||
const normalizedCode = normalizeMermaidCodeForExport(decodeHtmlEntities(node.textContent || ''))
|
|
||||||
const imgUrl = mermaidImageMap.get(normalizedCode)
|
|
||||||
if (!imgUrl) {
|
|
||||||
pre.remove()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const wrapper = document.createElement('div')
|
|
||||||
wrapper.className = 'mermaid-export'
|
|
||||||
const image = document.createElement('img')
|
|
||||||
image.src = imgUrl
|
|
||||||
image.alt = 'Mermaid diagram'
|
|
||||||
wrapper.appendChild(image)
|
|
||||||
pre.replaceWith(wrapper)
|
|
||||||
}
|
|
||||||
html = template.innerHTML
|
|
||||||
|
|
||||||
html = html.replace(/<pre class="language-(\w+)"><code>([\s\S]*?)<\/code><\/pre>/g,
|
|
||||||
'<pre class="code-simple"><code>$2</code></pre>')
|
|
||||||
|
|
||||||
// Process remote images
|
|
||||||
html = html.replace(/<img src="(http[s]?:\/\/[^"]+)"([^>]*)>/g, (match, src, attrs) => {
|
|
||||||
return `<img src="${src}"${attrs} style="max-width: 100%; height: auto;" />`
|
|
||||||
})
|
|
||||||
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
|
||||||
// 瀵煎嚭涓?DOCX (浣跨敤 HTML 鏍煎紡锛學ord 鍙互鐩存帴鎵撳紑)
|
|
||||||
const exportToDocx = async () => {
|
|
||||||
if (!crepe) return
|
|
||||||
|
|
||||||
showExportDropdown.value = false
|
|
||||||
|
|
||||||
try {
|
|
||||||
const markdown = await crepe.getMarkdown()
|
|
||||||
const html = await markdownToHtml(markdown)
|
|
||||||
|
|
||||||
// 鍖呰 HTML 涓哄畬鏁存枃妗o紝娣诲姞 Word 鍏煎鐨勫厓鏁版嵁
|
|
||||||
const fullHtml = `
|
|
||||||
<html xmlns:o="urn:schemas-microsoft-com:office:office"
|
|
||||||
xmlns:w="urn:schemas-microsoft-com:office:word"
|
|
||||||
xmlns="http://www.w3.org/TR/REC-html40">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Document</title>
|
|
||||||
<!--[if gte mso 9]>
|
|
||||||
<xml>
|
|
||||||
<w:WordDocument>
|
|
||||||
<w:View>Print</w:View>
|
|
||||||
</w:WordDocument>
|
|
||||||
</xml>
|
|
||||||
<![endif]-->
|
|
||||||
<style>
|
|
||||||
body { font-family: 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; padding: 40px; }
|
|
||||||
h1, h2, h3, h4, h5, h6 { margin-top: 1em; margin-bottom: 0.5em; font-weight: 600; }
|
|
||||||
p { margin: 1em 0; }
|
|
||||||
code { background-color: #f5f5f5; padding: 0.2em 0.4em; border-radius: 3px; font-family: monospace; }
|
|
||||||
pre { background-color: #f5f5f5; padding: 16px; border-radius: 6px; overflow-x: auto; }
|
|
||||||
pre code { background-color: transparent; padding: 0; }
|
|
||||||
blockquote { border-left: 4px solid #ddd; margin: 1em 0; padding-left: 16px; color: #666; }
|
|
||||||
img { max-width: 100%; height: auto; }
|
|
||||||
table { border-collapse: collapse; width: 100%; margin: 1em 0; }
|
|
||||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
|
||||||
th { background-color: #f5f5f5; font-weight: 600; }
|
|
||||||
.math-block { display: block; margin: 1em 0; text-align: center; overflow-x: auto; }
|
|
||||||
.math-inline { padding: 0 2px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
${html}
|
|
||||||
</body>
|
|
||||||
</html>`
|
|
||||||
|
|
||||||
// 浣跨敤 HTML 鏍煎紡淇濆瓨涓?.doc锛學ord 鍙互鐩存帴鎵撳紑
|
|
||||||
const blob = new Blob([fullHtml], { type: 'application/msword' })
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
const a = document.createElement('a')
|
|
||||||
const now = new Date()
|
|
||||||
const pad = (n) => String(n).padStart(2, '0')
|
|
||||||
const datePart = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
|
|
||||||
const timePart = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
|
|
||||||
a.href = url
|
|
||||||
a.download = `document${datePart}${timePart}.doc`
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
a.remove()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Export DOCX] Error:', error)
|
|
||||||
alert('瀵煎嚭 DOCX 澶辫触锛岃閲嶈瘯')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 瀵煎嚭涓?PDF
|
|
||||||
const exportToPdf = async () => {
|
|
||||||
if (!crepe) return
|
|
||||||
|
|
||||||
showExportDropdown.value = false
|
|
||||||
|
|
||||||
try {
|
|
||||||
const markdown = await crepe.getMarkdown()
|
|
||||||
const html = await markdownToHtml(markdown)
|
|
||||||
|
|
||||||
const fullHtml = `
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Document</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; padding: 40px; color: #111; background: #fff; }
|
|
||||||
h1, h2, h3, h4, h5, h6 { margin-top: 1em; margin-bottom: 0.5em; font-weight: 600; }
|
|
||||||
p { margin: 1em 0; }
|
|
||||||
code { background-color: #f5f5f5; padding: 0.2em 0.4em; border-radius: 3px; font-family: monospace; }
|
|
||||||
pre { background-color: #f5f5f5; padding: 16px; border-radius: 6px; overflow-x: auto; white-space: pre-wrap; word-break: break-word; }
|
|
||||||
pre code { background-color: transparent; padding: 0; }
|
|
||||||
blockquote { border-left: 4px solid #ddd; margin: 1em 0; padding-left: 16px; color: #666; }
|
|
||||||
img { max-width: 100%; height: auto; }
|
|
||||||
table { border-collapse: collapse; width: 100%; margin: 1em 0; }
|
|
||||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
|
||||||
th { background-color: #f5f5f5; font-weight: 600; }
|
|
||||||
.math-block { display: block; margin: 1em 0; text-align: center; overflow-x: auto; }
|
|
||||||
.math-inline { padding: 0 2px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
${html}
|
|
||||||
</body>
|
|
||||||
</html>`
|
|
||||||
|
|
||||||
// 閰嶇疆 PDF 閫夐」
|
|
||||||
const options = {
|
|
||||||
margin: [10, 10, 10, 10],
|
|
||||||
filename: `document${new Date().toISOString().slice(0, 10)}.pdf`,
|
|
||||||
image: { type: 'jpeg', quality: 0.98 },
|
|
||||||
html2canvas: { scale: 2, useCORS: true, backgroundColor: '#ffffff', windowWidth: 1200 },
|
|
||||||
pagebreak: { mode: ['css', 'legacy'] },
|
|
||||||
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 鐢熸垚 PDF
|
|
||||||
await html2pdf().set(options).from(fullHtml, 'string').save()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Export PDF] Error:', error)
|
|
||||||
alert('瀵煎嚭 PDF 澶辫触锛岃閲嶈瘯')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const triggerUpload = () => {
|
const triggerUpload = () => {
|
||||||
fileInputRef.value?.click()
|
fileInputRef.value?.click()
|
||||||
}
|
}
|
||||||
@@ -992,10 +559,6 @@ const toggleAI = async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleExportDropdown = () => {
|
|
||||||
showExportDropdown.value = !showExportDropdown.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleImageDropdown = () => {
|
const toggleImageDropdown = () => {
|
||||||
showImageDropdown.value = !showImageDropdown.value
|
showImageDropdown.value = !showImageDropdown.value
|
||||||
}
|
}
|
||||||
@@ -1067,21 +630,6 @@ onUnmounted(() => {
|
|||||||
rootResizeObserver = null
|
rootResizeObserver = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mermaidResizeTimer) {
|
|
||||||
clearTimeout(mermaidResizeTimer)
|
|
||||||
mermaidResizeTimer = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (themeObserver) {
|
|
||||||
themeObserver.disconnect()
|
|
||||||
themeObserver = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.value) {
|
|
||||||
root.value.removeEventListener('click', handleMermaidAction, true)
|
|
||||||
}
|
|
||||||
window.removeEventListener('resize', handleMermaidViewportResize)
|
|
||||||
|
|
||||||
for (const url of Array.from(objectUrls)) {
|
for (const url of Array.from(objectUrls)) {
|
||||||
revokeObjectUrl(url)
|
revokeObjectUrl(url)
|
||||||
}
|
}
|
||||||
@@ -1243,40 +791,6 @@ onUnmounted(() => {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.export-btn-wrapper {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.export-dropdown {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 100%;
|
|
||||||
right: 0;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
background: var(--panel-bg);
|
|
||||||
border: 1px solid var(--panel-border);
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: var(--panel-shadow);
|
|
||||||
overflow: hidden;
|
|
||||||
z-index: 10000;
|
|
||||||
min-width: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.export-dropdown button {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 16px;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--app-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.export-dropdown button:hover {
|
|
||||||
background: var(--crepe-color-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-btn-wrapper {
|
.image-btn-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -1386,60 +900,6 @@ onUnmounted(() => {
|
|||||||
filter: brightness(0.92);
|
filter: brightness(0.92);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mermaid-preview-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 10002;
|
|
||||||
background: var(--overlay-bg);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mermaid-preview-dialog {
|
|
||||||
width: min(96vw, 1280px);
|
|
||||||
max-height: min(92vh, 920px);
|
|
||||||
background: var(--panel-bg);
|
|
||||||
border: 1px solid var(--panel-border);
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: var(--panel-shadow);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mermaid-preview-close {
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
right: 8px;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
border: 1px solid var(--panel-border);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--btn-bg);
|
|
||||||
color: var(--btn-fg);
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mermaid-preview-close:hover {
|
|
||||||
background: var(--btn-hover-bg);
|
|
||||||
color: var(--btn-hover-fg);
|
|
||||||
border-color: var(--btn-hover-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mermaid-preview-scroll {
|
|
||||||
overflow: auto;
|
|
||||||
padding: 40px 16px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mermaid-preview-scroll img {
|
|
||||||
display: block;
|
|
||||||
max-width: none;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.milkdown-editor {
|
.milkdown-editor {
|
||||||
--editor-tail-space: calc(100vh - 32px);
|
--editor-tail-space: calc(100vh - 32px);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -1477,14 +937,10 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.milkdown-editor :deep(.ProseMirror img) {
|
.milkdown-editor :deep(.ProseMirror img) {
|
||||||
max-width: 80%;
|
max-width: 60%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.milkdown-editor :deep(.ProseMirror .mermaid-image) {
|
|
||||||
max-width: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.milkdown-editor :deep(.ProseMirror > *:first-child) {
|
.milkdown-editor :deep(.ProseMirror > *:first-child) {
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
@@ -1590,6 +1046,3 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,9 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
// 4. Preferences
|
// 4. Preferences
|
||||||
const language = ref('auto')
|
const language = ref('auto')
|
||||||
const currency = ref('auto')
|
const currency = ref('auto')
|
||||||
// const timezone = ref('auto') // removed
|
|
||||||
|
|
||||||
// 5. Background
|
// 5. Background
|
||||||
const backgroundType = ref('default') // 'default' | 'warm' | 'reading' | 'image'
|
const backgroundType = ref('default') // 'default' | 'warm' | 'reading' | 'image'
|
||||||
// const backgroundColor = ref('#ffffff') // removed
|
|
||||||
const backgroundImage = ref('')
|
const backgroundImage = ref('')
|
||||||
const backgroundOpacity = ref(0.2) // 0.05 - 0.50
|
const backgroundOpacity = ref(0.2) // 0.05 - 0.50
|
||||||
|
|
||||||
@@ -61,13 +59,10 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
if (typeof data.privacyMode === 'boolean') privacyMode.value = data.privacyMode
|
if (typeof data.privacyMode === 'boolean') privacyMode.value = data.privacyMode
|
||||||
if (data.language) language.value = data.language
|
if (data.language) language.value = data.language
|
||||||
if (data.currency) currency.value = data.currency
|
if (data.currency) currency.value = data.currency
|
||||||
// if (data.timezone) timezone.value = data.timezone // timezone legacy ignore
|
|
||||||
if (data.backgroundType) {
|
if (data.backgroundType) {
|
||||||
// migrate color to default if needed, or mapped
|
|
||||||
if (data.backgroundType === 'color') backgroundType.value = 'default'
|
if (data.backgroundType === 'color') backgroundType.value = 'default'
|
||||||
else backgroundType.value = data.backgroundType
|
else backgroundType.value = data.backgroundType
|
||||||
}
|
}
|
||||||
// if (data.backgroundColor) backgroundColor.value = data.backgroundColor // removed
|
|
||||||
if (data.backgroundImage) backgroundImage.value = data.backgroundImage
|
if (data.backgroundImage) backgroundImage.value = data.backgroundImage
|
||||||
if (data.backgroundOpacity) backgroundOpacity.value = data.backgroundOpacity
|
if (data.backgroundOpacity) backgroundOpacity.value = data.backgroundOpacity
|
||||||
}
|
}
|
||||||
@@ -86,9 +81,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
privacyMode: privacyMode.value,
|
privacyMode: privacyMode.value,
|
||||||
language: language.value,
|
language: language.value,
|
||||||
currency: currency.value,
|
currency: currency.value,
|
||||||
// timezone: timezone.value, // removed
|
|
||||||
backgroundType: backgroundType.value,
|
backgroundType: backgroundType.value,
|
||||||
// backgroundColor: backgroundColor.value, // removed
|
|
||||||
backgroundImage: backgroundImage.value,
|
backgroundImage: backgroundImage.value,
|
||||||
backgroundOpacity: backgroundOpacity.value,
|
backgroundOpacity: backgroundOpacity.value,
|
||||||
}
|
}
|
||||||
@@ -106,9 +99,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
privacyMode.value = false
|
privacyMode.value = false
|
||||||
language.value = 'auto'
|
language.value = 'auto'
|
||||||
currency.value = 'auto'
|
currency.value = 'auto'
|
||||||
// timezone.value = 'auto' // removed
|
|
||||||
backgroundType.value = 'default'
|
backgroundType.value = 'default'
|
||||||
// backgroundColor.value = '#ffffff' // removed
|
|
||||||
backgroundImage.value = ''
|
backgroundImage.value = ''
|
||||||
backgroundOpacity.value = 0.2
|
backgroundOpacity.value = 0.2
|
||||||
saveSettings()
|
saveSettings()
|
||||||
@@ -123,9 +114,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
privacyMode,
|
privacyMode,
|
||||||
language,
|
language,
|
||||||
currency,
|
currency,
|
||||||
// timezone, // removed
|
|
||||||
backgroundType,
|
backgroundType,
|
||||||
// backgroundColor, // removed
|
|
||||||
backgroundImage,
|
backgroundImage,
|
||||||
backgroundOpacity,
|
backgroundOpacity,
|
||||||
],
|
],
|
||||||
@@ -144,9 +133,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
privacyMode,
|
privacyMode,
|
||||||
language,
|
language,
|
||||||
currency,
|
currency,
|
||||||
// timezone, // removed
|
|
||||||
backgroundType,
|
backgroundType,
|
||||||
// backgroundColor, // removed
|
|
||||||
backgroundImage,
|
backgroundImage,
|
||||||
backgroundOpacity,
|
backgroundOpacity,
|
||||||
uiLanguage,
|
uiLanguage,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
export const DEBUG = import.meta.env.DEV
|
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://api.imageteach.tech:8002'
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://api.imageteach.tech:8002'
|
||||||
|
|
||||||
export const API_URL = import.meta.env.VITE_API_URL || `${API_BASE_URL}/v1/completions`
|
export const API_URL = import.meta.env.VITE_API_URL || `${API_BASE_URL}/v1/completions`
|
||||||
|
|||||||
Reference in New Issue
Block a user