af-mobile-client-vue3 1.4.56 → 1.4.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "af-mobile-client-vue3",
3
3
  "type": "module",
4
- "version": "1.4.56",
4
+ "version": "1.4.57",
5
5
  "packageManager": "pnpm@10.13.1",
6
6
  "description": "Vue + Vite component lib",
7
7
  "engines": {
package/src/App.vue CHANGED
@@ -99,6 +99,7 @@ input[type='password']::-ms-clear {
99
99
  <style scoped>
100
100
  .app-wrapper {
101
101
  width: 100%;
102
+ height: 100vh;
102
103
  position: relative;
103
104
  }
104
105
  </style>
@@ -6,7 +6,6 @@ import MateChatHeader from '@af-mobile-client-vue3/components/common/MateChat/co
6
6
  import { PromptList } from '@af-mobile-client-vue3/components/common/MateChat/components/PromptList'
7
7
  import { useMateChat } from '@af-mobile-client-vue3/components/common/MateChat/composables/useMateChat'
8
8
  import { useDebounceFn } from '@vueuse/core'
9
- import { Button as VanButton } from 'vant'
10
9
  import { computed, nextTick, ref, watch } from 'vue'
11
10
  import 'vant/es/image-preview/style'
12
11
 
@@ -110,6 +109,7 @@ function handleSelectSession(session: { chatId: string, title: string, lastTime:
110
109
  :app-id="props.config.appId"
111
110
  :app-key="props.config.appKey"
112
111
  @select-session="handleSelectSession"
112
+ @new-conversation="newConversation"
113
113
  />
114
114
  <McLayoutContent
115
115
  v-if="startPage"
@@ -161,20 +161,13 @@ function handleSelectSession(session: { chatId: string, title: string, lastTime:
161
161
  style="flex: 1"
162
162
  @item-click="onSubmit($event.label)"
163
163
  />
164
- <VanButton
165
- style="margin-left: auto"
166
- icon="add"
167
- title="新建对话"
168
- size="small"
169
- round
170
- @click="newConversation"
171
- />
172
164
  </div>
173
165
  <McLayoutSender>
174
166
  <McInput
175
167
  :value="inputValue"
176
168
  placeholder="请输入您的问题,我会为您解答"
177
169
  :max-length="2000"
170
+ :autosize="true"
178
171
  @change="(e) => (inputValue = e)"
179
172
  @submit="onSubmit"
180
173
  />
@@ -186,7 +179,7 @@ function handleSelectSession(session: { chatId: string, title: string, lastTime:
186
179
  .chat-card {
187
180
  width: 100%;
188
181
  max-width: 1200px;
189
- height: calc(100% - 40px);
182
+ height: 100%;
190
183
  background: #ffffff;
191
184
  border-radius: 24px;
192
185
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
@@ -205,7 +198,6 @@ function handleSelectSession(session: { chatId: string, title: string, lastTime:
205
198
  /* 移动端适配 */
206
199
  @media (max-width: 768px) {
207
200
  .chat-card {
208
- height: calc(100% - 16px);
209
201
  border-radius: 16px;
210
202
  padding: 8px;
211
203
  }
@@ -2,12 +2,13 @@
2
2
  import type { ChatHistoryItem } from '@af-mobile-client-vue3/components/common/MateChat/types'
3
3
  import { getHistories } from '@af-mobile-client-vue3/components/common/MateChat/apiService'
4
4
  import { useChatHistoryCache } from '@af-mobile-client-vue3/components/common/MateChat/composables/useChatHistoryCache'
5
- import { Empty, Icon, Loading, Popup } from 'vant'
5
+ import { Empty, Icon, Loading, Popup, Popover as VanPopover } from 'vant'
6
6
  import { ref } from 'vue'
7
7
  import 'vant/es/popup/style'
8
8
  import 'vant/es/empty/style'
9
9
  import 'vant/es/loading/style'
10
10
  import 'vant/es/icon/style'
11
+ import 'vant/es/popover/style'
11
12
 
12
13
  interface Props {
13
14
  /**
@@ -47,6 +48,10 @@ interface Emits {
47
48
  * 选择某条历史会话
48
49
  */
49
50
  (e: 'selectSession', session: SessionItem): void
51
+ /**
52
+ * 新建对话
53
+ */
54
+ (e: 'newConversation'): void
50
55
  }
51
56
 
52
57
  const props = withDefaults(defineProps<Props>(), {
@@ -58,6 +63,12 @@ const emit = defineEmits<Emits>()
58
63
  const showHistory = ref(false)
59
64
  const isLoading = ref(false)
60
65
  const sessionList = ref<SessionItem[]>([])
66
+ const showMenu = ref(false)
67
+
68
+ const menuActions: { text: string, key: 'new' | 'history', icon: string }[] = [
69
+ { text: '新对话', key: 'new', icon: 'add-o' },
70
+ { text: '历史对话', key: 'history', icon: 'clock-o' },
71
+ ]
61
72
 
62
73
  // 使用历史会话缓存
63
74
  const { getCachedHistory, setCachedHistory } = useChatHistoryCache()
@@ -72,6 +83,20 @@ function handleSelectSession(session: SessionItem) {
72
83
  showHistory.value = false
73
84
  }
74
85
 
86
+ /**
87
+ * 处理下拉菜单点击
88
+ */
89
+ function handleMenuSelect(action: { text: string, key: 'new' | 'history', icon?: string }) {
90
+ showMenu.value = false
91
+ if (action.key === 'new') {
92
+ emit('newConversation')
93
+ return
94
+ }
95
+ if (action.key === 'history') {
96
+ handleOpenHistory()
97
+ }
98
+ }
99
+
75
100
  /**
76
101
  * 格式化时间字符串为日期时间字符串
77
102
  */
@@ -139,11 +164,24 @@ async function fetchSessions() {
139
164
 
140
165
  <template>
141
166
  <div class="matechat-header-wrapper">
142
- <McHeader :logo-img="showTitle ? props.logoImage : ''" :title="showTitle ? props.title : ''" :logo-clickable="false">
167
+ <McHeader
168
+ :logo-img="showTitle ? props.logoImage : ''"
169
+ :title="showTitle ? props.title : ''"
170
+ :logo-clickable="false"
171
+ >
143
172
  <template #operationArea>
144
- <div class="matechat-header-history-btn" @click="handleOpenHistory">
145
- <Icon name="clock-o" size="16" />
146
- </div>
173
+ <VanPopover
174
+ v-model:show="showMenu"
175
+ placement="bottom-end"
176
+ :actions="menuActions"
177
+ @select="handleMenuSelect"
178
+ >
179
+ <template #reference>
180
+ <div class="matechat-header-history-btn">
181
+ <Icon name="ellipsis" size="16" />
182
+ </div>
183
+ </template>
184
+ </VanPopover>
147
185
  </template>
148
186
  </McHeader>
149
187
 
@@ -204,15 +242,17 @@ async function fetchSessions() {
204
242
  justify-content: center;
205
243
  width: 32px;
206
244
  height: 32px;
207
- border-radius: 50%;
208
- background-color: rgba(255, 255, 255, 0.8);
245
+ border-radius: 8px;
246
+ background-color: rgba(255, 255, 255, 0.9);
247
+ border: 1px solid rgba(0, 0, 0, 0.08);
248
+ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.12);
209
249
  cursor: pointer;
210
250
  transition: all 0.2s ease;
211
251
 
212
252
  &:hover {
213
- background-color: rgba(255, 255, 255, 1);
214
- transform: translateY(-1px);
215
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
253
+ background-color: #ffffff;
254
+ transform: translateY(-0.5px);
255
+ box-shadow: 0 2px 6px rgba(15, 23, 42, 0.16);
216
256
  }
217
257
  }
218
258
 
@@ -221,7 +261,6 @@ async function fetchSessions() {
221
261
  width: 320px;
222
262
  max-width: 80vw;
223
263
  background-color: #ffffff;
224
- border-radius: 16px;
225
264
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
226
265
  padding: 16px 16px 12px;
227
266
  box-sizing: border-box;
@@ -260,7 +299,7 @@ async function fetchSessions() {
260
299
 
261
300
  &__item {
262
301
  padding: 10px 8px;
263
- border-radius: 8px;
302
+ border-radius: 0;
264
303
  cursor: pointer;
265
304
  transition: all 0.2s ease;
266
305
 
@@ -138,97 +138,77 @@ export function useMateChat(config: MateChatConfig) {
138
138
  return
139
139
  }
140
140
 
141
- // 流式:使用 FastGPT SSE,增量更新最后一条模型消息内容,并在前缀为 {"msgType":"transfer"} 时转人工
142
- const transferPrefix = '{"msgType":"transfer"'
143
- let checkedTransfer = false
144
- let transferHandled = false
145
- let prefixBuffer = ''
141
+ // 流式:使用 FastGPT SSE,增量更新最后一条模型消息内容
142
+ // 规则:
143
+ // 1)每次收到 chunk 都立即累加到 msg.content,保证实时流式效果
144
+ // 2)在累加后的内容上做一次“是否为转人工 JSON 消息”的前缀粗判:以 {"msgType 开头
145
+ // 3)如果判断为转人工,则保持 loading 状态,等待 onComplete 再统一处理整条消息
146
+ // 4)如果判断不是转人工,则当作普通文本流式展示,onComplete 时仅结束 loading
147
+ const transferPrefix = '{"msgType'
148
+ let checkedTransfer = false // 是否已经做过类型判定
149
+ let isTransferMessage = false // 是否判定为转人工消息
146
150
 
147
151
  const callbacks: ChatStreamCallbacks = {
148
152
  onMessage(chunk) {
149
- if (transferHandled) {
153
+ const msg = messages.value[loadingMessageIndex]
154
+ if (!msg) {
150
155
  return
151
156
  }
157
+ // 1. 实时累加内容,保证流式体验
158
+ msg.content += chunk
152
159
 
153
- // 尚未判断是否为转人工前缀
160
+ // 2. 如果还没有做过类型判定,基于当前累积内容做一次前缀粗判
154
161
  if (!checkedTransfer) {
155
- const trimmed = chunk.trimStart()
156
- if (!trimmed) {
157
- // 纯空白,等待下一帧
162
+ const trimmed = msg.content.trimStart()
163
+ // 内容太短,无法判断,继续等待更多 chunk
164
+ if (!trimmed || trimmed.length < transferPrefix.length) {
158
165
  return
159
166
  }
160
167
 
161
- // 第一个有效字符不是 { ,本次不会是转人工 JSON,后续直接按普通文本处理
162
- if (!prefixBuffer && trimmed[0] !== '{') {
163
- checkedTransfer = true
164
- const msg = messages.value[loadingMessageIndex]
165
- if (!msg) {
166
- return
167
- }
168
- msg.content += chunk
169
- msg.loading = true
170
- return
171
- }
172
-
173
- // 有可能是 JSON,累积前缀做精准匹配
174
- prefixBuffer += trimmed
175
-
176
- // 如果当前前缀还是 transferPrefix 的前缀,继续等后续 chunk
177
- if (transferPrefix.startsWith(prefixBuffer)) {
178
- // 还没完整匹配上整个标识,继续等待
179
- if (prefixBuffer.length < transferPrefix.length) {
180
- return
181
- }
168
+ if (trimmed.startsWith(transferPrefix)) {
169
+ // {"msgType 开头,标记为“可能是转人工消息”
170
+ isTransferMessage = true
182
171
  }
183
-
184
- if (prefixBuffer.startsWith(transferPrefix)) {
185
- // 确认是转人工:移除 loading 模型气泡,插入客服气泡
186
- messages.value.splice(loadingMessageIndex, 1)
187
- messages.value.push({
188
- from: 'service',
189
- content: '您好,客服xxx工号xxx为你服务。功能开发中,敬请期待。',
190
- })
191
- transferHandled = true
192
- checkedTransfer = true
193
- // 更新缓存:追加用户消息和客服消息(在 onComplete 中统一处理)
194
- return
172
+ else {
173
+ // 不以 {"msgType 开头,当作普通文本处理
174
+ isTransferMessage = false
175
+ msg.loading = false
195
176
  }
196
-
197
- // 前缀与约定不匹配,当作普通内容处理,并不再尝试转人工识别
198
177
  checkedTransfer = true
199
- const msg = messages.value[loadingMessageIndex]
200
- if (!msg) {
201
- return
202
- }
203
- msg.content += prefixBuffer
204
- msg.loading = true
205
- prefixBuffer = ''
206
- return
207
178
  }
208
-
209
- // 已经判断过不会转人工,正常流式追加内容
210
- const msg = messages.value[loadingMessageIndex]
211
- if (!msg) {
212
- return
213
- }
214
- msg.content += chunk
215
- msg.loading = true
216
179
  },
217
180
  onComplete() {
218
- if (transferHandled) {
219
- // 转人工情况:更新缓存
220
- appendMessages(chatId.value, [
221
- { from: 'user', content: evt },
222
- { from: 'service', content: '您好,客服xxx工号xxx为你服务。功能开发中,敬请期待。' },
223
- ])
224
- return
225
- }
226
181
  const msg = messages.value[loadingMessageIndex]
227
182
  if (!msg) {
228
183
  return
229
184
  }
185
+ // 根据前面判定结果决定如何处理整条消息
186
+ if (isTransferMessage) {
187
+ // 尝试按 JSON 解析,判断是否真的是转人工指令
188
+ try {
189
+ const parsed = JSON.parse(msg.content.trim())
190
+ if (parsed && parsed.msgType === 'transfer') {
191
+ // 确认为转人工:移除模型气泡,插入客服气泡
192
+ messages.value.splice(loadingMessageIndex, 1)
193
+ messages.value.push({
194
+ from: 'service',
195
+ content: '您好,客服xxx工号xxx为你服务。功能开发中,敬请期待。',
196
+ })
197
+ // 转人工情况:更新缓存
198
+ appendMessages(chatId.value, [
199
+ { from: 'user', content: evt },
200
+ { from: 'service', content: '您好,客服xxx工号xxx为你服务。功能开发中,敬请期待。' },
201
+ ])
202
+ return
203
+ }
204
+ }
205
+ catch {
206
+ // 解析失败则回退为普通文本处理
207
+ }
208
+ }
209
+
210
+ // 普通消息:结束 loading,按完整文本渲染并写入缓存
230
211
  msg.loading = false
231
- // 流式完成后更新缓存:追加用户消息和完整的模型回复
232
212
  appendMessages(chatId.value, [
233
213
  { from: 'user', content: evt },
234
214
  { from: 'model', content: msg.content, loading: false },
@@ -366,6 +366,7 @@ onMounted(() => {
366
366
  #mate-chat-view {
367
367
  /* 外层渐变背景容器 */
368
368
  width: 100%;
369
+ height: 100%;
369
370
  min-height: 100%;
370
371
  /* 背景色通过 :style 绑定从配置中获取 */
371
372
  padding: 20px;