feat(manuscript): 文稿工作台支持编辑生成结果
生成后的正文可以就地改,不再只能复制出去: - 正文区加「编辑正文 / 完成编辑」:编辑态换 textarea 编 markdown 源码, 完成时按需回预览;三端(微信小程序 / H5 / App)行为一致 - 生成标题本来就可在结果区改,编辑正文时一起生效 - 「还原 AI 原文」把正文和标题一起回滚,AI 原文快照在生成完成时落存 - 编辑版本与 AI 原文存本地草稿(uni storage,单槽):退出再进能恢复, 「清空结果」一并清除草稿 - 有编辑版本时重新生成先弹确认,避免改动被静默冲掉 - 「复制正文」复制的是当前生效版本(编辑过就是编辑后的) 改的是 pages/manuscript/index.vue:data/computed 增加编辑态与 displayMarkdown, 草稿读写走 setStorageSync/getStorageSync,纯前端改动,未动接口与入库
This commit is contained in:
@@ -154,8 +154,8 @@
|
||||
<view class="panel-card result-card">
|
||||
<view class="result-head">
|
||||
<view class="panel-title">生成结果</view>
|
||||
<view class="result-status" :class="loading ? 'result-status--loading' : markdownContent ? 'result-status--done' : ''">
|
||||
{{ loading ? '生成中' : markdownContent ? '已生成' : '待生成' }}
|
||||
<view class="result-status" :class="loading ? 'result-status--loading' : hasContent ? 'result-status--done' : ''">
|
||||
{{ statusText }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -171,20 +171,50 @@
|
||||
</view>
|
||||
|
||||
<view class="field-item">
|
||||
<view class="field-label">正文内容</view>
|
||||
<view class="field-label">
|
||||
<text>正文内容</text>
|
||||
<text v-if="hasEdited" class="field-label__badge">已编辑</text>
|
||||
</view>
|
||||
<view class="result-shell">
|
||||
<view v-if="loading" class="result-streaming">
|
||||
{{ markdownContent || 'AI 正在生成内容...' }}
|
||||
<template v-if="editing">
|
||||
<view class="edit-hint">支持 markdown 语法:# 标题、**加粗**、- 列表;改完点「完成编辑」回预览</view>
|
||||
<textarea
|
||||
v-model="editMarkdown"
|
||||
class="result-editor"
|
||||
:maxlength="-1"
|
||||
:auto-height="true"
|
||||
:adjust-position="true"
|
||||
:cursor-spacing="24"
|
||||
placeholder="在这里修改正文"
|
||||
placeholder-style="color: #c0c4cc;"
|
||||
/>
|
||||
</template>
|
||||
<view v-else-if="loading" class="result-streaming">
|
||||
{{ displayMarkdown || 'AI 正在生成内容...' }}
|
||||
</view>
|
||||
<zero-markdown-view v-else-if="markdownContent" class="result-content" :markdown="markdownContent" />
|
||||
<zero-markdown-view v-else-if="displayMarkdown" class="result-content" :markdown="displayMarkdown" />
|
||||
<view v-else class="result-empty">填写创作条件后开始生成</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="result-actions">
|
||||
<view
|
||||
class="result-btn"
|
||||
:class="{ 'result-btn--disabled': loading || (!editing && !hasContent) }"
|
||||
@tap="toggleEdit"
|
||||
>
|
||||
{{ editing ? '完成编辑' : '编辑正文' }}
|
||||
</view>
|
||||
<view class="result-btn" @tap="copyGenerated">复制正文</view>
|
||||
<view class="result-btn" @tap="copyGeneratedTitle">复制标题</view>
|
||||
<view class="result-btn" @tap="clearGenerated">清空结果</view>
|
||||
<view class="result-btn result-btn--ghost" @tap="clearGenerated">清空结果</view>
|
||||
<view
|
||||
v-if="editing && canRevert"
|
||||
class="result-btn result-btn--ghost"
|
||||
@tap="revertEdit"
|
||||
>
|
||||
还原 AI 原文
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -223,6 +253,13 @@
|
||||
const DOC_TYPE_VALUES = DOC_TYPE_ITEMS.map((item) => item.value)
|
||||
const EVENT_TYPE_OPTIONS = ['校园活动', '志愿服务', '会议论坛', '社会实践', '文体活动', '品牌项目']
|
||||
|
||||
/**
|
||||
* 文稿草稿的本地存储 key:存当前这份稿子的正文(编辑过的就存编辑版本)
|
||||
* 和 AI 原文快照。只存一份 —— 工作台一次只做一份稿子,存多份反而让
|
||||
* 「进来该恢复哪一份」没有答案。
|
||||
*/
|
||||
const DRAFT_KEY = 'manuscript_gen_draft_v1'
|
||||
|
||||
const createDefaultForm = () => ({
|
||||
docType: DOC_TYPE_FREE,
|
||||
freeContent: '',
|
||||
@@ -249,6 +286,12 @@
|
||||
streamStarted: false,
|
||||
streamFailed: false,
|
||||
streamResolver: null,
|
||||
// 正文编辑:AI 原文存快照,编辑版本单独放,随时可还原
|
||||
editing: false,
|
||||
editMarkdown: '',
|
||||
hasEdited: false,
|
||||
aiMarkdown: '',
|
||||
aiTitle: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -283,9 +326,36 @@
|
||||
},
|
||||
markdownContent() {
|
||||
return this.parseAssistantContent(this.generatedContent).content.trim()
|
||||
},
|
||||
/** 当前生效的正文:编辑过就用编辑版本,否则用 AI 生成的 */
|
||||
displayMarkdown() {
|
||||
return this.hasEdited ? this.editMarkdown : this.markdownContent
|
||||
},
|
||||
hasContent() {
|
||||
return !!String(this.displayMarkdown || '').trim()
|
||||
},
|
||||
/** 有编辑版本才给「还原」,否则按钮没有意义 */
|
||||
canRevert() {
|
||||
return this.editing && this.hasEdited && !!String(this.aiMarkdown || '').trim()
|
||||
},
|
||||
statusText() {
|
||||
if (this.loading) {
|
||||
return '生成中'
|
||||
}
|
||||
if (this.editing) {
|
||||
return '编辑中'
|
||||
}
|
||||
return this.hasContent ? (this.hasEdited ? '已编辑' : '已生成') : '待生成'
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
if (this.restoreDraft()) {
|
||||
this.showToast('已恢复上次编辑的稿件')
|
||||
}
|
||||
},
|
||||
onUnload() {
|
||||
// 页面被销毁前把编辑版本落本地,避免切走就丢
|
||||
this.persistDraft()
|
||||
this.clearStreamResolver()
|
||||
},
|
||||
methods: {
|
||||
@@ -295,6 +365,113 @@
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
/** 编辑 / 完成编辑:完成时把改动落成编辑版本,并写本地草稿 */
|
||||
toggleEdit() {
|
||||
if (this.loading) {
|
||||
this.showToast('正在生成中,暂不能编辑')
|
||||
return
|
||||
}
|
||||
if (!this.editing) {
|
||||
if (!this.hasContent) {
|
||||
this.showToast('还没有可编辑的正文')
|
||||
return
|
||||
}
|
||||
this.editMarkdown = this.displayMarkdown
|
||||
this.editing = true
|
||||
return
|
||||
}
|
||||
const next = String(this.editMarkdown || '').trim()
|
||||
if (!next) {
|
||||
this.showToast('正文不能为空')
|
||||
return
|
||||
}
|
||||
this.editMarkdown = next
|
||||
// 和 AI 原文一致就不算编辑过,状态标签也不必变成「已编辑」
|
||||
this.hasEdited = next !== this.markdownContent
|
||||
this.editing = false
|
||||
this.persistDraft()
|
||||
this.showToast('编辑已保存')
|
||||
},
|
||||
/**
|
||||
* 还原成 AI 原文:正文和标题一起回滚。
|
||||
* AI 原文还要写回 generatedContent —— markdownContent 是「原文」的唯一来源,
|
||||
* 只改 editMarkdown 的话,hasEdited 一变 false,正文会直接变空。
|
||||
*/
|
||||
revertEdit() {
|
||||
if (!String(this.aiMarkdown || '').trim()) {
|
||||
this.showToast('没有可还原的 AI 原文')
|
||||
return
|
||||
}
|
||||
this.generatedContent = this.aiMarkdown
|
||||
this.generatedTitle = this.aiTitle || this.generatedTitle
|
||||
this.editMarkdown = this.aiMarkdown
|
||||
this.hasEdited = false
|
||||
this.persistDraft()
|
||||
this.showToast('已还原为 AI 原文')
|
||||
},
|
||||
persistDraft() {
|
||||
try {
|
||||
const markdown = String(this.displayMarkdown || '').trim()
|
||||
const title = String(this.generatedTitle || '').trim()
|
||||
if (!markdown && !title) {
|
||||
this.clearDraft()
|
||||
return
|
||||
}
|
||||
uni.setStorageSync(DRAFT_KEY, {
|
||||
title: this.generatedTitle,
|
||||
markdown: this.displayMarkdown,
|
||||
aiTitle: this.aiTitle,
|
||||
aiMarkdown: this.aiMarkdown,
|
||||
hasEdited: this.hasEdited
|
||||
})
|
||||
} catch (error) {
|
||||
// 存不下就算了,不能因为本地存储报错挡住编辑
|
||||
}
|
||||
},
|
||||
clearDraft() {
|
||||
try {
|
||||
uni.removeStorageSync(DRAFT_KEY)
|
||||
} catch (error) {
|
||||
// 同上
|
||||
}
|
||||
},
|
||||
restoreDraft() {
|
||||
try {
|
||||
const draft = uni.getStorageSync(DRAFT_KEY)
|
||||
if (
|
||||
!draft ||
|
||||
(!String(draft.markdown || '').trim() && !String(draft.title || '').trim())
|
||||
) {
|
||||
return false
|
||||
}
|
||||
this.generatedTitle = draft.title || ''
|
||||
this.aiTitle = draft.aiTitle || ''
|
||||
this.aiMarkdown = draft.aiMarkdown || ''
|
||||
// AI 原文回填到生成结果:markdownContent 才是「原文」,状态与还原都靠它
|
||||
this.generatedContent = draft.aiMarkdown || draft.markdown || ''
|
||||
this.hasEdited = !!draft.hasEdited
|
||||
this.editMarkdown = this.hasEdited ? draft.markdown || '' : ''
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
/** 有编辑版本时,重新生成要先问一句:那会把改动冲掉 */
|
||||
confirmDiscardEdit() {
|
||||
if (!this.editing && !this.hasEdited) {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
uni.showModal({
|
||||
title: '重新生成',
|
||||
content: '当前有编辑过的内容,重新生成会丢弃这些修改。',
|
||||
confirmText: '继续生成',
|
||||
cancelText: '取消',
|
||||
success: (res) => resolve(!!res.confirm),
|
||||
fail: () => resolve(false)
|
||||
})
|
||||
})
|
||||
},
|
||||
handleDocTypeChange(event) {
|
||||
const index = Number(event.detail.value)
|
||||
this.form.docType = DOC_TYPE_VALUES[index] || this.form.docType
|
||||
@@ -549,6 +726,13 @@
|
||||
clearGenerated() {
|
||||
this.generatedTitle = ''
|
||||
this.generatedContent = ''
|
||||
// 编辑版本是这份正文的产物,正文清空了就不能留着
|
||||
this.editing = false
|
||||
this.editMarkdown = ''
|
||||
this.hasEdited = false
|
||||
this.aiMarkdown = ''
|
||||
this.aiTitle = ''
|
||||
this.clearDraft()
|
||||
},
|
||||
resetForm() {
|
||||
this.form = createDefaultForm()
|
||||
@@ -656,6 +840,9 @@
|
||||
: '请至少填写稿件标题、亮点内容或上传附件中的一项')
|
||||
return
|
||||
}
|
||||
if (!(await this.confirmDiscardEdit())) {
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.clearGenerated()
|
||||
this.generatedTitle = String(this.form.theme || '').trim()
|
||||
@@ -672,6 +859,13 @@
|
||||
// 兜底:流结束但没收到 __END__ 时也别把页面卡在加载中
|
||||
this.resolveStream()
|
||||
await waitStream
|
||||
// 生成结束:这次的结果记为 AI 原文快照,旧编辑版本与草稿作废
|
||||
this.aiMarkdown = this.markdownContent
|
||||
this.aiTitle = this.generatedTitle
|
||||
this.hasEdited = false
|
||||
this.editMarkdown = ''
|
||||
this.editing = false
|
||||
this.clearDraft()
|
||||
if (this.streamFailed) {
|
||||
// 后端判定工作流失败,正文区已显示失败文案,这里同步提示
|
||||
this.showToast('生成失败,请稍后重试')
|
||||
@@ -717,12 +911,13 @@
|
||||
}
|
||||
},
|
||||
copyGenerated() {
|
||||
if (!this.markdownContent) {
|
||||
// 复制的是当前生效版本:用户改过正文,复制的就该是改过的
|
||||
if (!this.displayMarkdown) {
|
||||
this.showToast('暂无可复制的正文内容')
|
||||
return
|
||||
}
|
||||
uni.setClipboardData({
|
||||
data: this.markdownContent,
|
||||
data: this.displayMarkdown,
|
||||
success: () => {
|
||||
this.showToast('正文已复制')
|
||||
}
|
||||
@@ -850,12 +1045,24 @@
|
||||
}
|
||||
|
||||
.field-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 25rpx;
|
||||
font-weight: 600;
|
||||
color: #2b3037;
|
||||
}
|
||||
|
||||
.field-label__badge {
|
||||
margin-left: 12rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #eef9f1;
|
||||
color: #1f8f49;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.field-picker,
|
||||
.field-textarea {
|
||||
width: 100%;
|
||||
@@ -1069,4 +1276,33 @@
|
||||
.result-empty {
|
||||
color: #9a8f89;
|
||||
}
|
||||
|
||||
.edit-hint {
|
||||
margin-bottom: 14rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.6;
|
||||
color: #9a8f89;
|
||||
}
|
||||
|
||||
.result-editor {
|
||||
width: 100%;
|
||||
min-height: 420rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.85;
|
||||
color: #2b3037;
|
||||
}
|
||||
|
||||
/* 四个操作按钮排 2×2,多出来的「还原」也在同一套网格里 */
|
||||
.result-actions {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.result-btn--ghost {
|
||||
background: #fff;
|
||||
border: 1rpx solid #cfe7fb;
|
||||
}
|
||||
|
||||
.result-btn--disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user