af-mobile-client-vue3 1.4.53 → 1.4.54
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 +1 -1
- package/src/App.vue +14 -2
- package/src/components/common/MateChat/MateChat.vue +248 -0
- package/src/components/common/MateChat/apiService.ts +254 -0
- package/src/components/common/MateChat/assets/035-avatar-13.svg +1 -0
- package/src/components/common/MateChat/components/MateChatHeader.vue +253 -0
- package/src/components/common/MateChat/components/PromptList/PromptList.vue +189 -0
- package/src/components/common/MateChat/components/PromptList/index.ts +1 -0
- package/src/components/common/MateChat/useMateChat.ts +212 -0
- package/src/components/common/otherCharge/ChargePrintSelectorAndRemarks.vue +137 -137
- package/src/components/common/otherCharge/CodePayment.vue +357 -357
- package/src/components/common/otherCharge/FileUploader.vue +602 -602
- package/src/components/common/otherCharge/GridFileUploader.vue +846 -846
- package/src/components/common/otherCharge/PaymentMethodSelector.vue +202 -202
- package/src/components/common/otherCharge/PaymentMethodSelectorCard.vue +45 -45
- package/src/components/common/otherCharge/ReceiptModal.vue +273 -273
- package/src/components/common/otherCharge/index.ts +43 -43
- package/src/components/data/OtherCharge/OtherChargeItemModal.vue +547 -547
- package/src/components/data/XFormGroup/doc/DeviceForm.vue +1 -1
- package/src/components/data/XFormGroup/doc/UserForm.vue +1 -1
- package/src/components/data/XReportGrid/XReportDemo.vue +33 -33
- package/src/components/data/XReportGrid/print.js +184 -184
- package/src/utils/queryFormDefaultRangePicker.ts +57 -57
- package/src/utils/timeUtil.ts +27 -27
- package/src/views/component/MateChat/MateChatView.vue +30 -233
- package/src/views/component/XCellListView/index.vue +107 -138
- package/src/views/component/XFormGroupView/index.vue +78 -82
- package/src/views/component/XFormView/index.vue +41 -46
- package/src/views/component/MateChat/apiService.ts +0 -104
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useUserStore } from '@af-mobile-client-vue3/stores/modules/user'
|
|
3
|
+
import { Empty, Icon, Loading, Popup } from 'vant'
|
|
4
|
+
import { ref } from 'vue'
|
|
5
|
+
import 'vant/es/popup/style'
|
|
6
|
+
import 'vant/es/empty/style'
|
|
7
|
+
import 'vant/es/loading/style'
|
|
8
|
+
import 'vant/es/icon/style'
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
/**
|
|
12
|
+
* 头部左侧 Logo 图片
|
|
13
|
+
*/
|
|
14
|
+
logoImage: string
|
|
15
|
+
/**
|
|
16
|
+
* 头部标题文案
|
|
17
|
+
*/
|
|
18
|
+
title: string
|
|
19
|
+
/**
|
|
20
|
+
* 历史对话弹层标题
|
|
21
|
+
*/
|
|
22
|
+
historyTitle?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SessionItem {
|
|
26
|
+
id: string
|
|
27
|
+
title: string
|
|
28
|
+
lastTime: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Emits {
|
|
32
|
+
/**
|
|
33
|
+
* 选择某条历史会话
|
|
34
|
+
*/
|
|
35
|
+
(e: 'selectSession', session: SessionItem): void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
39
|
+
historyTitle: '对话历史',
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const emit = defineEmits<Emits>()
|
|
43
|
+
|
|
44
|
+
const userStore = useUserStore()
|
|
45
|
+
|
|
46
|
+
const showHistory = ref(false)
|
|
47
|
+
const isLoading = ref(false)
|
|
48
|
+
const sessionList = ref<SessionItem[]>([])
|
|
49
|
+
|
|
50
|
+
function handleOpenHistory() {
|
|
51
|
+
showHistory.value = true
|
|
52
|
+
fetchSessions()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function handleSelectSession(session: SessionItem) {
|
|
56
|
+
emit('selectSession', session)
|
|
57
|
+
showHistory.value = false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function fetchSessions() {
|
|
61
|
+
isLoading.value = true
|
|
62
|
+
try {
|
|
63
|
+
const userInfo = userStore.getUserInfo()
|
|
64
|
+
const userId = userInfo?.id ?? 'guest'
|
|
65
|
+
const userName = userInfo?.name ?? '当前用户'
|
|
66
|
+
|
|
67
|
+
// 模拟接口延迟
|
|
68
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
69
|
+
|
|
70
|
+
// 模拟会话列表数据
|
|
71
|
+
sessionList.value = [
|
|
72
|
+
{
|
|
73
|
+
id: `${userId}-1`,
|
|
74
|
+
title: `${userName} 最近的缴费咨询`,
|
|
75
|
+
lastTime: '2025-01-01 10:00',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: `${userId}-2`,
|
|
79
|
+
title: `${userName} 燃气报修记录`,
|
|
80
|
+
lastTime: '2025-01-02 14:30',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: `${userId}-3`,
|
|
84
|
+
title: `${userName} 安全用气相关问题`,
|
|
85
|
+
lastTime: '2025-01-03 09:15',
|
|
86
|
+
},
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
isLoading.value = false
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
</script>
|
|
94
|
+
|
|
95
|
+
<template>
|
|
96
|
+
<div class="matechat-header-wrapper">
|
|
97
|
+
<McHeader :logo-img="props.logoImage" :title="props.title" :logo-clickable="false">
|
|
98
|
+
<template #operationArea>
|
|
99
|
+
<div class="matechat-header-history-btn" @click="handleOpenHistory">
|
|
100
|
+
<Icon name="clock-o" size="16" />
|
|
101
|
+
</div>
|
|
102
|
+
</template>
|
|
103
|
+
</McHeader>
|
|
104
|
+
|
|
105
|
+
<Popup
|
|
106
|
+
v-model:show="showHistory"
|
|
107
|
+
position="center"
|
|
108
|
+
round
|
|
109
|
+
:overlay="true"
|
|
110
|
+
class="matechat-history-popup"
|
|
111
|
+
>
|
|
112
|
+
<div class="matechat-history-card">
|
|
113
|
+
<div class="matechat-history-card__header">
|
|
114
|
+
<span class="matechat-history-card__title">
|
|
115
|
+
{{ props.historyTitle }}
|
|
116
|
+
</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="matechat-history-card__content">
|
|
119
|
+
<div v-if="isLoading" class="matechat-history-card__loading">
|
|
120
|
+
<Loading size="24px" />
|
|
121
|
+
</div>
|
|
122
|
+
<template v-else>
|
|
123
|
+
<div
|
|
124
|
+
v-if="sessionList.length"
|
|
125
|
+
class="matechat-history-card__list"
|
|
126
|
+
>
|
|
127
|
+
<div
|
|
128
|
+
v-for="session in sessionList"
|
|
129
|
+
:key="session.id"
|
|
130
|
+
class="matechat-history-card__item"
|
|
131
|
+
@click="handleSelectSession(session)"
|
|
132
|
+
>
|
|
133
|
+
<div class="matechat-history-card__item-title">
|
|
134
|
+
{{ session.title }}
|
|
135
|
+
</div>
|
|
136
|
+
<div class="matechat-history-card__item-time">
|
|
137
|
+
{{ session.lastTime }}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div v-else class="matechat-history-card__empty">
|
|
142
|
+
<Empty description="无数据" />
|
|
143
|
+
</div>
|
|
144
|
+
</template>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</Popup>
|
|
148
|
+
</div>
|
|
149
|
+
</template>
|
|
150
|
+
|
|
151
|
+
<style scoped lang="less">
|
|
152
|
+
.matechat-header-wrapper {
|
|
153
|
+
position: relative;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.matechat-header-history-btn {
|
|
157
|
+
display: inline-flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
justify-content: center;
|
|
160
|
+
width: 32px;
|
|
161
|
+
height: 32px;
|
|
162
|
+
border-radius: 50%;
|
|
163
|
+
background-color: rgba(255, 255, 255, 0.8);
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
transition: all 0.2s ease;
|
|
166
|
+
|
|
167
|
+
&:hover {
|
|
168
|
+
background-color: rgba(255, 255, 255, 1);
|
|
169
|
+
transform: translateY(-1px);
|
|
170
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.matechat-history-popup {
|
|
175
|
+
.matechat-history-card {
|
|
176
|
+
width: 320px;
|
|
177
|
+
max-width: 80vw;
|
|
178
|
+
background-color: #ffffff;
|
|
179
|
+
border-radius: 16px;
|
|
180
|
+
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
|
|
181
|
+
padding: 16px 16px 12px;
|
|
182
|
+
box-sizing: border-box;
|
|
183
|
+
|
|
184
|
+
&__header {
|
|
185
|
+
display: flex;
|
|
186
|
+
align-items: center;
|
|
187
|
+
justify-content: space-between;
|
|
188
|
+
margin-bottom: 12px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
&__title {
|
|
192
|
+
font-size: 16px;
|
|
193
|
+
font-weight: 600;
|
|
194
|
+
color: #252b3a;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
&__content {
|
|
198
|
+
min-height: 120px;
|
|
199
|
+
max-height: 260px;
|
|
200
|
+
overflow: auto;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
&__loading {
|
|
204
|
+
display: flex;
|
|
205
|
+
align-items: center;
|
|
206
|
+
justify-content: center;
|
|
207
|
+
padding: 24px 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
&__list {
|
|
211
|
+
display: flex;
|
|
212
|
+
flex-direction: column;
|
|
213
|
+
gap: 8px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
&__item {
|
|
217
|
+
padding: 10px 8px;
|
|
218
|
+
border-radius: 8px;
|
|
219
|
+
cursor: pointer;
|
|
220
|
+
transition: all 0.2s ease;
|
|
221
|
+
|
|
222
|
+
&:hover {
|
|
223
|
+
background-color: #f5f6f9;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
&__item-title {
|
|
228
|
+
font-size: 14px;
|
|
229
|
+
color: #252b3a;
|
|
230
|
+
margin-bottom: 2px;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
&__item-time {
|
|
234
|
+
font-size: 12px;
|
|
235
|
+
color: #a0a4af;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
&__empty {
|
|
239
|
+
padding: 16px 0 8px;
|
|
240
|
+
|
|
241
|
+
:deep(.van-empty__image) {
|
|
242
|
+
width: 80px;
|
|
243
|
+
height: 80px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
:deep(.van-empty__description) {
|
|
247
|
+
font-size: 13px;
|
|
248
|
+
color: #a0a4af;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
</style>
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Icon } from 'vant'
|
|
3
|
+
import 'vant/es/icon/style'
|
|
4
|
+
|
|
5
|
+
// 定义 props 接口
|
|
6
|
+
interface IconConfig {
|
|
7
|
+
name: string
|
|
8
|
+
color?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface PromptItem {
|
|
12
|
+
value: string
|
|
13
|
+
label: string
|
|
14
|
+
iconConfig?: IconConfig
|
|
15
|
+
desc?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
list: PromptItem[]
|
|
20
|
+
direction?: 'horizontal' | 'vertical'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 定义 emits
|
|
24
|
+
interface Emits {
|
|
25
|
+
(e: 'itemClick', item: PromptItem): void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
29
|
+
direction: 'vertical',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const emit = defineEmits<Emits>()
|
|
33
|
+
|
|
34
|
+
// 处理点击事件
|
|
35
|
+
function handleItemClick(item: PromptItem) {
|
|
36
|
+
emit('itemClick', item)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 将 icon-info-o 等图标名称转换为 Vant 图标名称
|
|
40
|
+
function getVantIconName(iconName: string): string {
|
|
41
|
+
const iconMap: Record<string, string> = {
|
|
42
|
+
'icon-info-o': 'info-o',
|
|
43
|
+
'icon-star': 'star',
|
|
44
|
+
'icon-priority': 'fire',
|
|
45
|
+
}
|
|
46
|
+
return iconMap[iconName] || iconName.replace('icon-', '')
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<template>
|
|
51
|
+
<div class="prompt-list" :class="[`prompt-list--${direction}`]">
|
|
52
|
+
<div
|
|
53
|
+
v-for="item in list"
|
|
54
|
+
:key="item.value"
|
|
55
|
+
class="prompt-item"
|
|
56
|
+
@click="handleItemClick(item)"
|
|
57
|
+
>
|
|
58
|
+
<div v-if="item.iconConfig" class="prompt-item__icon">
|
|
59
|
+
<Icon
|
|
60
|
+
:name="getVantIconName(item.iconConfig.name)"
|
|
61
|
+
:color="item.iconConfig.color"
|
|
62
|
+
size="20"
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="prompt-item__content">
|
|
66
|
+
<div class="prompt-item__label">
|
|
67
|
+
{{ item.label }}
|
|
68
|
+
</div>
|
|
69
|
+
<div v-if="item.desc" class="prompt-item__desc">
|
|
70
|
+
{{ item.desc }}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<style scoped lang="less">
|
|
78
|
+
.prompt-list {
|
|
79
|
+
display: flex;
|
|
80
|
+
gap: 12px;
|
|
81
|
+
width: 100%;
|
|
82
|
+
|
|
83
|
+
&--horizontal {
|
|
84
|
+
flex-direction: row;
|
|
85
|
+
flex-wrap: wrap;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
&--vertical {
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.prompt-item {
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: flex-start;
|
|
96
|
+
gap: 12px;
|
|
97
|
+
padding: 16px;
|
|
98
|
+
background: linear-gradient(135deg, rgba(94, 124, 224, 0.05) 0%, rgba(58, 194, 149, 0.05) 100%);
|
|
99
|
+
border-radius: 12px;
|
|
100
|
+
border: 1px solid rgba(94, 124, 224, 0.1);
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
transition: all 0.3s ease;
|
|
103
|
+
flex: 1;
|
|
104
|
+
min-width: 0;
|
|
105
|
+
|
|
106
|
+
&:hover {
|
|
107
|
+
background: linear-gradient(135deg, rgba(94, 124, 224, 0.1) 0%, rgba(58, 194, 149, 0.1) 100%);
|
|
108
|
+
border-color: rgba(94, 124, 224, 0.2);
|
|
109
|
+
transform: translateY(-2px);
|
|
110
|
+
box-shadow: 0 4px 12px rgba(94, 124, 224, 0.15);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
&:active {
|
|
114
|
+
transform: translateY(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
&__icon {
|
|
118
|
+
flex-shrink: 0;
|
|
119
|
+
display: flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
justify-content: center;
|
|
122
|
+
width: 32px;
|
|
123
|
+
height: 32px;
|
|
124
|
+
background: rgba(255, 255, 255, 0.8);
|
|
125
|
+
border-radius: 8px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
&__content {
|
|
129
|
+
flex: 1;
|
|
130
|
+
min-width: 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
&__label {
|
|
134
|
+
font-size: 14px;
|
|
135
|
+
font-weight: 500;
|
|
136
|
+
color: #252b3a;
|
|
137
|
+
line-height: 20px;
|
|
138
|
+
margin-bottom: 4px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
&__desc {
|
|
142
|
+
font-size: 12px;
|
|
143
|
+
color: #71757f;
|
|
144
|
+
line-height: 16px;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 水平方向时的特殊样式
|
|
149
|
+
.prompt-list--horizontal {
|
|
150
|
+
.prompt-item {
|
|
151
|
+
flex: 1;
|
|
152
|
+
min-width: 140px;
|
|
153
|
+
|
|
154
|
+
&__content {
|
|
155
|
+
display: flex;
|
|
156
|
+
flex-direction: column;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 垂直方向时的特殊样式
|
|
162
|
+
.prompt-list--vertical {
|
|
163
|
+
.prompt-item {
|
|
164
|
+
width: 100%;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 简化版样式(用于快捷提示)
|
|
169
|
+
.shortcut-prompt {
|
|
170
|
+
.prompt-item {
|
|
171
|
+
padding: 10px 14px;
|
|
172
|
+
min-width: auto;
|
|
173
|
+
|
|
174
|
+
&__icon {
|
|
175
|
+
width: 24px;
|
|
176
|
+
height: 24px;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
&__label {
|
|
180
|
+
font-size: 13px;
|
|
181
|
+
margin-bottom: 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
&__desc {
|
|
185
|
+
display: none;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as PromptList } from './PromptList.vue'
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { ChatBizResult, ChatStreamCallbacks } from './apiService'
|
|
2
|
+
import { showToast } from 'vant'
|
|
3
|
+
import { ref } from 'vue'
|
|
4
|
+
import { chatBiz, chatCompletionsStream } from './apiService'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* MateChat 组件内部使用的消息结构
|
|
8
|
+
*/
|
|
9
|
+
export interface MateChatMessage {
|
|
10
|
+
from: 'user' | 'model' | 'service'
|
|
11
|
+
content: string
|
|
12
|
+
loading?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 封装 MateChat 核心对话逻辑的组合式函数
|
|
17
|
+
* - 负责 startPage / inputValue / messages 等状态
|
|
18
|
+
* - 根据 useStream 决定使用非流式(chatBiz)还是流式(chatCompletionsStream)
|
|
19
|
+
*/
|
|
20
|
+
export function useMateChat(options?: { useStream?: boolean }) {
|
|
21
|
+
const startPage = ref(true)
|
|
22
|
+
const inputValue = ref('')
|
|
23
|
+
const messages = ref<MateChatMessage[]>([])
|
|
24
|
+
const useStream = options?.useStream === true
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 新建会话:回到起始页并清空历史消息
|
|
28
|
+
*/
|
|
29
|
+
function newConversation() {
|
|
30
|
+
startPage.value = true
|
|
31
|
+
messages.value = []
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 发送一条消息
|
|
36
|
+
* - 推入用户消息
|
|
37
|
+
* - 添加一条 loading 的模型消息
|
|
38
|
+
* - 根据 useStream 调用对应的接口
|
|
39
|
+
*/
|
|
40
|
+
async function onSubmit(evt: string) {
|
|
41
|
+
if (!evt.trim()) {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
inputValue.value = ''
|
|
46
|
+
startPage.value = false
|
|
47
|
+
|
|
48
|
+
// 用户发送消息
|
|
49
|
+
messages.value.push({
|
|
50
|
+
from: 'user',
|
|
51
|
+
content: evt,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// 添加 loading 状态的 model 消息
|
|
55
|
+
const loadingMessageIndex = messages.value.length
|
|
56
|
+
messages.value.push({
|
|
57
|
+
from: 'model',
|
|
58
|
+
content: '',
|
|
59
|
+
loading: true,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
if (!useStream) {
|
|
63
|
+
// 非流式:一次性拿到完整结果
|
|
64
|
+
try {
|
|
65
|
+
const result: ChatBizResult = await chatBiz(evt)
|
|
66
|
+
|
|
67
|
+
if (result.type === 'transfer') {
|
|
68
|
+
// 移除 loading 消息
|
|
69
|
+
messages.value.splice(loadingMessageIndex, 1)
|
|
70
|
+
// 添加人工客服消息
|
|
71
|
+
messages.value.push({
|
|
72
|
+
from: 'service',
|
|
73
|
+
content: '您好,客服xxx工号xxx为你服务。功能开发中,敬请期待。',
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// 正常消息:替换 loading 为模型回复
|
|
78
|
+
messages.value[loadingMessageIndex] = {
|
|
79
|
+
from: 'model',
|
|
80
|
+
content: result.content,
|
|
81
|
+
loading: false,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error: any) {
|
|
86
|
+
// 处理错误
|
|
87
|
+
console.error('聊天请求失败:', error)
|
|
88
|
+
messages.value[loadingMessageIndex] = {
|
|
89
|
+
from: 'model',
|
|
90
|
+
content: '抱歉,服务暂时不可用,请稍后再试。',
|
|
91
|
+
loading: false,
|
|
92
|
+
}
|
|
93
|
+
showToast(error?.message || '请求失败,请稍后再试')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 流式:使用 FastGPT SSE,增量更新最后一条模型消息内容,并在前缀为 {"msgType":"transfer"} 时转人工
|
|
100
|
+
const transferPrefix = '{"msgType":"transfer"'
|
|
101
|
+
let checkedTransfer = false
|
|
102
|
+
let transferHandled = false
|
|
103
|
+
let prefixBuffer = ''
|
|
104
|
+
|
|
105
|
+
const callbacks: ChatStreamCallbacks = {
|
|
106
|
+
onMessage(chunk) {
|
|
107
|
+
if (transferHandled) {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 尚未判断是否为转人工前缀
|
|
112
|
+
if (!checkedTransfer) {
|
|
113
|
+
const trimmed = chunk.trimStart()
|
|
114
|
+
if (!trimmed) {
|
|
115
|
+
// 纯空白,等待下一帧
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 第一个有效字符不是 { ,本次不会是转人工 JSON,后续直接按普通文本处理
|
|
120
|
+
if (!prefixBuffer && trimmed[0] !== '{') {
|
|
121
|
+
checkedTransfer = true
|
|
122
|
+
const msg = messages.value[loadingMessageIndex]
|
|
123
|
+
if (!msg) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
msg.content += chunk
|
|
127
|
+
msg.loading = true
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 有可能是 JSON,累积前缀做精准匹配
|
|
132
|
+
prefixBuffer += trimmed
|
|
133
|
+
|
|
134
|
+
// 如果当前前缀还是 transferPrefix 的前缀,继续等后续 chunk
|
|
135
|
+
if (transferPrefix.startsWith(prefixBuffer)) {
|
|
136
|
+
// 还没完整匹配上整个标识,继续等待
|
|
137
|
+
if (prefixBuffer.length < transferPrefix.length) {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (prefixBuffer.startsWith(transferPrefix)) {
|
|
143
|
+
// 确认是转人工:移除 loading 模型气泡,插入客服气泡
|
|
144
|
+
messages.value.splice(loadingMessageIndex, 1)
|
|
145
|
+
messages.value.push({
|
|
146
|
+
from: 'service',
|
|
147
|
+
content: '您好,客服xxx工号xxx为你服务。功能开发中,敬请期待。',
|
|
148
|
+
})
|
|
149
|
+
transferHandled = true
|
|
150
|
+
checkedTransfer = true
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 前缀与约定不匹配,当作普通内容处理,并不再尝试转人工识别
|
|
155
|
+
checkedTransfer = true
|
|
156
|
+
const msg = messages.value[loadingMessageIndex]
|
|
157
|
+
if (!msg) {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
msg.content += prefixBuffer
|
|
161
|
+
msg.loading = true
|
|
162
|
+
prefixBuffer = ''
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 已经判断过不会转人工,正常流式追加内容
|
|
167
|
+
const msg = messages.value[loadingMessageIndex]
|
|
168
|
+
if (!msg) {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
msg.content += chunk
|
|
172
|
+
msg.loading = true
|
|
173
|
+
},
|
|
174
|
+
onComplete() {
|
|
175
|
+
if (transferHandled) {
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
const msg = messages.value[loadingMessageIndex]
|
|
179
|
+
if (!msg) {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
msg.loading = false
|
|
183
|
+
},
|
|
184
|
+
onError(error) {
|
|
185
|
+
console.error('聊天请求失败:', error)
|
|
186
|
+
const msg = messages.value[loadingMessageIndex]
|
|
187
|
+
if (!msg) {
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
msg.content = '抱歉,服务暂时不可用,请稍后再试。'
|
|
191
|
+
msg.loading = false
|
|
192
|
+
showToast((error as any)?.message || '请求失败,请稍后再试')
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
await chatCompletionsStream(evt, callbacks)
|
|
198
|
+
}
|
|
199
|
+
catch (error: any) {
|
|
200
|
+
// 兜底错误处理(理论上 callbacks.onError 已经处理)
|
|
201
|
+
console.error('聊天流式请求异常:', error)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
startPage,
|
|
207
|
+
inputValue,
|
|
208
|
+
messages,
|
|
209
|
+
newConversation,
|
|
210
|
+
onSubmit,
|
|
211
|
+
}
|
|
212
|
+
}
|