feat(ai): 写作模式加「写作参数」面板,可选填活动信息
选了文体(走「内容创作与宣传助手」)后,输入框上方出现可折叠的写作参数面板: 活动类型 chip(含「不限」)、活动主题/时间/地点/参与人员四格,字数并入同一块。 自由创作(走「AI 创意助手」)时整块不出现。 - 面板 body 限高 340rpx 并在内部滚动:不限高会把底部输入框顶到 tabbar 后面。 - 只填参数、不写正文也能发送,气泡上用生成摘要占位。 - 活动类型默认空串(= 不限),不替用户猜类型。
This commit is contained in:
@@ -153,6 +153,88 @@
|
|||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
写作参数:只有选了文体(走「内容创作与宣传助手」)才有意义,
|
||||||
|
自由创作时创意助手只吃一段内容,这块整块不出现。
|
||||||
|
默认收起 —— 手机屏窄,展开会占掉消息区小半屏。
|
||||||
|
-->
|
||||||
|
<view
|
||||||
|
v-if="activeChatMode === 'writing' && writingTemplateMode"
|
||||||
|
class="writing-params"
|
||||||
|
:class="{ 'writing-params--open': writingParamsOpen }"
|
||||||
|
>
|
||||||
|
<view class="writing-params-head" @tap="toggleWritingParams">
|
||||||
|
<text class="writing-params-title">写作参数</text>
|
||||||
|
<text class="writing-params-hint">可选填,填了会一并提交</text>
|
||||||
|
<text v-if="writingParamCount" class="writing-params-count">已填 {{ writingParamCount }} 项</text>
|
||||||
|
<text class="writing-params-action">{{ writingParamsOpen ? '收起' : '展开' }}</text>
|
||||||
|
</view>
|
||||||
|
<scroll-view
|
||||||
|
v-if="writingParamsOpen"
|
||||||
|
scroll-y
|
||||||
|
:show-scrollbar="false"
|
||||||
|
class="writing-params-body"
|
||||||
|
>
|
||||||
|
<scroll-view scroll-x :show-scrollbar="false" class="option-scroll">
|
||||||
|
<view class="option-track">
|
||||||
|
<text class="option-label">类型</text>
|
||||||
|
<view
|
||||||
|
class="option-chip"
|
||||||
|
:class="{ 'option-chip--active': !writingParams.eventType, 'option-chip--disabled': loading }"
|
||||||
|
@tap="pickWritingEventType('')"
|
||||||
|
>不限</view>
|
||||||
|
<view
|
||||||
|
v-for="item in writingEventTypeOptions"
|
||||||
|
:key="item"
|
||||||
|
class="option-chip"
|
||||||
|
:class="{ 'option-chip--active': writingParams.eventType === item, 'option-chip--disabled': loading }"
|
||||||
|
@tap="pickWritingEventType(item)"
|
||||||
|
>{{ item }}</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="param-field">
|
||||||
|
<text class="param-field-label">主题</text>
|
||||||
|
<input
|
||||||
|
v-model="writingParams.theme"
|
||||||
|
class="param-field-input"
|
||||||
|
:disabled="loading"
|
||||||
|
placeholder="春季植树志愿服务"
|
||||||
|
placeholder-class="param-placeholder"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="param-field">
|
||||||
|
<text class="param-field-label">时间</text>
|
||||||
|
<input
|
||||||
|
v-model="writingParams.time"
|
||||||
|
class="param-field-input"
|
||||||
|
:disabled="loading"
|
||||||
|
placeholder="2026年4月8日下午"
|
||||||
|
placeholder-class="param-placeholder"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="param-field">
|
||||||
|
<text class="param-field-label">地点</text>
|
||||||
|
<input
|
||||||
|
v-model="writingParams.location"
|
||||||
|
class="param-field-input"
|
||||||
|
:disabled="loading"
|
||||||
|
placeholder="校史馆广场"
|
||||||
|
placeholder-class="param-placeholder"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="param-field">
|
||||||
|
<text class="param-field-label">人员</text>
|
||||||
|
<input
|
||||||
|
v-model="writingParams.participants"
|
||||||
|
class="param-field-input"
|
||||||
|
:disabled="loading"
|
||||||
|
placeholder="青年志愿者代表120人"
|
||||||
|
placeholder-class="param-placeholder"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 「添加附件」放在最左:它是要点的主操作,不能被滑到屏幕外 -->
|
<!-- 「添加附件」放在最左:它是要点的主操作,不能被滑到屏幕外 -->
|
||||||
<scroll-view
|
<scroll-view
|
||||||
v-if="activeChatMode === 'image'"
|
v-if="activeChatMode === 'image'"
|
||||||
@@ -257,6 +339,7 @@
|
|||||||
import {
|
import {
|
||||||
WRITING_DOC_TYPE_FREE,
|
WRITING_DOC_TYPE_FREE,
|
||||||
WRITING_DOC_TYPE_OPTIONS,
|
WRITING_DOC_TYPE_OPTIONS,
|
||||||
|
WRITING_EVENT_TYPE_OPTIONS,
|
||||||
WRITING_WORD_COUNT_OPTIONS
|
WRITING_WORD_COUNT_OPTIONS
|
||||||
} from './writing-params'
|
} from './writing-params'
|
||||||
import {
|
import {
|
||||||
@@ -292,11 +375,22 @@
|
|||||||
chatModeOptions: CHAT_MODE_OPTIONS,
|
chatModeOptions: CHAT_MODE_OPTIONS,
|
||||||
writingDocTypeOptions: WRITING_DOC_TYPE_OPTIONS,
|
writingDocTypeOptions: WRITING_DOC_TYPE_OPTIONS,
|
||||||
writingWordCountOptions: WRITING_WORD_COUNT_OPTIONS,
|
writingWordCountOptions: WRITING_WORD_COUNT_OPTIONS,
|
||||||
|
writingEventTypeOptions: WRITING_EVENT_TYPE_OPTIONS,
|
||||||
IMAGE_FILE_LIMIT,
|
IMAGE_FILE_LIMIT,
|
||||||
// 写作模式:默认「自由创作」——不套模板,原话直接交给 AI 创意助手
|
// 写作模式:默认「自由创作」——不套模板,原话直接交给 AI 创意助手
|
||||||
writingDocType: WRITING_DOC_TYPE_FREE,
|
writingDocType: WRITING_DOC_TYPE_FREE,
|
||||||
// 字数只在选了文体后才有意义,默认不下发
|
// 字数只在选了文体后才有意义,默认不下发
|
||||||
writingWordCount: 'auto',
|
writingWordCount: 'auto',
|
||||||
|
// 「写作参数」面板:选了文体才出现,默认收起;留空的字段不下发
|
||||||
|
writingParamsOpen: false,
|
||||||
|
writingParams: {
|
||||||
|
// 空串 = 不限,不替用户猜活动类型;选了才下发 event_type
|
||||||
|
eventType: '',
|
||||||
|
theme: '',
|
||||||
|
time: '',
|
||||||
|
location: '',
|
||||||
|
participants: ''
|
||||||
|
},
|
||||||
// 图片模式的参考素材(Dify 开始节点 file-list 变量 user_files)
|
// 图片模式的参考素材(Dify 开始节点 file-list 变量 user_files)
|
||||||
imageFiles: [],
|
imageFiles: [],
|
||||||
uploadingFiles: false,
|
uploadingFiles: false,
|
||||||
@@ -336,11 +430,17 @@
|
|||||||
return this.statusBarHeight + 44
|
return this.statusBarHeight + 44
|
||||||
},
|
},
|
||||||
canSend() {
|
canSend() {
|
||||||
|
if (!this.currentUser.userId || this.loading || this.uploadingFiles) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (this.draft.trim()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 选了文体并填了写作参数时,正文可以先空着:亮点由参数与上游提示词补
|
||||||
return (
|
return (
|
||||||
!!this.currentUser.userId &&
|
this.activeChatMode === 'writing' &&
|
||||||
!!this.draft.trim() &&
|
this.writingTemplateMode &&
|
||||||
!this.loading &&
|
this.writingParamCount > 0
|
||||||
!this.uploadingFiles
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
canPickImageFiles() {
|
canPickImageFiles() {
|
||||||
@@ -350,6 +450,16 @@
|
|||||||
writingTemplateMode() {
|
writingTemplateMode() {
|
||||||
return this.writingDocType !== WRITING_DOC_TYPE_FREE
|
return this.writingDocType !== WRITING_DOC_TYPE_FREE
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 写作参数里用户真正填了几项。
|
||||||
|
* 不算活动类型:它带着默认值「校园活动」,算进去会一打开就显示「已填 1 项」。
|
||||||
|
*/
|
||||||
|
writingParamCount() {
|
||||||
|
const params = this.writingParams || {}
|
||||||
|
return ['theme', 'time', 'location', 'participants'].filter((key) =>
|
||||||
|
String(params[key] || '').trim()
|
||||||
|
).length
|
||||||
|
},
|
||||||
assistantTitle() {
|
assistantTitle() {
|
||||||
if (this.activeChatMode === 'writing') {
|
if (this.activeChatMode === 'writing') {
|
||||||
return 'AI 写作助手'
|
return 'AI 写作助手'
|
||||||
@@ -373,6 +483,9 @@
|
|||||||
return '请先登录后使用 AI 助手'
|
return '请先登录后使用 AI 助手'
|
||||||
}
|
}
|
||||||
if (this.activeChatMode === 'writing') {
|
if (this.activeChatMode === 'writing') {
|
||||||
|
if (this.writingTemplateMode) {
|
||||||
|
return '写清活动亮点、流程、成果等关键信息,会作为「亮点内容」提交;标题地点等可在上方「写作参数」里补'
|
||||||
|
}
|
||||||
return '描述你想创作的内容,例如:帮我写一段迎新晚会的主持人开场词,口语化一点(选了文体后才需要再定字数)'
|
return '描述你想创作的内容,例如:帮我写一段迎新晚会的主持人开场词,口语化一点(选了文体后才需要再定字数)'
|
||||||
}
|
}
|
||||||
if (this.activeChatMode === 'image') {
|
if (this.activeChatMode === 'image') {
|
||||||
@@ -968,8 +1081,9 @@
|
|||||||
* 两条路,都不解析自然语言:
|
* 两条路,都不解析自然语言:
|
||||||
* - 自由创作(未选文体):原话整段作为 `userContent`,后端见 `docType` 为空
|
* - 自由创作(未选文体):原话整段作为 `userContent`,后端见 `docType` 为空
|
||||||
* 就转「AI 创意助手」(只吃 `user_content`);
|
* 就转「AI 创意助手」(只吃 `user_content`);
|
||||||
* - 选了文体:原话整段作为 `highlights`(核心亮点与具体要求),只下发界面
|
* - 选了文体:原话整段作为 `highlights`(核心亮点与具体要求),
|
||||||
* 上显式选过的 docType / number,其余可选字段留空交给上游。
|
* 「写作参数」里填了的活动类型 / 主题 / 时间 / 地点 / 人员原样下发,
|
||||||
|
* 没填的不带(后端 buildManuscriptInputs 会跳过空串)。
|
||||||
*
|
*
|
||||||
* 过去这里用 `parseWritingRequest` 从原话猜 doc_type / 字数 / 主题,
|
* 过去这里用 `parseWritingRequest` 从原话猜 doc_type / 字数 / 主题,
|
||||||
* 猜错会把垃圾值当主控指令(issue 13);现已废弃该解析,见 `./writing-params`。
|
* 猜错会把垃圾值当主控指令(issue 13);现已废弃该解析,见 `./writing-params`。
|
||||||
@@ -984,11 +1098,34 @@
|
|||||||
highlights: content,
|
highlights: content,
|
||||||
files: []
|
files: []
|
||||||
}
|
}
|
||||||
|
const params = this.writingParams || {}
|
||||||
|
const optionalFields = {
|
||||||
|
eventType: params.eventType,
|
||||||
|
theme: params.theme,
|
||||||
|
time: params.time,
|
||||||
|
location: params.location,
|
||||||
|
participants: params.participants
|
||||||
|
}
|
||||||
|
Object.keys(optionalFields).forEach((key) => {
|
||||||
|
const value = String(optionalFields[key] || '').trim()
|
||||||
|
if (value) {
|
||||||
|
payload[key] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
if (this.writingWordCount !== 'auto') {
|
if (this.writingWordCount !== 'auto') {
|
||||||
payload.number = Number(this.writingWordCount)
|
payload.number = Number(this.writingWordCount)
|
||||||
}
|
}
|
||||||
return payload
|
return payload
|
||||||
},
|
},
|
||||||
|
/** 没写正文、只填了写作参数时,气泡上给一句说明,不留空消息 */
|
||||||
|
writingRequestSummary() {
|
||||||
|
const params = this.writingParams || {}
|
||||||
|
const parts = [
|
||||||
|
this.writingDocType,
|
||||||
|
params.theme || params.location || params.time
|
||||||
|
].filter((item) => String(item || '').trim())
|
||||||
|
return `(按写作参数生成:${parts.join(' · ')})`
|
||||||
|
},
|
||||||
formatWritingResult(payload) {
|
formatWritingResult(payload) {
|
||||||
if (typeof payload === 'string') {
|
if (typeof payload === 'string') {
|
||||||
return payload
|
return payload
|
||||||
@@ -1095,6 +1232,18 @@
|
|||||||
}
|
}
|
||||||
this.writingWordCount = value
|
this.writingWordCount = value
|
||||||
},
|
},
|
||||||
|
toggleWritingParams() {
|
||||||
|
if (this.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.writingParamsOpen = !this.writingParamsOpen
|
||||||
|
},
|
||||||
|
pickWritingEventType(value) {
|
||||||
|
if (this.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.writingParams.eventType = value
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 选参考素材。
|
* 选参考素材。
|
||||||
*
|
*
|
||||||
@@ -1398,6 +1547,12 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const query = this.draft.trim()
|
const query = this.draft.trim()
|
||||||
|
// 只填了写作参数、没写正文时,气泡上仍要有一句话,否则是一条空消息
|
||||||
|
const displayQuery =
|
||||||
|
query ||
|
||||||
|
(this.activeChatMode === 'writing' && this.writingTemplateMode
|
||||||
|
? this.writingRequestSummary()
|
||||||
|
: query)
|
||||||
this.loading = true
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
const assistantId = this.createMessageId('assistant')
|
const assistantId = this.createMessageId('assistant')
|
||||||
@@ -1406,7 +1561,7 @@
|
|||||||
id: this.createMessageId('user'),
|
id: this.createMessageId('user'),
|
||||||
anchorId: this.createMessageId('anchor'),
|
anchorId: this.createMessageId('anchor'),
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: query,
|
content: displayQuery,
|
||||||
time: userTime
|
time: userTime
|
||||||
})
|
})
|
||||||
this.messageList.push({
|
this.messageList.push({
|
||||||
@@ -1864,6 +2019,100 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 「写作参数」:只在选了文体时出现,默认收起,展开后按行堆叠(手机屏窄,不做两列) */
|
||||||
|
.writing-params {
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
padding: 12rpx 14rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
border: 1rpx dashed rgba(20, 150, 242, 0.28);
|
||||||
|
background: rgba(255, 255, 255, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.writing-params--open {
|
||||||
|
border-style: solid;
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.writing-params-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.writing-params-title {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1496f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.writing-params-hint {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #a59a94;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.writing-params-count {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 2rpx 12rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(20, 150, 242, 0.12);
|
||||||
|
color: #1496f2;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.writing-params-action {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #1496f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
展开后限高 + 内部滚动:四个输入框 + 类型行全展开约 360rpx,
|
||||||
|
不限高会把输入框顶到 tabbar 后面(H5 与小程序都会)。
|
||||||
|
*/
|
||||||
|
.writing-params-body {
|
||||||
|
max-height: 340rpx;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
padding-top: 12rpx;
|
||||||
|
border-top: 1rpx solid rgba(20, 150, 242, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-field-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 56rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #8a7f79;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-field-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: 56rpx;
|
||||||
|
padding: 0 16rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
border: 1rpx solid rgba(20, 150, 242, 0.2);
|
||||||
|
background: #fff;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #4a5560;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-placeholder {
|
||||||
|
color: #b8ada7;
|
||||||
|
}
|
||||||
|
|
||||||
.attach-list {
|
.attach-list {
|
||||||
margin-bottom: 12rpx;
|
margin-bottom: 12rpx;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,3 +38,19 @@ export const WRITING_WORD_COUNT_OPTIONS = [
|
|||||||
{ label: '1000字', value: '1000' },
|
{ label: '1000字', value: '1000' },
|
||||||
{ label: '1500字', value: '1500' }
|
{ label: '1500字', value: '1500' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活动类型(上游 `event_type`)的候选值。
|
||||||
|
*
|
||||||
|
* 上游开始节点把 event_type 声明成普通 text-input,没有枚举约束,这里给一组常用值
|
||||||
|
* 只是为了让用户不用手打;与写作页 `pages/manuscript/index.vue` 的
|
||||||
|
* `EVENT_TYPE_OPTIONS`、web 侧 `writing-params.ts` 保持一致。
|
||||||
|
*/
|
||||||
|
export const WRITING_EVENT_TYPE_OPTIONS = [
|
||||||
|
'校园活动',
|
||||||
|
'志愿服务',
|
||||||
|
'会议论坛',
|
||||||
|
'社会实践',
|
||||||
|
'文体活动',
|
||||||
|
'品牌项目'
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user