Migrate backend jobs to Redis Streams
This commit is contained in:
@@ -7,6 +7,20 @@
|
||||
<div class="doc-card__time">{{ displayTime }}</div>
|
||||
</div>
|
||||
<div class="doc-card__actions">
|
||||
<button type="button" class="doc-card__btn" :title="'压缩文档'" @click="handleCompress">
|
||||
<svg v-if="compressState === 'idle' || compressState === 'completed'" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 14h6v7H4z"/>
|
||||
<path d="M14 9h6v12h-6z"/>
|
||||
<path d="M4 9h6v5H4z"/>
|
||||
</svg>
|
||||
<span v-if="compressState === 'queued' || compressState === 'processing'" class="doc-card__spinner"></span>
|
||||
<svg v-else-if="compressState === 'error'" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="15" y1="9" x2="9" y2="15"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span v-if="compressState === 'queued'" class="doc-card__status-label">排队中</span>
|
||||
<button type="button" class="doc-card__btn" :title="collapsedState ? '展开文件' : '折叠文件'" @click="toggleCollapse">
|
||||
<svg v-if="collapsedState" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6"/>
|
||||
@@ -39,7 +53,7 @@ import { Crepe } from '@milkdown/crepe'
|
||||
import { editorViewCtx } from '@milkdown/kit/core'
|
||||
import { copilotPlugin, copilotConfigCtx, copilotGhostMark, setCopilotEnabled, clearGhostSuggestion } from '../plugins/copilotPlugin'
|
||||
import { hiddenTextInputPlugin, hiddenTextNode, hiddenTextRemark, hiddenTextView } from '../plugins/hiddenTextPlugin'
|
||||
import { fetchSuggestion } from '../utils/api.js'
|
||||
import { fetchSuggestion, submitCompress, pollCompressStatus } from '../utils/api.js'
|
||||
import { isDocumentVisible, getRecommendedDebounce, getRecommendedSyncInterval } from '../composables/useVisibility.js'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -52,14 +66,52 @@ const props = defineProps({
|
||||
onUpdateContent: { type: Function, default: null },
|
||||
onUpdateCollapsed: { type: Function, default: null },
|
||||
onDelete: { type: Function, default: null },
|
||||
onCompress: { type: Function, default: null },
|
||||
})
|
||||
|
||||
const editorRoot = ref(null)
|
||||
const collapsedState = ref(Boolean(props.collapsed))
|
||||
const currentContent = ref(props.content || '')
|
||||
const compressState = ref('idle') // idle | queued | processing | error
|
||||
let crepe = null
|
||||
let syncTimer = null
|
||||
let syncingExternal = false
|
||||
let compressPoller = null
|
||||
|
||||
const handleCompress = () => {
|
||||
if (compressState.value !== 'idle') return
|
||||
|
||||
// 直接从嵌套编辑器获取最新内容,不依赖外部同步缓存
|
||||
const content = crepe?.getMarkdown() || ''
|
||||
if (!content.trim()) {
|
||||
compressState.value = 'error'
|
||||
setTimeout(() => { compressState.value = 'idle' }, 2000)
|
||||
return
|
||||
}
|
||||
|
||||
submitCompress(content, props.docType).then((result) => {
|
||||
compressState.value = 'queued'
|
||||
|
||||
if (compressPoller) compressPoller.stop()
|
||||
compressPoller = pollCompressStatus(result.task_id, (status, compressedContent, message) => {
|
||||
compressState.value = status
|
||||
|
||||
if (status === 'completed') {
|
||||
// 压缩完成,直接更新嵌套编辑器的内容
|
||||
crepe.editor.action(replaceAll(compressedContent))
|
||||
} else if (status === 'error') {
|
||||
setTimeout(() => { compressState.value = 'idle' }, 3000)
|
||||
}
|
||||
})
|
||||
|
||||
if (compressState.value === 'queued') {
|
||||
// Task already completed before polling started
|
||||
}
|
||||
}).catch(() => {
|
||||
compressState.value = 'error'
|
||||
setTimeout(() => { compressState.value = 'idle' }, 3000)
|
||||
})
|
||||
}
|
||||
|
||||
const typeLabel = computed(() => {
|
||||
if (props.docType === 'docx') return 'DOCX'
|
||||
@@ -84,6 +136,10 @@ const syncContent = () => {
|
||||
if (!crepe) return
|
||||
// Skip content sync when tab is hidden (energy saving for nested editors)
|
||||
if (!isDocumentVisible()) return
|
||||
|
||||
// Don't sync during compression to avoid overwriting compressed content
|
||||
if (compressState.value !== 'idle') return
|
||||
|
||||
if (syncTimer) clearTimeout(syncTimer)
|
||||
const syncInterval = getRecommendedSyncInterval(120)
|
||||
syncTimer = setTimeout(async () => {
|
||||
@@ -96,11 +152,24 @@ const syncContent = () => {
|
||||
|
||||
const syncExternalContent = async (nextValue) => {
|
||||
const value = nextValue || ''
|
||||
|
||||
// Reject empty content to prevent accidental document clearing (e.g., from failed compression)
|
||||
if (!value || !value.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!crepe) {
|
||||
currentContent.value = value
|
||||
return
|
||||
}
|
||||
if (value === currentContent.value) return
|
||||
|
||||
// Clear pending sync timer to prevent stale content from overwriting new content
|
||||
if (syncTimer) {
|
||||
clearTimeout(syncTimer)
|
||||
syncTimer = null
|
||||
}
|
||||
|
||||
syncingExternal = true
|
||||
try {
|
||||
crepe.editor.action(replaceAll(value))
|
||||
@@ -173,6 +242,10 @@ onUnmounted(() => {
|
||||
clearTimeout(syncTimer)
|
||||
syncTimer = null
|
||||
}
|
||||
if (compressPoller) {
|
||||
compressPoller.stop()
|
||||
compressPoller = null
|
||||
}
|
||||
if (crepe) {
|
||||
crepe.editor.action((ctx) => {
|
||||
const view = ctx.get(editorViewCtx)
|
||||
@@ -262,6 +335,26 @@ onUnmounted(() => {
|
||||
.doc-card__actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.doc-card__status-label {
|
||||
font-size: 10px;
|
||||
color: #f59e0b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.doc-card__spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid rgba(59, 130, 246, 0.2);
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: doc-card-spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes doc-card-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.doc-card__btn {
|
||||
|
||||
Reference in New Issue
Block a user