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.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/src/App.vue +14 -2
  3. package/src/components/common/MateChat/MateChat.vue +248 -0
  4. package/src/components/common/MateChat/apiService.ts +254 -0
  5. package/src/components/common/MateChat/assets/035-avatar-13.svg +1 -0
  6. package/src/components/common/MateChat/components/MateChatHeader.vue +253 -0
  7. package/src/components/common/MateChat/components/PromptList/PromptList.vue +189 -0
  8. package/src/components/common/MateChat/components/PromptList/index.ts +1 -0
  9. package/src/components/common/MateChat/useMateChat.ts +212 -0
  10. package/src/components/common/otherCharge/ChargePrintSelectorAndRemarks.vue +137 -137
  11. package/src/components/common/otherCharge/CodePayment.vue +357 -357
  12. package/src/components/common/otherCharge/FileUploader.vue +602 -602
  13. package/src/components/common/otherCharge/GridFileUploader.vue +846 -846
  14. package/src/components/common/otherCharge/PaymentMethodSelector.vue +202 -202
  15. package/src/components/common/otherCharge/PaymentMethodSelectorCard.vue +45 -45
  16. package/src/components/common/otherCharge/ReceiptModal.vue +273 -273
  17. package/src/components/common/otherCharge/index.ts +43 -43
  18. package/src/components/data/OtherCharge/OtherChargeItemModal.vue +547 -547
  19. package/src/components/data/XFormGroup/doc/DeviceForm.vue +1 -1
  20. package/src/components/data/XFormGroup/doc/UserForm.vue +1 -1
  21. package/src/components/data/XReportGrid/XReportDemo.vue +33 -33
  22. package/src/components/data/XReportGrid/print.js +184 -184
  23. package/src/utils/queryFormDefaultRangePicker.ts +57 -57
  24. package/src/utils/timeUtil.ts +27 -27
  25. package/src/views/component/MateChat/MateChatView.vue +30 -233
  26. package/src/views/component/XCellListView/index.vue +107 -138
  27. package/src/views/component/XFormGroupView/index.vue +78 -82
  28. package/src/views/component/XFormView/index.vue +41 -46
  29. 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
+ }