af-mobile-client-vue3 1.3.90 → 1.3.92

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.3.90",
4
+ "version": "1.3.92",
5
5
  "packageManager": "pnpm@10.13.1",
6
6
  "description": "Vue + Vite component lib",
7
7
  "engines": {
@@ -0,0 +1,319 @@
1
+ <script setup lang="ts">
2
+ import { post } from '@af-mobile-client-vue3/services/restTools'
3
+ import useUserStore from '@af-mobile-client-vue3/stores/modules/user'
4
+ import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
5
+ import { Badge as VanBadge, Icon as VanIcon } from 'vant'
6
+ import { onMounted, onUnmounted, ref } from 'vue'
7
+ import { closeWebSocket, initWebSocket } from './webSocket'
8
+ import 'vant/es/badge/style'
9
+ import 'vant/es/icon/style'
10
+
11
+ const userInfo = useUserStore().getUserInfo()
12
+
13
+ // 未读消息数量
14
+ const unreadCount = ref(0)
15
+
16
+ // 打开消息下拉菜单
17
+ const isShowMessageDropdown = ref(false)
18
+
19
+ // 消息列表数据
20
+ const messages = ref<any[]>([])
21
+
22
+ // 组件根元素的引用
23
+ const messageNotificationRef = ref<HTMLElement | null>(null)
24
+
25
+ // 获取消息数据
26
+ async function fetchMessages() {
27
+ const param = {
28
+ userId: `${userInfo.id}:phone`, // |phone
29
+ typeList: '\'phone\'', // phone
30
+ pageNo: 1,
31
+ pageSize: 100,
32
+ }
33
+ const res = await post('/af-system/logic/getNotificationListByType', param)
34
+ messages.value = (res || []).map((item: any) => {
35
+ const event: any = JSON.parse(item.f_event || '{}')
36
+ return {
37
+ id: item.id,
38
+ type: event.type,
39
+ time: item.f_input_date,
40
+ title: event.title || '',
41
+ status: item.f_state,
42
+ content: event.description || '',
43
+ }
44
+ })
45
+ }
46
+
47
+ // 获取未读消息数量
48
+ async function fetchMessagesCount() {
49
+ const param = {
50
+ userId: `${userInfo.id}:phone`, // |phone
51
+ typeList: '\'phone\'', // phone
52
+ }
53
+ const count = await post('/af-system/logic/getNotificationCount', param)
54
+ unreadCount.value = count.count
55
+ }
56
+
57
+ // 处理 WebSocket 消息
58
+ function createMessageCallback(message: any) {
59
+ fetchMessagesCount()
60
+ fetchMessages()
61
+ }
62
+ // 处理 不同的业务
63
+ function sendMessageCallback(message: any) {
64
+ console.log('>>>> 处理业务', JSON.stringify(message))
65
+ if (message.businessType === 'audio') {
66
+ // 播放响铃, 响铃文件为 message.audioName
67
+ mobileUtil.execute({
68
+ funcName: 'playAudio',
69
+ param: {
70
+ fileName: message.audioName,
71
+ },
72
+ callbackFunc: (result: any) => {
73
+ // {"status":"success","data":{"message":"音频播放成功","fileName":"anjian.mp3","path":"assets/audio/anjian.mp3"}}
74
+ },
75
+ })
76
+ }
77
+ }
78
+
79
+ // 全部标记为已读(清空消息列表)
80
+ function markAllAsRead() {
81
+ if (messages.value.length === 0)
82
+ return
83
+
84
+ const messageIds = messages.value.map(msg => msg.id).join(',')
85
+ markAsRead(messageIds)
86
+ }
87
+
88
+ // 单条消息标记为已读
89
+ function markAsRead(id: string | number) {
90
+ post('/af-system/logic/confirmMessage', {
91
+ id,
92
+ f_state: 2,
93
+ }).then(() => {
94
+ fetchMessages()
95
+ fetchMessagesCount()
96
+ })
97
+ }
98
+
99
+ function handleIconClick(message: any) {
100
+ // 乐观更新为已读状态,使图标立刻变绿
101
+ message.status = 2
102
+ setTimeout(() => {
103
+ markAsRead(message.id)
104
+ }, 200)
105
+ }
106
+
107
+ function showMessageDropdown() {
108
+ isShowMessageDropdown.value = !isShowMessageDropdown.value
109
+ if (isShowMessageDropdown.value) {
110
+ fetchMessages()
111
+ }
112
+ }
113
+
114
+ // 处理全局点击事件
115
+ function handleDocumentClick(event: MouseEvent) {
116
+ if (messageNotificationRef.value && !messageNotificationRef.value.contains(event.target as Node)) {
117
+ isShowMessageDropdown.value = false
118
+ }
119
+ }
120
+
121
+ onMounted(() => {
122
+ fetchMessages()
123
+ fetchMessagesCount()
124
+ // 初始化 WebSocket 连接
125
+ initWebSocket(userInfo.id, sendMessageCallback, createMessageCallback)
126
+ // 添加全局点击事件监听器
127
+ document.addEventListener('click', handleDocumentClick)
128
+ })
129
+
130
+ onUnmounted(() => {
131
+ closeWebSocket()
132
+ // 移除全局点击事件监听器
133
+ document.removeEventListener('click', handleDocumentClick)
134
+ })
135
+ </script>
136
+
137
+ <template>
138
+ <div ref="messageNotificationRef" class="message_icon" @click="showMessageDropdown">
139
+ <VanBadge v-if="unreadCount > 0" :content="unreadCount" max="99">
140
+ <VanIcon name="bell" />
141
+ </VanBadge>
142
+ <VanIcon v-else name="bell" />
143
+ </div>
144
+
145
+ <div v-if="isShowMessageDropdown" class="message_overlay" @click="isShowMessageDropdown = false" />
146
+
147
+ <div v-show="isShowMessageDropdown" class="message_dropdown" @click.stop>
148
+ <div class="message_header">
149
+ <span class="label">消息通知</span>
150
+ <span class="action" @click="markAllAsRead">全部已读</span>
151
+ </div>
152
+ <div class="message_list">
153
+ <template v-if="messages.length > 0">
154
+ <div
155
+ v-for="message in messages"
156
+ :key="message.id"
157
+ class="message_item"
158
+ :class="{ info: message.type === 'info', warning: message.type === 'warning' }"
159
+ >
160
+ <!-- 第一行:div1 + div2 -->
161
+ <div class="message_row">
162
+ <!-- div1:标题+时间 -->
163
+ <div class="message_left">
164
+ <p class="message_title">
165
+ {{ message.title }}
166
+ </p>
167
+ <p class="message_time">
168
+ {{ message.time }}
169
+ </p>
170
+ </div>
171
+ <!-- div2:图标 -->
172
+ <div class="message_right">
173
+ <VanIcon
174
+ name="checked"
175
+ :color="message.status === 2 ? '#52c41a' : '#999'"
176
+ size="28"
177
+ @click.stop="handleIconClick(message)"
178
+ />
179
+ </div>
180
+ </div>
181
+ <!-- 第二行:div3 内容 -->
182
+ <div class="message_content">
183
+ <p class="message_text">
184
+ {{ message.content }}
185
+ </p>
186
+ </div>
187
+ </div>
188
+ </template>
189
+ <div v-else class="empty_message">
190
+ 暂无未读消息
191
+ </div>
192
+ </div>
193
+ </div>
194
+ </template>
195
+
196
+ <style scoped lang="less">
197
+ .message_dropdown {
198
+ position: absolute;
199
+ top: 56px;
200
+ left: 0;
201
+ right: 0;
202
+ background-color: #fff;
203
+ box-shadow: 0 12px 24px rgba(0, 0, 0, 0.2);
204
+ z-index: 60;
205
+ max-height: 400px;
206
+ overflow-y: auto;
207
+
208
+ .message_header {
209
+ display: flex;
210
+ justify-content: space-between;
211
+ align-items: center;
212
+ padding: 12px 16px;
213
+ border-bottom: 1px solid #eee;
214
+
215
+ .label {
216
+ font-size: 14px;
217
+ color: #999;
218
+ }
219
+ .action {
220
+ font-size: 14px;
221
+ color: #1989fa;
222
+ cursor: pointer;
223
+ }
224
+ }
225
+
226
+ .message_list {
227
+ .message_item {
228
+ padding: 12px 16px;
229
+ border-bottom: 1px solid #f5f5f5;
230
+
231
+ &.info {
232
+ background-color: #eff6ff;
233
+ }
234
+ &.warning {
235
+ background-color: #fff7ed;
236
+ }
237
+
238
+ .message_row {
239
+ display: flex;
240
+ justify-content: space-between;
241
+ align-items: flex-start;
242
+ }
243
+
244
+ .message_left {
245
+ flex: 1;
246
+ min-width: 0;
247
+
248
+ .message_title {
249
+ font-size: 14px;
250
+ font-weight: 500;
251
+ color: #333;
252
+ margin: 0;
253
+ white-space: nowrap;
254
+ overflow: hidden;
255
+ text-overflow: ellipsis;
256
+ }
257
+
258
+ .message_time {
259
+ font-size: 12px;
260
+ color: #999;
261
+ margin: 4px 0 0 0;
262
+ }
263
+ }
264
+
265
+ .message_right {
266
+ flex: 0 0 auto;
267
+ display: flex;
268
+ align-items: center;
269
+ padding-left: 8px;
270
+
271
+ .van-icon {
272
+ font-size: 18px;
273
+ color: #999;
274
+ }
275
+ }
276
+
277
+ .message_content {
278
+ margin-top: 8px;
279
+
280
+ .message_text {
281
+ border-radius: 4px;
282
+ font-size: 13px;
283
+ color: #666;
284
+ background-color: #fafafc;
285
+ margin: 0;
286
+ padding: 8px 12px;
287
+ line-height: 1.4;
288
+ }
289
+ }
290
+ }
291
+
292
+ .empty_message {
293
+ padding: 60px 0;
294
+ text-align: center;
295
+ color: #999;
296
+ font-size: 14px;
297
+ }
298
+ }
299
+ }
300
+
301
+ .message_overlay {
302
+ position: fixed;
303
+ inset: 0;
304
+ background: rgba(0, 0, 0, 0.45);
305
+ z-index: 50;
306
+ margin-top: 56px;
307
+ margin-bottom: 50px;
308
+ }
309
+ .message_icon {
310
+ padding: 8px;
311
+ position: relative;
312
+ cursor: pointer;
313
+
314
+ .van-icon {
315
+ font-size: 20px;
316
+ color: #666;
317
+ }
318
+ }
319
+ </style>
@@ -0,0 +1,112 @@
1
+ let heartCheckTimer: number | null = null
2
+ let reconnectTimer: number | null = null
3
+ let websocket: WebSocket | null = null
4
+
5
+ let notifyId = ''
6
+ let sendMessageCallback: ((message: any) => void) | null = null
7
+ let createMessageCallback: ((message: any) => void) | null = null
8
+
9
+ function reloadInitWebSocket() {
10
+ initWebSocket(notifyId, sendMessageCallback, createMessageCallback)
11
+ }
12
+
13
+ export function initWebSocket(notify: string | null = null, sendCallback: (message: any) => void, createCallback: (message: any) => void) {
14
+ if (notify) {
15
+ notifyId = notify
16
+ }
17
+ if (sendCallback) {
18
+ sendMessageCallback = sendCallback
19
+ }
20
+ if (createCallback) {
21
+ createMessageCallback = createCallback
22
+ }
23
+
24
+ // 已有连接则不重复连接(包括正在连接或已连接的状态)
25
+ if (websocket && (websocket.readyState === WebSocket.OPEN || websocket.readyState === WebSocket.CONNECTING)) {
26
+ console.log('>>>> webSocket 连接 已存在')
27
+ return
28
+ }
29
+
30
+ // 初始化 WebSocket
31
+ websocket = new WebSocket(`ws:${window.location.host}/socket/af-system/sendMessage?userId=${notifyId}:phone`)
32
+
33
+ // WebSocket 打开连接时触发
34
+ websocket.onopen = () => {
35
+ startHeartCheck()
36
+ }
37
+
38
+ // WebSocket 连接发生错误时触发
39
+ websocket.onerror = () => {
40
+ reconnect()
41
+ }
42
+
43
+ // WebSocket 接收到消息时触发
44
+ websocket.onmessage = (event: MessageEvent) => {
45
+ try {
46
+ if (event.data === 'phone_ping')
47
+ return
48
+ const message = JSON.parse(event.data)
49
+ if (message && message.sendMessage === true) {
50
+ // 接收消息 处理自己的业务, 不再 提示列表中展示 (后端直接调用system-sendMessage)
51
+ sendMessageCallback?.(message)
52
+ }
53
+ else {
54
+ // 消息数据 处理 更新消息列表展示
55
+ createMessageCallback?.(message)
56
+ sendMessageCallback?.(message)
57
+ }
58
+ }
59
+ catch (e) {
60
+ console.error('WebSocket message parsing error:', e)
61
+ }
62
+ }
63
+
64
+ // WebSocket 连接关闭时触发
65
+ websocket.onclose = () => {
66
+ stopHeartCheck()
67
+ reconnect()
68
+ }
69
+ }
70
+
71
+ // 停止心跳检测
72
+ function stopHeartCheck() {
73
+ if (heartCheckTimer !== null) {
74
+ clearInterval(heartCheckTimer)
75
+ heartCheckTimer = null
76
+ }
77
+ }
78
+
79
+ // 心跳机制
80
+ function startHeartCheck() {
81
+ stopHeartCheck()
82
+ heartCheckTimer = window.setInterval(() => {
83
+ if (websocket && websocket.readyState === WebSocket.OPEN) {
84
+ websocket.send(JSON.stringify({ event: 'phone_ping', userList: [`${notifyId}:phone`] }))
85
+ }
86
+ }, 30000)
87
+ }
88
+
89
+ function reconnect() {
90
+ // 防止频繁重连
91
+ if (reconnectTimer !== null) {
92
+ return
93
+ }
94
+
95
+ reconnectTimer = window.setTimeout(() => {
96
+ reloadInitWebSocket() // 重新初始化 WebSocket
97
+ reconnectTimer = null
98
+ }, 5000) // 5秒尝试重连
99
+ }
100
+
101
+ // 关闭 WebSocket 连接
102
+ export function closeWebSocket() {
103
+ if (websocket) {
104
+ websocket.close()
105
+ websocket = null
106
+ }
107
+ stopHeartCheck()
108
+ if (reconnectTimer !== null) {
109
+ clearTimeout(reconnectTimer)
110
+ reconnectTimer = null
111
+ }
112
+ }
@@ -233,8 +233,22 @@ function initComponent() {
233
233
  }
234
234
  }
235
235
 
236
- if (item.btnIcon)
237
- btnList.value.push(item)
236
+ if (item.mobileColumnType === 'mobile_header_column') {
237
+ try {
238
+ // 只有在 item.btnIcon 是字符串时才尝试解析
239
+ if (typeof item.btnIcon === 'string') {
240
+ const parsed = JSON.parse(item.btnIcon)
241
+ // 只有解析结果是数组才赋值
242
+ if (Array.isArray(parsed)) {
243
+ btnList.value = parsed
244
+ }
245
+ }
246
+ }
247
+ catch (e) {
248
+ // 解析失败,不做任何处理(保留原来的值)
249
+ console.error('btnIcon JSON 解析失败:', e)
250
+ }
251
+ }
238
252
 
239
253
  if (result.showSortIcon && item.sortable) {
240
254
  orderList.value.push({
@@ -729,6 +743,30 @@ function handleTitleClick(item: any, event: any) {
729
743
  }
730
744
  }
731
745
 
746
+ // 主标题副标题点击 popover 展示
747
+ const showTitlePopoverKey = ref<string | null>(null)
748
+ const textPopoverLabel = ref('')
749
+ const textPopoverContent = ref('')
750
+
751
+ function openTextPopover(item: any, column: any, key: string, event: any) {
752
+ event.stopPropagation()
753
+
754
+ if (isMultiSelectMode.value) {
755
+ toggleItemSelection(item)
756
+ return
757
+ }
758
+
759
+ textPopoverLabel.value = column?.title || ''
760
+ const rawValue = item?.[column?.dataIndex]
761
+ textPopoverContent.value = rawValue === undefined || rawValue === null ? '' : String(rawValue)
762
+ showTitlePopoverKey.value = key
763
+ }
764
+
765
+ function handleTextPopoverShow(val: boolean, key: string) {
766
+ if (!val && showTitlePopoverKey.value === key)
767
+ showTitlePopoverKey.value = null
768
+ }
769
+
732
770
  function handleCheckboxChange(item: any, checked: boolean) {
733
771
  if (!isMultiSelectMode.value)
734
772
  return
@@ -865,41 +903,70 @@ function handleCheckboxChange(item: any, checked: boolean) {
865
903
  />
866
904
  </div>
867
905
  <div v-for="(column) in mainColumns" :key="`main_${column.dataIndex}`" class="main-title">
868
- <p
869
- class="card_item_title"
870
- :style="handleFunctionStyle(column.styleFunctionForTitle, item)"
871
- :class="{ 'selectable-title': isMultiSelectMode }"
872
- @click="handleTitleClick(item, $event)"
906
+ <VanPopover
907
+ :show="showTitlePopoverKey === `main_${index}_${column.dataIndex}`"
908
+ placement="bottom-start"
909
+ :overlay="false"
910
+ @update:show="(v) => handleTextPopoverShow(v, `main_${index}_${column.dataIndex}`)"
873
911
  >
874
- <XBadge
875
- :style="handleFunctionStyle(column.styleFunctionForValue, item)"
876
- :dict-name="column.dictName"
877
- :dict-value="item[column.dataIndex]"
878
- :dict-type="column.slotType"
879
- :service-name="serviceName"
880
- />
881
- </p>
912
+ <template #reference>
913
+ <p
914
+ class="card_item_title"
915
+ :style="handleFunctionStyle(column.styleFunctionForTitle, item)"
916
+ :class="{ 'selectable-title': isMultiSelectMode }"
917
+ @click="(e) => openTextPopover(item, column, `main_${index}_${column.dataIndex}`, e)"
918
+ >
919
+ <XBadge
920
+ :style="handleFunctionStyle(column.styleFunctionForValue, item)"
921
+ :dict-name="column.dictName"
922
+ :dict-value="item[column.dataIndex]"
923
+ :dict-type="column.slotType"
924
+ :service-name="serviceName"
925
+ />
926
+ </p>
927
+ </template>
928
+ <div style="max-width: 80vw; padding: 6px 8px; font-size: 14px; line-height: 1.5;">
929
+ <div style="white-space: normal; word-break: break-word;">
930
+ {{ textPopoverContent }}
931
+ </div>
932
+ </div>
933
+ </VanPopover>
882
934
  </div>
883
935
  <div v-for="(column) in subTitleColumns" :key="`subtitle_${column.dataIndex}`" class="sub-title">
884
- <p
885
- class="card_item_subtitle"
886
- :style="handleFunctionStyle(column.styleFunctionForTitle, item)"
936
+ <VanPopover
937
+ :show="showTitlePopoverKey === `sub_${index}_${column.dataIndex}`"
938
+ placement="bottom-start"
939
+ :overlay="false"
940
+ @update:show="(v) => handleTextPopoverShow(v, `sub_${index}_${column.dataIndex}`)"
887
941
  >
888
- <XBadge
889
- :style="handleFunctionStyle(column.styleFunctionForValue, item)"
890
- :dict-name="column.dictName"
891
- :dict-value="item[column.dataIndex]"
892
- :dict-type="column.slotType"
893
- :service-name="serviceName"
894
- />
895
- </p>
942
+ <template #reference>
943
+ <p
944
+ class="card_item_subtitle"
945
+ :style="handleFunctionStyle(column.styleFunctionForTitle, item)"
946
+ @click="(e) => openTextPopover(item, column, `sub_${index}_${column.dataIndex}`, e)"
947
+ >
948
+ <XBadge
949
+ :style="handleFunctionStyle(column.styleFunctionForValue, item)"
950
+ :dict-name="column.dictName"
951
+ :dict-value="item[column.dataIndex]"
952
+ :dict-type="column.slotType"
953
+ :service-name="serviceName"
954
+ />
955
+ </p>
956
+ </template>
957
+ <div style="max-width: 80vw; padding: 6px 8px; font-size: 14px; line-height: 1.5;">
958
+ <div style="white-space: normal; word-break: break-word;">
959
+ {{ textPopoverContent }}
960
+ </div>
961
+ </div>
962
+ </VanPopover>
896
963
  </div>
897
964
  <div v-if="!hideAllActions" class="action-buttons">
898
965
  <VanButton
899
966
  v-for="btn in btnList"
900
967
  :key="btn.dataIndex"
901
968
  class="action-button"
902
- :icon="btn.btnIcon"
969
+ :icon="btn"
903
970
  size="small"
904
971
  :disabled="isMultiSelectMode"
905
972
  @click.stop="handleButtonClick(btn, item)"
@@ -1099,6 +1166,8 @@ function handleCheckboxChange(item: any, checked: boolean) {
1099
1166
  align-items: center;
1100
1167
  margin-bottom: 2px;
1101
1168
  width: 100%;
1169
+ flex-wrap: nowrap; // 不换行,按钮保持同一行最右侧
1170
+ overflow: hidden; // 内容过长时在左侧截断
1102
1171
 
1103
1172
  &.multi-select-title-row {
1104
1173
  padding-left: 0;
@@ -1107,6 +1176,12 @@ function handleCheckboxChange(item: any, checked: boolean) {
1107
1176
  .main-title {
1108
1177
  display: inline-flex;
1109
1178
  align-items: center;
1179
+ min-width: 0; // 允许缩小以便省略号生效
1180
+ :deep(.van-popover__wrapper) {
1181
+ display: inline-flex;
1182
+ max-width: 100%;
1183
+ min-width: 0;
1184
+ }
1110
1185
  .card_item_title {
1111
1186
  font-size: var(--van-font-size-lg);
1112
1187
  font-weight: 700;
@@ -1134,6 +1209,13 @@ function handleCheckboxChange(item: any, checked: boolean) {
1134
1209
  display: inline-flex;
1135
1210
  align-items: center;
1136
1211
  margin-left: 8px;
1212
+ min-width: 0; // 允许缩小以便省略号生效
1213
+ max-width: 40%;
1214
+ :deep(.van-popover__wrapper) {
1215
+ display: inline-flex;
1216
+ max-width: 100%;
1217
+ min-width: 0;
1218
+ }
1137
1219
  .card_item_subtitle {
1138
1220
  font-size: var(--van-font-size-md);
1139
1221
  color: var(--van-text-color-2);
@@ -1143,7 +1225,8 @@ function handleCheckboxChange(item: any, checked: boolean) {
1143
1225
  .action-buttons {
1144
1226
  display: flex;
1145
1227
  align-items: center;
1146
- margin-left: auto;
1228
+ margin-left: auto; // 占据最右侧
1229
+ flex-shrink: 0; // 不被压缩,避免换行
1147
1230
  .action-button {
1148
1231
  margin-left: 6px;
1149
1232
  width: 32px;
@@ -188,4 +188,4 @@ export interface PolygonLayerConfig {
188
188
  onClick?: (polygon: PolygonData, event: any) => void
189
189
  /** 多边形数据提供者 */
190
190
  dataProvider?: () => PolygonData[] | Promise<PolygonData[]>
191
- }
191
+ }