feat(ai): 写作模式拆成自由创作与模板两条路(issue 18)

自由创作不再解析自然语言:原话整段作为 userContent 下发,docType 留空,由后端转
「AI 创意助手」。选了文体才提交 docType 等结构化条件,走「内容创作与宣传助手」。

- writing-params.js 删掉 parseWritingRequest 与关键字表,只留选项常量;`auto`
  改语义为「自由创作」。
- 字数选择器只在选中文体后显示;未选文体时不下发 number。
- 撤掉图片比例选择器:上游创意助手开始节点没有比例变量,只能拼进 user_content,
  那等于替上游写 prompt。要恢复需先在上游加变量。
- 稿件页同步:默认「自由创作」,选了模板才展开标题、活动类型、时间地点等可选项。
This commit is contained in:
2026-10-21 12:39:48 +08:00
parent cf20f06cba
commit a7a143bb26
3 changed files with 151 additions and 413 deletions
+87 -19
View File
@@ -5,13 +5,14 @@
<view class="hero-kicker">AI CONTENT STUDIO</view>
<view class="hero-title">AI 内容创作与宣传助手</view>
<view class="hero-desc">
面向团委宣传、活动报道、新闻稿和推文草稿的快速生成场景。填写创作条件后,页面会调用
`/ai/manuscriptGen` 并通过 WebSocket 实时接收生成内容。
两种用法:不选模板(自由创作)时只需描述想写什么,由「AI 创意助手」生成;选了稿件类型后再展开活动信息等可选项,
由「内容创作与宣传助手」按体裁生成。结果通过 WebSocket 实时回传。
</view>
<view class="hero-actions">
<view class="hero-action" @tap="fillDemo('news')">活动新闻稿</view>
<view class="hero-action" @tap="fillDemo('wechat')">公众号推文</view>
<view class="hero-action" @tap="fillDemo('speech')">发言稿提纲</view>
<view class="hero-action" @tap="fillDemo('free')">自由创作示例</view>
<view class="hero-action hero-action--primary" @tap="goChatPage">进入问答助手</view>
</view>
</view>
@@ -19,17 +20,6 @@
<view class="panel-card">
<view class="panel-title">创作配置</view>
<view class="field-list">
<view class="field-item">
<view class="field-label">稿件标题</view>
<uv-input
v-model="form.theme"
class="field-input"
placeholder="例如:广西医科大学开展2026年春季志愿服务活动"
placeholder-style="color: #c0c4cc;"
:maxlength="-1"
/>
</view>
<view class="field-item">
<view class="field-label">稿件类型</view>
<picker mode="selector" :range="docTypeLabels" :value="docTypeIndex" @change="handleDocTypeChange">
@@ -37,9 +27,22 @@
<text class="field-value">{{ form.docType || '请选择稿件类型' }}</text>
</view>
</picker>
<view class="field-tip">选「自由创作」= 不套稿件模板,只描述你想写什么;选具体模板后才会展开下方其它可选项。</view>
</view>
<view class="field-item">
<template v-if="templateMode">
<view class="field-item">
<view class="field-label">稿件标题</view>
<uv-input
v-model="form.theme"
class="field-input"
placeholder="例如:广西医科大学开展2026年春季志愿服务活动"
placeholder-style="color: #c0c4cc;"
:maxlength="-1"
/>
</view>
<view class="field-item">
<view class="field-label">活动类型</view>
<picker mode="selector" :range="eventTypeLabels" :value="eventTypeIndex" @change="handleEventTypeChange">
<view class="field-picker">
@@ -127,6 +130,23 @@
</view>
</view>
</view>
</template>
<template v-else>
<view class="field-item">
<view class="field-label">想创作什么</view>
<textarea
v-model="form.freeContent"
class="field-textarea field-textarea--free"
placeholder="直接描述你的创作需求,例如:帮我写一段迎新晚会的主持人开场词,口语化一点"
placeholder-style="color: #c0c4cc;"
maxlength="-1"
/>
</view>
<view class="free-tip">
自由模式不套用稿件模板,内容将交给「AI 创意助手」生成。想按新闻稿、推文等固定体裁写,请在上方选择对应模板。
</view>
</template>
</view>
<view class="submit-bar">
@@ -139,7 +159,8 @@
<view class="hint-block">
<view class="hint-title">提交说明</view>
<view class="hint-text">
页面会先提交 `POST /ai/manuscriptGen`,再通过 WebSocket 流式接收生成结果;如果后端直接返回正文,也会自动兼容显示。
自由模式只提交一段内容(`docType` 留空),由后端转「AI 创意助手」;选了模板才提交 `docType`、活动类型、时间地点等结构化条件。
两种都会先 `POST /ai/manuscriptGen`,再通过 WebSocket 流式接收生成结果;如果后端直接返回正文,也会自动兼容显示。
</view>
</view>
</view>
@@ -207,11 +228,14 @@
const THINK_OPEN_TAG = '<think>'
const THINK_CLOSE_TAG = '</think>'
const DOC_TYPE_OPTIONS = ['新闻稿', '公众号推文', '宣传稿', '活动总结', '发言稿', '短视频脚本']
const DOC_TYPE_FREE = '自由创作'
// 「自由创作」是默认项:不选具体模板时只填一段内容,后端转「AI 创意助手」。
const DOC_TYPE_OPTIONS = [DOC_TYPE_FREE, '新闻稿', '公众号推文', '宣传稿', '活动总结', '发言稿', '短视频脚本']
const EVENT_TYPE_OPTIONS = ['校园活动', '志愿服务', '会议论坛', '社会实践', '文体活动', '品牌项目']
const createDefaultForm = () => ({
docType: '新闻稿',
docType: DOC_TYPE_FREE,
freeContent: '',
eventType: '校园活动',
theme: '',
time: '',
@@ -254,7 +278,16 @@
const index = this.eventTypeLabels.indexOf(this.form.eventType)
return index > -1 ? index : 0
},
freeMode() {
return !this.form.docType || this.form.docType === DOC_TYPE_FREE
},
templateMode() {
return !this.freeMode
},
canSubmit() {
if (this.freeMode) {
return !!String(this.form.freeContent || '').trim()
}
return !!(
String(this.form.theme || '').trim() ||
String(this.form.highlights || '').trim() ||
@@ -329,6 +362,13 @@
}
},
buildPayload() {
if (this.freeMode) {
// 自由模式:不发 docType,只发一段用户原话。后端据此转「AI 创意助手」。
return {
docType: '',
userContent: String(this.form.freeContent || '').trim()
}
}
return {
docType: this.form.docType,
eventType: this.form.eventType,
@@ -640,6 +680,11 @@
this.uploadedFiles = []
},
fillDemo(type) {
if (type === 'free') {
this.form.docType = DOC_TYPE_FREE
this.form.freeContent = '帮我写一段迎新晚会的主持人开场词,口语化一点,时长两分钟左右'
return
}
if (type === 'news') {
this.form.theme = '广西医科大学青年志愿者开展“清洁校园”志愿服务活动'
this.form.docType = '新闻稿'
@@ -731,7 +776,9 @@
return
}
if (!this.canSubmit) {
this.showToast('请至少填写稿件标题、亮点内容或上传附件中的一项')
this.showToast(this.freeMode
? '请先描述你想创作什么'
: '请至少填写稿件标题、亮点内容或上传附件中的一项')
return
}
this.loading = true
@@ -740,7 +787,9 @@
this.responseSummary = '正在建立 WebSocket 连接...'
try {
await this.connectWebSocket()
this.responseSummary = '连接成功,已提交生成任务,等待流式返回...'
this.responseSummary = this.freeMode
? '自由创作已提交给 AI 创意助手,生成完成后一次性返回...'
: '连接成功,已提交生成任务,等待流式返回...'
const waitStream = this.createStreamWaiter()
const result = await manuscriptGen(this.buildPayload())
if (!this.streamStarted && this.shouldTreatAsDirectResult(result)) {
@@ -911,6 +960,25 @@
min-height: 220rpx;
}
.field-textarea--free {
min-height: 300rpx;
}
.field-tip,
.free-tip {
margin-top: 10rpx;
font-size: 22rpx;
line-height: 1.7;
color: #8f959e;
}
.free-tip {
padding: 18rpx 22rpx;
border-radius: 18rpx;
background: rgba(20, 150, 242, 0.06);
color: #6b7d8f;
}
.field-value {
color: #303133;
}