chore: 更新项目配置和依赖,优化前后端代码

This commit is contained in:
2026-04-04 20:05:40 +08:00
parent ef162de168
commit be4000b774
33 changed files with 2338 additions and 942 deletions
+250
View File
@@ -0,0 +1,250 @@
<template>
<div class="doc-block-crepe" :class="{ collapsed: isCollapsed }">
<div class="doc-header">
<div class="doc-icon">
<svg v-if="docType === 'pdf'" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<path d="M9 15v-2h6v2"/>
<path d="M12 13v4"/>
</svg>
<svg v-else-if="docType === 'doc'" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<path d="M16 13H8"/>
<path d="M16 17H8"/>
<path d="M10 9H8"/>
</svg>
<svg v-else-if="docType === 'ppt'" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2"/>
<path d="M8 21h8"/>
<path d="M12 17v4"/>
</svg>
<svg v-else width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<path d="M16 13H8"/>
<path d="M16 17H8"/>
</svg>
</div>
<div class="doc-name">{{ docName }}</div>
<div class="doc-actions">
<button @click="downloadDoc" class="action-btn" title="下载文档">
<svg width="14" height="14" 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"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
</button>
<button @click="toggleCollapse" class="action-btn collapse-btn" :title="isCollapsed ? '展开' : '折叠'">
<svg v-if="isCollapsed" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"/>
</svg>
<svg v-else width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
</div>
</div>
<div class="doc-editor" v-show="!isCollapsed">
<div ref="editorRoot" class="inner-crepe"></div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { Crepe } from '@milkdown/crepe'
import { editorViewCtx, serializerCtx } from '@milkdown/kit/core'
import { copilotPlugin, copilotConfigCtx, setCopilotEnabled } from '../plugins/copilotPlugin'
import { fetchSuggestion } from '../utils/api.js'
const props = defineProps({
docType: { type: String, default: 'text' },
docName: { type: String, default: 'document.txt' },
uploadTime: { type: String, default: '' },
initialContent: { type: String, default: '' }
})
const emit = defineEmits(['update:content', 'delete'])
const editorRoot = ref(null)
const isCollapsed = ref(false)
let crepe = null
let internalChangeTimer = null
const toggleCollapse = () => {
isCollapsed.value = !isCollapsed.value
}
const downloadDoc = () => {
if (!crepe) return
crepe.getMarkdown().then(markdown => {
const blob = new Blob([markdown], { type: 'text/plain;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = props.docName
document.body.appendChild(a)
a.click()
a.remove()
URL.revokeObjectURL(url)
})
}
const syncContent = () => {
if (!crepe) return
if (internalChangeTimer) clearTimeout(internalChangeTimer)
internalChangeTimer = setTimeout(async () => {
const markdown = await crepe.getMarkdown()
emit('update:content', markdown)
}, 120)
}
onMounted(async () => {
if (!editorRoot.value) return
crepe = new Crepe({
root: editorRoot.value,
defaultValue: props.initialContent || '',
features: {
[Crepe.Feature.Latex]: true,
[Crepe.Feature.ImageBlock]: true,
[Crepe.Feature.Table]: true,
[Crepe.Feature.ListCheck]: true,
},
config: { showLineNumber: false }
})
crepe.editor.config(ctx => {
ctx.set(copilotConfigCtx.key, {
fetchSuggestion,
debounceMs: 1000
})
})
crepe.editor.use(copilotPlugin)
await crepe.create()
crepe.on(listener => {
listener.updated(() => {
syncContent()
})
})
crepe.editor.action(ctx => {
const view = ctx.get(editorViewCtx)
setCopilotEnabled(view, true)
})
})
watch(() => props.initialContent, (newVal) => {
if (crepe && newVal !== undefined) {
crepe.editor.action(ctx => {
const view = ctx.get(editorViewCtx)
const currentPos = view.state.selection.from
view.dispatch(view.state.tr.insertText(newVal))
})
}
})
onUnmounted(() => {
if (internalChangeTimer) clearTimeout(internalChangeTimer)
if (crepe) {
crepe.destroy()
crepe = null
}
})
defineExpose({
getContent: () => crepe ? crepe.getMarkdown() : Promise.resolve(''),
getEditor: () => crepe
})
</script>
<style scoped>
.doc-block-crepe {
margin: 12px 0;
border-radius: 8px;
overflow: hidden;
background: var(--crepe-color-surface-low);
border: 1px solid var(--panel-border);
}
.doc-block-crepe.collapsed .doc-editor {
display: none;
}
.doc-header {
display: flex;
align-items: center;
padding: 10px 12px;
background: var(--crepe-color-surface);
border-bottom: 1px solid var(--panel-border);
gap: 10px;
}
.doc-icon {
display: flex;
align-items: center;
justify-content: center;
color: var(--crepe-color-primary);
}
.doc-name {
flex: 1;
font-size: 14px;
font-weight: 500;
color: var(--crepe-color-on-surface);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.doc-actions {
display: flex;
align-items: center;
gap: 4px;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
border: none;
background: transparent;
color: var(--crepe-color-on-surface-variant);
cursor: pointer;
border-radius: 4px;
opacity: 0.7;
}
.action-btn:hover {
background: var(--crepe-color-hover);
opacity: 1;
}
.doc-editor {
padding: 8px;
background: var(--crepe-color-surface-low);
min-height: 120px;
max-height: 400px;
overflow-y: auto;
}
.inner-crepe {
width: 100%;
height: 100%;
}
.inner-crepe :deep(.milkdown) {
background: transparent !important;
}
.inner-crepe :deep(.ProseMirror) {
min-height: 80px;
padding: 8px !important;
}
</style>
+195
View File
@@ -0,0 +1,195 @@
<template>
<div class="doc-block" :class="{ collapsed: isCollapsed }">
<!-- 深色条文件头 -->
<div class="doc-header">
<!-- 最左边文件类型icon -->
<div class="doc-icon">
<!-- PDF icon -->
<svg v-if="docType === 'pdf'" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<path d="M9 15v-2h6v2"/>
<path d="M12 13v4"/>
</svg>
<!-- Word icon -->
<svg v-else-if="docType === 'doc'" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<path d="M16 13H8"/>
<path d="M16 17H8"/>
<path d="M10 9H8"/>
</svg>
<!-- PPT icon -->
<svg v-else-if="docType === 'ppt'" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2"/>
<path d="M8 21h8"/>
<path d="M12 17v4"/>
</svg>
<!-- TXT icon -->
<svg v-else width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<path d="M16 13H8"/>
<path d="M16 17H8"/>
</svg>
</div>
<!-- 中间文件名 -->
<div class="doc-name">{{ docName }}</div>
<!-- 最右边下载按钮 + 折叠按钮 -->
<div class="doc-actions">
<button @click="downloadDoc" class="action-btn" title="下载文档">
<svg width="14" height="14" 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"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
</button>
<button @click="toggleCollapse" class="action-btn collapse-btn" :title="isCollapsed ? '展开' : '折叠'">
<svg v-if="isCollapsed" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"/>
</svg>
<svg v-else width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
</div>
</div>
<!-- 浅色块文档内容非折叠状态显示 -->
<div class="doc-content" v-show="!isCollapsed">
<pre>{{ content }}</pre>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
docType: {
type: String,
default: 'text'
},
docName: {
type: String,
default: 'document.txt'
},
uploadTime: {
type: String,
default: ''
},
content: {
type: String,
default: ''
}
})
const isCollapsed = ref(false)
const toggleCollapse = () => {
isCollapsed.value = !isCollapsed.value
}
const downloadDoc = () => {
const blob = new Blob([props.content], { type: 'text/plain;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = props.docName
document.body.appendChild(a)
a.click()
a.remove()
URL.revokeObjectURL(url)
}
</script>
<style scoped>
.doc-block {
margin: 12px 0;
border-radius: 8px;
overflow: hidden;
background: var(--crepe-color-surface-low);
border: 1px solid var(--panel-border);
}
.doc-block.collapsed .doc-content {
display: none;
}
/* 深色条 */
.doc-header {
display: flex;
align-items: center;
padding: 10px 12px;
background: var(--crepe-color-surface);
border-bottom: 1px solid var(--panel-border);
gap: 10px;
}
/* 文件类型icon */
.doc-icon {
display: flex;
align-items: center;
justify-content: center;
color: var(--crepe-color-primary);
}
/* 文件名 */
.doc-name {
flex: 1;
font-size: 14px;
font-weight: 500;
color: var(--crepe-color-on-surface);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 操作按钮 */
.doc-actions {
display: flex;
align-items: center;
gap: 4px;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
border: none;
background: transparent;
color: var(--crepe-color-on-surface-variant);
cursor: pointer;
border-radius: 4px;
opacity: 0.7;
}
.action-btn:hover {
background: var(--crepe-color-hover);
opacity: 1;
}
/* 浅色块:文档内容 */
.doc-content {
padding: 12px;
background: var(--crepe-color-surface-low);
max-height: 400px;
overflow-y: auto;
}
.doc-content pre {
margin: 0;
padding: 0;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', monospace;
font-size: 13px;
line-height: 1.6;
color: var(--crepe-color-on-surface);
white-space: pre-wrap;
word-break: break-word;
}
</style>
+1 -1
View File
@@ -16,7 +16,7 @@ const props = defineProps({
})
const md = new MarkdownIt({
html: true,
html: false,
linkify: true,
typographer: true
})
+2 -1
View File
@@ -1,4 +1,4 @@
<template>
<template>
<div class="editor-container">
<div ref="root" class="milkdown-editor"></div>
@@ -1198,3 +1198,4 @@ onUnmounted(() => {
}
</style>
+4 -1
View File
@@ -2,10 +2,13 @@
import { ref, watch, computed, onMounted, onUnmounted } from 'vue'
import { useSettingsStore } from '../stores/settings'
import { useTheme } from '../composables/useTheme'
import packageJson from '../../package.json'
const store = useSettingsStore()
const { setTheme } = useTheme()
const VERSION = packageJson.version || '0.0.0'
const isOpen = ref(false)
let systemThemeMediaQuery = null
@@ -272,7 +275,7 @@ const t = (key) => store.t[key]
<div class="about-card">
<h4>llm-in-text</h4>
<p>A smart Markdown editor with local LLM intelligence.</p>
<p class="version">v0.1.0-beta</p>
<p class="version">v{{ VERSION }}</p>
</div>
</section>
</div>