前端优化
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, watch, nextTick,onUnmounted } from 'vue'
|
||||
import { renderMarkdown } from '@/utils/markdown'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -17,93 +17,94 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
// 当前渲染的内容(避免重复渲染)
|
||||
const currentContent = ref('')
|
||||
// 渲染的 HTML 内容
|
||||
// 内部维护的纯文本内容(用于计算增量和渲染源)
|
||||
const internalContent = ref('')
|
||||
// 最终渲染的 HTML
|
||||
const renderedContent = ref('')
|
||||
|
||||
/**
|
||||
* 更新渲染内容
|
||||
* 只有当内容真正改变时才更新,避免重复渲染
|
||||
*/
|
||||
function updateRenderedContent() {
|
||||
const content = currentContent.value
|
||||
let debounceTimer = null
|
||||
|
||||
// 避免重复渲染相同内容
|
||||
if (content === renderedContent.value.replace(/<[^>]*>/g, '')) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
renderedContent.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
// 渲染 markdown 为 HTML
|
||||
renderedContent.value = renderMarkdown(content)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理内容更新
|
||||
* 流式渲染:需要拼接增量内容
|
||||
*/
|
||||
function handleContentUpdate(newContent) {
|
||||
if (!newContent) {
|
||||
currentContent.value = ''
|
||||
updateRenderedContent()
|
||||
return
|
||||
}
|
||||
|
||||
// 流式模式下拼接增量内容
|
||||
if (props.isStreaming) {
|
||||
currentContent.value += newContent
|
||||
// 仅在流式时计算并追加增量
|
||||
function appendStreamingDelta(newFullContent) {
|
||||
const prev = internalContent.value
|
||||
if (newFullContent.startsWith(prev)) {
|
||||
// 正常情况:新内容包含旧内容 → 只追加差值
|
||||
const delta = newFullContent.slice(prev.length)
|
||||
internalContent.value += delta
|
||||
} else {
|
||||
// 非流式模式下直接替换
|
||||
currentContent.value = newContent
|
||||
// 异常情况(如后端重发了不连续的内容),直接覆盖防止乱序
|
||||
console.warn('[PromptDisplay] Streaming content out of order, forcing replace')
|
||||
internalContent.value = newFullContent
|
||||
}
|
||||
|
||||
updateRenderedContent()
|
||||
}
|
||||
|
||||
// 监听 content 变化,使用防抖处理避免频繁更新
|
||||
let updateTimeout = null
|
||||
watch(() => props.content, (newContent) => {
|
||||
// 清除之前的定时器
|
||||
if (updateTimeout) {
|
||||
clearTimeout(updateTimeout)
|
||||
// 更新 Markdown 渲染
|
||||
async function updateRendered() {
|
||||
if (!internalContent.value) {
|
||||
renderedContent.value = ''
|
||||
return
|
||||
}
|
||||
// renderMarkdown 可能包含异步操作(如 highlight.js),用 nextTick 确保 DOM 就绪
|
||||
renderedContent.value = await renderMarkdown(internalContent.value)
|
||||
}
|
||||
|
||||
// 主更新逻辑
|
||||
function updateContent(newContent = '') {
|
||||
if (props.isStreaming) {
|
||||
appendStreamingDelta(newContent)
|
||||
} else {
|
||||
internalContent.value = newContent
|
||||
}
|
||||
|
||||
// 延迟更新,避免流式传输时频繁更新导致的性能问题
|
||||
updateTimeout = setTimeout(() => {
|
||||
handleContentUpdate(newContent)
|
||||
}, 50) // 50ms 防抖
|
||||
})
|
||||
// 流式直接渲染,非流式防抖(避免频繁完整替换导致光标跳动或闪烁)
|
||||
if (props.isStreaming) {
|
||||
updateRendered()
|
||||
} else {
|
||||
clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => {
|
||||
updateRendered()
|
||||
}, 60) // 60ms 足够平滑且不卡顿
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 isStreaming 变化
|
||||
// 监听 content 变化(外部每次推送的都是当前完整内容)
|
||||
watch(() => props.content, (newVal) => {
|
||||
updateContent(newVal || '')
|
||||
}, { immediate: true })
|
||||
|
||||
// 当流式开始时清空,防止旧内容残留
|
||||
watch(() => props.isStreaming, (newVal, oldVal) => {
|
||||
// 流式传输开始时,清空之前的内容
|
||||
if (newVal && !oldVal) {
|
||||
currentContent.value = ''
|
||||
internalContent.value = ''
|
||||
renderedContent.value = ''
|
||||
}
|
||||
// 流式传输结束时,确保显示完整内容
|
||||
if (!newVal && oldVal && props.content) {
|
||||
currentContent.value = props.content
|
||||
updateRenderedContent()
|
||||
}
|
||||
})
|
||||
|
||||
// 立即渲染初始内容
|
||||
handleContentUpdate(props.content)
|
||||
// 可选:组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 修复 pre 标签撑开容器的问题 */
|
||||
.prompt-display {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 代码块优化 */
|
||||
.prompt-display :deep(pre) {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
background: #282c34;
|
||||
color: #abb2bf;
|
||||
padding: 1em;
|
||||
border-radius: 8px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
.prompt-display :deep(code) {
|
||||
font-family: 'Fira Code', Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
||||
</style>
|
||||
@@ -32,7 +32,6 @@ const editForm = reactive({
|
||||
content: '',
|
||||
category: '',
|
||||
status: 1,
|
||||
_originalRecord: null, // 保存原始记录,用于更新时获取必需字段
|
||||
})
|
||||
const editFormRef = ref(null)
|
||||
|
||||
@@ -173,12 +172,7 @@ function handleReset() {
|
||||
|
||||
// 新增
|
||||
function handleAdd() {
|
||||
editForm.id = null
|
||||
editForm.name = ''
|
||||
editForm.content = ''
|
||||
editForm.category = ''
|
||||
editForm.status = 1
|
||||
editForm._originalRecord = null
|
||||
resetEditForm()
|
||||
editModalVisible.value = true
|
||||
}
|
||||
|
||||
@@ -188,62 +182,72 @@ function handleEdit(record) {
|
||||
editForm.name = record.name || ''
|
||||
editForm.content = record.content || ''
|
||||
editForm.category = record.category || ''
|
||||
editForm.status = record.status ?? 1
|
||||
// 保存原始记录的完整信息,用于更新时传递必需字段
|
||||
editForm._originalRecord = record
|
||||
editForm.status = record.status !== null && record.status !== undefined ? record.status : 1
|
||||
editModalVisible.value = true
|
||||
}
|
||||
|
||||
// 表单重置
|
||||
function resetEditForm() {
|
||||
editForm.id = null
|
||||
editForm.name = ''
|
||||
editForm.content = ''
|
||||
editForm.category = ''
|
||||
editForm.status = 1
|
||||
}
|
||||
|
||||
// 通用API调用
|
||||
async function apiCall(apiFunc, param, successMessage, isDelete = false) {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await apiFunc(param)
|
||||
if (response && (response.code === 0 || response.code === 200)) {
|
||||
message.success(successMessage)
|
||||
if (!isDelete) {
|
||||
editModalVisible.value = false
|
||||
}
|
||||
loadData()
|
||||
return true
|
||||
} else {
|
||||
throw new Error(response?.msg || response?.message || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('API调用失败:', error)
|
||||
message.error(error?.message || '操作失败,请稍后重试')
|
||||
return false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存(新增/编辑)
|
||||
async function handleSave() {
|
||||
try {
|
||||
await editFormRef.value.validate()
|
||||
} catch (error) {
|
||||
console.error('保存提示词失败:', error)
|
||||
console.error('表单验证失败:', error)
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
name: editForm.name.trim(),
|
||||
content: editForm.content.trim(),
|
||||
category: editForm.category.trim() || null,
|
||||
status: editForm.status,
|
||||
}
|
||||
const payload = {
|
||||
name: editForm.name.trim(),
|
||||
content: editForm.content.trim(),
|
||||
category: editForm.category.trim() || null,
|
||||
status: editForm.status,
|
||||
}
|
||||
|
||||
if (editForm.id) {
|
||||
// 更新:只需要传递要修改的字段,后端会自动填充其他字段
|
||||
payload.id = editForm.id
|
||||
// 注意:sort、useCount、isPublic 等字段后端会自动从数据库获取,无需前端传递
|
||||
|
||||
const response = await UserPromptApi.updateUserPrompt(payload)
|
||||
if (response && (response.code === 0 || response.code === 200)) {
|
||||
message.success('更新成功')
|
||||
editModalVisible.value = false
|
||||
loadData()
|
||||
} else {
|
||||
throw new Error(response?.msg || response?.message || '更新失败')
|
||||
}
|
||||
} else {
|
||||
// 新增:需要包含所有必需字段
|
||||
payload.sort = 0
|
||||
payload.useCount = 0
|
||||
payload.isPublic = false
|
||||
|
||||
const response = await UserPromptApi.createUserPrompt(payload)
|
||||
if (response && (response.code === 0 || response.code === 200)) {
|
||||
message.success('创建成功')
|
||||
editModalVisible.value = false
|
||||
loadData()
|
||||
} else {
|
||||
throw new Error(response?.msg || response?.message || '创建失败')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存提示词失败:', error)
|
||||
message.error(error?.message || '保存失败,请稍后重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (editForm.id) {
|
||||
payload.id = editForm.id
|
||||
await apiCall(
|
||||
(data) => UserPromptApi.updateUserPrompt(data),
|
||||
payload,
|
||||
'更新成功'
|
||||
)
|
||||
} else {
|
||||
await apiCall(
|
||||
(data) => UserPromptApi.createUserPrompt(data),
|
||||
payload,
|
||||
'创建成功'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,21 +257,12 @@ function handleDelete(record) {
|
||||
title: '确认删除',
|
||||
content: `确定要删除提示词"${record.name}"吗?`,
|
||||
onOk: async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await UserPromptApi.deleteUserPrompt(record.id)
|
||||
if (response && (response.code === 0 || response.code === 200)) {
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
} else {
|
||||
throw new Error(response?.msg || response?.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除提示词失败:', error)
|
||||
message.error(error?.message || '删除失败,请稍后重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
await apiCall(
|
||||
(id) => UserPromptApi.deleteUserPrompt(id),
|
||||
record.id,
|
||||
'删除成功',
|
||||
true
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user