From eaeba6120a0fc47172e35e412370ef5f1dcde73b Mon Sep 17 00:00:00 2001 From: weicw1996 Date: Wed, 21 Oct 2026 13:31:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20=E5=86=99=E4=BD=9C=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E5=8A=A0=E3=80=8C=E5=86=99=E4=BD=9C=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E3=80=8D=E9=9D=A2=E6=9D=BF=EF=BC=8C=E5=8F=AF=E9=80=89=E5=A1=AB?= =?UTF-8?q?=E6=B4=BB=E5=8A=A8=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 选了文体(走「内容创作与宣传助手」)后,输入框上方出现可折叠的写作参数面板: 活动类型 chip(含「不限」)、活动主题/时间/地点/参与人员四格,字数并入同一块。 自由创作(走「AI 创意助手」)时整块不出现。 - 面板 body 限高 340rpx 并在内部滚动:不限高会把底部输入框顶到 tabbar 后面。 - 只填参数、不写正文也能发送,气泡上用生成摘要占位。 - 活动类型默认空串(= 不限),不替用户猜类型。 --- src/pages/assistant/index.vue | 263 +++++++++++++++++++++++++- src/pages/assistant/writing-params.js | 16 ++ 2 files changed, 272 insertions(+), 7 deletions(-) diff --git a/src/pages/assistant/index.vue b/src/pages/assistant/index.vue index 59345ca..95e0d4c 100644 --- a/src/pages/assistant/index.vue +++ b/src/pages/assistant/index.vue @@ -153,6 +153,88 @@ + + + + 写作参数 + 可选填,填了会一并提交 + 已填 {{ writingParamCount }} 项 + {{ writingParamsOpen ? '收起' : '展开' }} + + + + + 类型 + 不限 + {{ item }} + + + + 主题 + + + + 时间 + + + + 地点 + + + + 人员 + + + + + 0 ) }, canPickImageFiles() { @@ -350,6 +450,16 @@ writingTemplateMode() { 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() { if (this.activeChatMode === 'writing') { return 'AI 写作助手' @@ -373,6 +483,9 @@ return '请先登录后使用 AI 助手' } if (this.activeChatMode === 'writing') { + if (this.writingTemplateMode) { + return '写清活动亮点、流程、成果等关键信息,会作为「亮点内容」提交;标题地点等可在上方「写作参数」里补' + } return '描述你想创作的内容,例如:帮我写一段迎新晚会的主持人开场词,口语化一点(选了文体后才需要再定字数)' } if (this.activeChatMode === 'image') { @@ -968,8 +1081,9 @@ * 两条路,都不解析自然语言: * - 自由创作(未选文体):原话整段作为 `userContent`,后端见 `docType` 为空 * 就转「AI 创意助手」(只吃 `user_content`); - * - 选了文体:原话整段作为 `highlights`(核心亮点与具体要求),只下发界面 - * 上显式选过的 docType / number,其余可选字段留空交给上游。 + * - 选了文体:原话整段作为 `highlights`(核心亮点与具体要求), + * 「写作参数」里填了的活动类型 / 主题 / 时间 / 地点 / 人员原样下发, + * 没填的不带(后端 buildManuscriptInputs 会跳过空串)。 * * 过去这里用 `parseWritingRequest` 从原话猜 doc_type / 字数 / 主题, * 猜错会把垃圾值当主控指令(issue 13);现已废弃该解析,见 `./writing-params`。 @@ -984,11 +1098,34 @@ highlights: content, 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') { payload.number = Number(this.writingWordCount) } 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) { if (typeof payload === 'string') { return payload @@ -1095,6 +1232,18 @@ } 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 } const query = this.draft.trim() + // 只填了写作参数、没写正文时,气泡上仍要有一句话,否则是一条空消息 + const displayQuery = + query || + (this.activeChatMode === 'writing' && this.writingTemplateMode + ? this.writingRequestSummary() + : query) this.loading = true try { const assistantId = this.createMessageId('assistant') @@ -1406,7 +1561,7 @@ id: this.createMessageId('user'), anchorId: this.createMessageId('anchor'), role: 'user', - content: query, + content: displayQuery, time: userTime }) this.messageList.push({ @@ -1864,6 +2019,100 @@ 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 { margin-bottom: 12rpx; } diff --git a/src/pages/assistant/writing-params.js b/src/pages/assistant/writing-params.js index 2c584f3..5d7278a 100644 --- a/src/pages/assistant/writing-params.js +++ b/src/pages/assistant/writing-params.js @@ -38,3 +38,19 @@ export const WRITING_WORD_COUNT_OPTIONS = [ { label: '1000字', value: '1000' }, { 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 = [ + '校园活动', + '志愿服务', + '会议论坛', + '社会实践', + '文体活动', + '品牌项目' +]