vue2-client 1.22.15 → 1.22.16

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,6 +1,6 @@
1
1
  {
2
2
  "name": "vue2-client",
3
- "version": "1.22.15",
3
+ "version": "1.22.16",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "serve": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint",
@@ -99,7 +99,9 @@ export default {
99
99
  emptyText: { type: String, default: '暂无图片' },
100
100
  columns: { type: Number, default: 3, validator: v => v >= 1 && v <= 6 },
101
101
  imageSize: { type: [String, Number], default: 90 },
102
- gap: { type: [String, Number], default: 10 }
102
+ gap: { type: [String, Number], default: 10 },
103
+ /** 为 true 时禁止内部预览弹窗;点击图片触发 image-click 事件,由父组件接管预览逻辑 */
104
+ noPreview: { type: Boolean, default: false }
103
105
  },
104
106
  data () {
105
107
  return {
@@ -186,7 +188,13 @@ export default {
186
188
  this.galleryVisible = false
187
189
  },
188
190
 
191
+ /** 点击图片:noPreview 模式下仅抛事件,由父组件处理预览;否则打开内部画廊 */
189
192
  handleImageClick (index) {
193
+ if (this.noPreview) {
194
+ const allUrls = this.fileIds.map((fd, i) => this.urlsMap[i] || (fd && typeof fd === 'object' && fd.url) || '')
195
+ this.$emit('image-click', { index, url: allUrls[index] || '', fileData: this.fileIds[index], allUrls })
196
+ return
197
+ }
190
198
  this.currentIndex = index
191
199
  this.galleryVisible = true
192
200
  },
@@ -201,6 +209,11 @@ export default {
201
209
 
202
210
  nextImage () {
203
211
  if (this.currentIndex < this.fileIds.length - 1) this.currentIndex++
212
+ },
213
+
214
+ /** 返回当前所有已解析的图片 URL 数组(外部通过此方法拉取,而非事件推) */
215
+ getAllResolvedUrls () {
216
+ return this.fileIds.map((fd, i) => this.urlsMap[i] || (fd && typeof fd === 'object' && fd.url) || '')
204
217
  }
205
218
  }
206
219
  }
@@ -40,6 +40,7 @@
40
40
  <!-- 底部工具栏 -->
41
41
  <div class="preview-toolbar bottom-toolbar" @wheel.prevent.stop>
42
42
  <div class="toolbar-content">
43
+ <slot name="toolbarPrepend" />
43
44
  <a-tooltip title="放大">
44
45
  <a-button type="primary" icon="zoom-in" @click="zoomIn" size="small" class="toolbar-btn" />
45
46
  </a-tooltip>
@@ -71,6 +72,9 @@
71
72
  </a-tooltip>
72
73
  </div>
73
74
  </div>
75
+ <div class="bottom-slot">
76
+ <slot name="thumbnailStrip" />
77
+ </div>
74
78
  </div>
75
79
  </template>
76
80
 
@@ -305,10 +309,10 @@ export default {
305
309
  // 鼠标滚轮缩放(居中缩放,与按钮行为一致)
306
310
  handleWheel(event) {
307
311
  event.preventDefault()
308
-
312
+
309
313
  const delta = event.deltaY > 0 ? -ZOOM_CONFIG.STEP : ZOOM_CONFIG.STEP
310
314
  const newScale = Math.min(Math.max(ZOOM_CONFIG.MIN, this.scale + delta), ZOOM_CONFIG.MAX)
311
-
315
+
312
316
  // 如果缩放值没有变化,直接返回
313
317
  if (newScale === this.scale) return
314
318
 
@@ -486,11 +490,19 @@ export default {
486
490
  margin-bottom: 8px;
487
491
  }
488
492
  }
489
-
493
+ .bottom-slot{
494
+ position: fixed;
495
+ bottom: 10px;
496
+ left: 0;
497
+ right: 0;
498
+ z-index: 1011;
499
+ display: flex;
500
+ justify-content: center;
501
+ }
490
502
  // 底部工具栏样式
491
503
  .bottom-toolbar {
492
504
  position: fixed;
493
- bottom: 40px;
505
+ bottom: 60px;
494
506
  left: 0;
495
507
  right: 0;
496
508
  z-index: 1011;
@@ -54,6 +54,9 @@
54
54
  :columns="2"
55
55
  :image-size="60"
56
56
  :gap="8"
57
+ :no-preview="true"
58
+ @image-click="handleFileImageClick"
59
+ :ref="el => registerFig(el)"
57
60
  />
58
61
  </div>
59
62
  <div v-else class="violation-images-empty"></div>
@@ -102,6 +105,9 @@
102
105
  :columns="2"
103
106
  :image-size="60"
104
107
  :gap="8"
108
+ :no-preview="true"
109
+ @image-click="handleFileImageClick"
110
+ :ref="el => registerFig(el)"
105
111
  />
106
112
  <span v-else></span></div>
107
113
  <div class="inconsistent-item">
@@ -164,6 +170,9 @@
164
170
  :columns="2"
165
171
  :image-size="60"
166
172
  :gap="8"
173
+ :no-preview="true"
174
+ @image-click="handleFileImageClick"
175
+ :ref="el => registerFig(el)"
167
176
  />
168
177
  <span v-else></span></div>
169
178
  <div class="danger-item">{{ item.aiVal }}</div>
@@ -222,6 +231,20 @@ export default {
222
231
  return unmatched + add
223
232
  },
224
233
  },
234
+ data () {
235
+ return {
236
+ /** 储存本组件内所有 FileImageGroup 实例(通过函数 ref 注册) */
237
+ figInstances: []
238
+ }
239
+ },
240
+ watch: {
241
+ /** 设备列表变化时清空实例缓存,重新渲染时会重新注册 */
242
+ deviceList: {
243
+ handler () { this.figInstances = [] },
244
+ deep: true,
245
+ immediate: true
246
+ }
247
+ },
225
248
  methods: {
226
249
 
227
250
  /**
@@ -287,12 +310,36 @@ export default {
287
310
  : []
288
311
  },
289
312
 
290
- /**
291
- * 预览隐患照片
292
- */
293
- handlePreviewHazardPhotos(hazardPhotos, currentIndex = 0) {
294
- const photos = hazardPhotos.map(photo => photo.url)
295
- this.$emit('preview-photos', photos, currentIndex)
313
+ /** 通过函数 ref 收集 FileImageGroup 实例 */
314
+ registerFig (fig) {
315
+ if (fig && this.figInstances.indexOf(fig) === -1) {
316
+ this.figInstances.push(fig)
317
+ }
318
+ },
319
+
320
+ /** 收集本组件内所有 FileImageGroup 的已解析图片 URL,去重后返回 */
321
+ _collectAllUrls () {
322
+ const urls = []
323
+ this.figInstances.forEach(fig => {
324
+ if (fig && fig.getAllResolvedUrls) {
325
+ fig.getAllResolvedUrls().forEach(u => { if (u && urls.indexOf(u) === -1) urls.push(u) })
326
+ }
327
+ })
328
+ return urls
329
+ },
330
+
331
+ /** 父组件通过 ref 调用的公共方法:获取本组件全部图片 URL */
332
+ getAllPhotoUrls () {
333
+ return this._collectAllUrls()
334
+ },
335
+
336
+ /** 点击 FileImageGroup 图片:收集全组件 URL 抛给父组件统一预览 */
337
+ handleFileImageClick({ index, url }) {
338
+ const urls = this._collectAllUrls()
339
+ if (urls.length > 0) {
340
+ const globalIdx = url ? urls.indexOf(url) : Math.min(index, urls.length - 1)
341
+ this.$emit('preview-photos', urls, globalIdx >= 0 ? globalIdx : 0)
342
+ }
296
343
  }
297
344
  }
298
345
  }
@@ -123,27 +123,14 @@
123
123
  </div>
124
124
  </a-card>
125
125
 
126
- <!-- 图片全屏预览 -->
127
- <image-preview-modal
128
- v-if="previewVisible"
129
- :visible="previewVisible"
130
- :image-url="currentPreviewUrl"
131
- :file-id="currentPreviewUrl ? null : currentPreviewFileId"
132
- :service-name="currentPreviewUrl ? '' : injectedServiceName"
133
- :image-name="`照片${previewIndex + 1}`"
134
- :append-to-body="true"
135
- @close="previewVisible = false"
136
- />
137
126
  </div>
138
127
  </template>
139
128
 
140
129
  <script>
141
- import ImagePreviewModal from '@vue2-client/base-client/components/common/ImagePreviewModal/ImagePreviewModal.vue'
142
130
  import { createFileImageService } from '@vue2-client/utils/fileImageService'
143
131
 
144
132
  export default {
145
133
  name: 'CheckItems',
146
- components: { ImagePreviewModal },
147
134
  inject: {
148
135
  serviceName: { default: '' }
149
136
  },
@@ -159,9 +146,6 @@ export default {
159
146
  },
160
147
  data () {
161
148
  return {
162
- previewVisible: false,
163
- previewIndex: 0,
164
- previewPhotos: [],
165
149
  thumbUrlMap: {},
166
150
  thumbStateMap: {}
167
151
  }
@@ -169,22 +153,6 @@ export default {
169
153
  computed: {
170
154
  injectedServiceName () {
171
155
  return this.serviceName || ''
172
- },
173
- currentPhoto () {
174
- return this.previewPhotos[this.previewIndex] || null
175
- },
176
- currentPreviewUrl () {
177
- if (!this.currentPhoto) return ''
178
- if (this.currentPhoto.url) return this.currentPhoto.url
179
- if (this.currentPhoto.fileId) {
180
- const key = this.thumbCacheKey(this.currentPhoto)
181
- return this.thumbUrlMap[key] || ''
182
- }
183
- return ''
184
- },
185
- currentPreviewFileId () {
186
- if (!this.currentPhoto) return null
187
- return this.currentPhoto.fileId || null
188
156
  }
189
157
  },
190
158
  watch: {
@@ -213,6 +181,30 @@ export default {
213
181
  })
214
182
  },
215
183
 
184
+ /** 父组件通过 ref 调用的公共方法:获取本组件全部图片 URL(含已解析的 fileId 缩略图) */
185
+ getAllPhotoUrls () {
186
+ const urls = []
187
+ if (!this.checkItems) return urls
188
+ this.checkItems.forEach(category => {
189
+ if (!category.items) return
190
+ category.items.forEach(item => {
191
+ if (item.photos) {
192
+ item.photos.forEach(p => {
193
+ const u = p.url || this.thumbUrlMap[this.thumbCacheKey(p)]
194
+ if (u && urls.indexOf(u) === -1) urls.push(u)
195
+ })
196
+ }
197
+ if (this.isResultPhotos(item.result)) {
198
+ this.parseResultPhotos(item.result).forEach(p => {
199
+ const u = p.url
200
+ if (u && urls.indexOf(u) === -1) urls.push(u)
201
+ })
202
+ }
203
+ })
204
+ })
205
+ return urls
206
+ },
207
+
216
208
  /**
217
209
  * 缩略图缓存必须用字符串键。若用对象作 fileId,JS 会统一变成 "[object Object]",导致所有缩略图共用一张图。
218
210
  * 优先使用业务侧已生成的 photo.id,其次从 fileId 上解析 id。
@@ -281,9 +273,10 @@ export default {
281
273
  * 打开预览
282
274
  */
283
275
  openPreview (photos, index) {
284
- this.previewPhotos = photos
285
- this.previewIndex = index
286
- this.previewVisible = true
276
+ const urls = photos.map(p => p.url || this.thumbUrlMap[this.thumbCacheKey(p)] || '').filter(Boolean)
277
+ if (urls.length > 0) {
278
+ this.$emit('preview-photos', urls, Math.min(index, urls.length - 1))
279
+ }
287
280
  },
288
281
 
289
282
  /**
@@ -59,6 +59,9 @@
59
59
  :image-size="60"
60
60
  :gap="8"
61
61
  :alt-prefix="field.label + '图片'"
62
+ :no-preview="true"
63
+ @image-click="handleFileImageClick"
64
+ :ref="el => registerFig(el)"
62
65
  />
63
66
  </div>
64
67
  <span v-else class="tag-value">{{ formatFieldValue(field) }}</span>
@@ -93,10 +96,12 @@
93
96
  :gap="8"
94
97
  :alt-prefix="device.name + '照片'"
95
98
  empty-text="暂无设备照片"
99
+ :no-preview="true"
100
+ @image-click="handleFileImageClick"
101
+ :ref="el => registerFig(el)"
96
102
  />
97
103
  </div>
98
-
99
- <!-- 隐患照片 -->
104
+ <!-- 隐患照片 -->
100
105
  <div class="hazard-photos-section" v-if="device.hazardPhotos && device.hazardPhotos.length > 0">
101
106
  <div class="section-title">隐患照片</div>
102
107
  <!-- 隐患照片使用FileImageGroup -->
@@ -109,6 +114,9 @@
109
114
  :gap="8"
110
115
  :alt-prefix="device.name + '隐患'"
111
116
  empty-text="暂无隐患照片"
117
+ :no-preview="true"
118
+ @image-click="handleFileImageClick"
119
+ :ref="el => registerFig(el)"
112
120
  />
113
121
  <!-- 备用显示方式 -->
114
122
  <div v-else class="hazard-photos-grid">
@@ -166,6 +174,20 @@ export default {
166
174
  default: () => []
167
175
  }
168
176
  },
177
+ data () {
178
+ return {
179
+ /** 储存本组件内所有 FileImageGroup 实例(通过函数 ref 注册) */
180
+ figInstances: []
181
+ }
182
+ },
183
+ watch: {
184
+ /** 设备列表变化时清空实例缓存,下一帧重新渲染时会重新注册 */
185
+ deviceList: {
186
+ handler () { this.figInstances = [] },
187
+ deep: true,
188
+ immediate: true
189
+ }
190
+ },
169
191
  methods: {
170
192
  /**
171
193
  * 根据设备信息获取图标类型
@@ -256,12 +278,48 @@ export default {
256
278
  return new Date().toISOString().substring(0, 16).replace('T', ' ')
257
279
  },
258
280
 
281
+ /** 通过函数 ref 收集 FileImageGroup 实例 */
282
+ registerFig (fig) {
283
+ if (fig && this.figInstances.indexOf(fig) === -1) {
284
+ this.figInstances.push(fig)
285
+ }
286
+ },
287
+
288
+ /** 收集本组件内所有 FileImageGroup 的已解析图片 URL,去重后返回 */
289
+ _collectAllUrls () {
290
+ const urls = []
291
+ this.figInstances.forEach(fig => {
292
+ if (fig && fig.getAllResolvedUrls) {
293
+ fig.getAllResolvedUrls().forEach(u => { if (u && urls.indexOf(u) === -1) urls.push(u) })
294
+ }
295
+ })
296
+ return urls
297
+ },
298
+
299
+ /** 父组件通过 ref 调用的公共方法:获取本组件全部图片 URL */
300
+ getAllPhotoUrls () {
301
+ return this._collectAllUrls()
302
+ },
303
+
259
304
  /**
260
- * 预览隐患照片
305
+ * 点击隐患照片(非 FileImageGroup 容灾展示):收集全组件 URL 抛给父组件
261
306
  */
262
307
  handlePreviewHazardPhotos (hazardPhotos, currentIndex = 0) {
263
308
  const photos = hazardPhotos.map(photo => photo.url)
264
- this.$emit('preview-photos', photos, currentIndex)
309
+ const all = this._collectAllUrls()
310
+ const urls = all.length > 0 ? all : photos
311
+ if (urls.length > 0) {
312
+ this.$emit('preview-photos', urls, Math.min(currentIndex, urls.length - 1))
313
+ }
314
+ },
315
+
316
+ /** 点击 FileImageGroup 图片:收集全组件 URL 抛给父组件统一预览 */
317
+ handleFileImageClick ({ index, url }) {
318
+ const urls = this._collectAllUrls()
319
+ if (urls.length > 0) {
320
+ const globalIdx = url ? urls.indexOf(url) : Math.min(index, urls.length - 1)
321
+ this.$emit('preview-photos', urls, globalIdx >= 0 ? globalIdx : 0)
322
+ }
265
323
  },
266
324
 
267
325
  /**
@@ -68,6 +68,9 @@
68
68
  :gap="10"
69
69
  alt-prefix="隐患照片"
70
70
  empty-text="暂无隐患照片"
71
+ :no-preview="true"
72
+ @image-click="handleImageClick"
73
+ :ref="el => registerFig(el)"
71
74
  />
72
75
  </div>
73
76
 
@@ -91,6 +94,9 @@
91
94
  :gap="10"
92
95
  alt-prefix="现场处理照片"
93
96
  empty-text="暂无现场处理照片"
97
+ :no-preview="true"
98
+ @image-click="handleImageClick"
99
+ :ref="el => registerFig(el)"
94
100
  />
95
101
  </div>
96
102
  </div>
@@ -130,7 +136,17 @@ export default {
130
136
  },
131
137
  data () {
132
138
  return {
133
- hazardLevelConfig: []
139
+ hazardLevelConfig: [],
140
+ /** 储存本组件内所有 FileImageGroup 实例(通过函数 ref 注册) */
141
+ figInstances: []
142
+ }
143
+ },
144
+ watch: {
145
+ /** 隐患列表数据变化时清空实例缓存,下一帧重新渲染时会重新注册 */
146
+ hazardList: {
147
+ handler () { this.figInstances = [] },
148
+ deep: true,
149
+ immediate: true
134
150
  }
135
151
  },
136
152
  methods: {
@@ -206,6 +222,38 @@ export default {
206
222
  minor: 'info-circle'
207
223
  }
208
224
  return iconMap[level] || 'warning'
225
+ },
226
+
227
+ /** 通过函数 ref 收集 FileImageGroup 实例(v-for 中无法使用静态 ref) */
228
+ registerFig (fig) {
229
+ if (fig && this.figInstances.indexOf(fig) === -1) {
230
+ this.figInstances.push(fig)
231
+ }
232
+ },
233
+
234
+ /** 收集本组件内所有 FileImageGroup 的已解析图片 URL,去重后返回 */
235
+ _collectAllUrls () {
236
+ const urls = []
237
+ this.figInstances.forEach(fig => {
238
+ if (fig && fig.getAllResolvedUrls) {
239
+ fig.getAllResolvedUrls().forEach(u => { if (u && urls.indexOf(u) === -1) urls.push(u) })
240
+ }
241
+ })
242
+ return urls
243
+ },
244
+
245
+ /** 父组件通过 ref 调用的公共方法:获取本组件全部图片 URL */
246
+ getAllPhotoUrls () {
247
+ return this._collectAllUrls()
248
+ },
249
+
250
+ /** 点击图片:收集本组件全部 URL 抛给父组件统一预览 */
251
+ handleImageClick ({ index, url }) {
252
+ const urls = this._collectAllUrls()
253
+ if (urls.length > 0) {
254
+ const globalIdx = url ? urls.indexOf(url) : index
255
+ this.$emit('preview-photos', urls, globalIdx >= 0 ? globalIdx : 0)
256
+ }
209
257
  }
210
258
  },
211
259
  mounted() {
@@ -74,6 +74,7 @@
74
74
  </div>
75
75
  <div class="device-section" v-show="showAiDeviceTab">
76
76
  <ai-device-status
77
+ ref="aiDeviceStatus"
77
78
  :device-list="aiDeviceItem"
78
79
  :config="AICheckConfig?.XInspectionDetailDrawer?.AiDeviceStatus"
79
80
  :check-total-count="checkTotalCount"
@@ -83,6 +84,7 @@
83
84
  </div>
84
85
  <div class="device-section" v-show="inspectionData.deviceList.length > 0">
85
86
  <device-status
87
+ ref="deviceStatus"
86
88
  :device-list="inspectionData.deviceList"
87
89
  @preview-photos="(photos, idx) => handlePreviewPhotos(photos, idx)"
88
90
  />
@@ -90,14 +92,20 @@
90
92
 
91
93
  <div class="check-items-section" v-show="inspectionData.checkItems.length > 0">
92
94
  <check-items
95
+ ref="checkItems"
93
96
  :check-items="inspectionData.checkItems"
97
+ @preview-photos="(photos, idx) => handlePreviewPhotos(photos, idx)"
94
98
  />
95
99
  </div>
96
100
 
97
101
  <div class="hazard-gallery-section" v-show="inspectionData.basicInfo.f_entry_status === '入户'">
98
102
  <a-card class="section-card" :bordered="false">
99
103
  <div class="section-grid">
100
- <hazard-photos :hazard-list="inspectionData.hazardList" />
104
+ <hazard-photos
105
+ ref="hazardPhotos"
106
+ :hazard-list="inspectionData.hazardList"
107
+ @preview-photos="(photos, idx) => handlePreviewPhotos(photos, idx)"
108
+ />
101
109
  <div class="overall-photos">
102
110
  <div class="photos-header">
103
111
  <div class="photos-title">
@@ -124,6 +132,9 @@
124
132
  :gap="10"
125
133
  :alt-prefix="field.name"
126
134
  :empty-text="`暂无${field.name}`"
135
+ :no-preview="true"
136
+ @image-click="handleFormFileImageClick"
137
+ :ref="el => registerFormFig(el)"
127
138
  />
128
139
  <span v-else class="empty-text">暂无</span>
129
140
  </template>
@@ -138,6 +149,9 @@
138
149
  :gap="10"
139
150
  :alt-prefix="field.name"
140
151
  :empty-text="`暂无${field.name}`"
152
+ :no-preview="true"
153
+ @image-click="handleFormFileImageClick"
154
+ :ref="el => registerFormFig(el)"
141
155
  />
142
156
  <span v-else class="empty-text">暂无</span>
143
157
  </template>
@@ -167,7 +181,45 @@
167
181
  :image-name="`预览图片${previewModal.current + 1}`"
168
182
  :append-to-body="true"
169
183
  @close="previewModal.visible = false"
170
- />
184
+ >
185
+ <template #toolbarPrepend v-if="allPhotoList.length > 0">
186
+ <a-tooltip title="上一张">
187
+ <a-button
188
+ type="primary"
189
+ icon="left"
190
+ @click="goPrevPhoto"
191
+ size="small"
192
+ :disabled="previewModal.current <= 0"
193
+ style="background:rgba(255,255,255,0.15);border-color:rgba(255,255,255,0.2);color:#fff;"
194
+ />
195
+ </a-tooltip>
196
+ <span style="color:#fff;font-size:13px;min-width:50px;text-align:center;display:inline-block;">{{ previewModal.current + 1 }} / {{ allPhotoList.length }}</span>
197
+ <a-tooltip title="下一张">
198
+ <a-button
199
+ type="primary"
200
+ icon="right"
201
+ @click="goNextPhoto"
202
+ size="small"
203
+ :disabled="previewModal.current >= allPhotoList.length - 1"
204
+ style="background:rgba(255,255,255,0.15);border-color:rgba(255,255,255,0.2);color:#fff;"
205
+ />
206
+ </a-tooltip>
207
+ <span style="width:1px;height:20px;background:rgba(255,255,255,0.2);margin:0 4px;display:inline-block;"></span>
208
+ </template>
209
+ <template #thumbnailStrip v-if="allPhotoList.length > 1">
210
+ <div class="thumbnail-strip">
211
+ <div
212
+ v-for="(thumb, i) in allPhotoList"
213
+ :key="i"
214
+ class="thumbnail-item"
215
+ :class="{ active: i === previewModal.current }"
216
+ @click="previewModal.current = i"
217
+ >
218
+ <img :src="thumb" :alt="`缩略图 ${i + 1}`" />
219
+ </div>
220
+ </div>
221
+ </template>
222
+ </image-preview-modal>
171
223
 
172
224
  <a-modal
173
225
  v-if="approveModal"
@@ -271,6 +323,9 @@ export default {
271
323
  }
272
324
  },
273
325
  previewModal: { visible: false, images: [], current: 0 },
326
+ allPhotoList: [],
327
+ /** 收集"拍照签名信息"区域中 FileImageGroup 实例(通过函数 ref 注册) */
328
+ formFigInstances: [],
274
329
  approveModal: { visible: false, type: 'approve', form: { note: '' }, loading: false },
275
330
  approveRules: {
276
331
  note: [
@@ -366,6 +421,8 @@ export default {
366
421
  hazardList: [],
367
422
  attachments: { signature: null, recordings: [], filePhotos: [], imagePhotos: [] }
368
423
  }
424
+ this.allPhotoList = []
425
+ this.formFigInstances = []
369
426
 
370
427
  this.showHistoryPanel = !params?.id && !!params?.f_userinfo_id
371
428
 
@@ -406,10 +463,12 @@ export default {
406
463
  console.error('getPcAiInspectionAssistResultLogic', e)
407
464
  }
408
465
  console.warn('datra', data)
466
+ this.formFigInstances = []
409
467
  this.inspectionData = {
410
468
  ...data,
411
469
  historyList: this.inspectionData.historyList || data.historyList || []
412
470
  }
471
+ this.allPhotoList = this._rebuildAllPhotoList()
413
472
  if (this.inspectionData.historyList && this.inspectionData.historyList.length > 0) {
414
473
  const activeItem = this.inspectionData.historyList.find(item => item.isActive)
415
474
  if (activeItem) this.currentHistoryId = activeItem.id
@@ -423,6 +482,7 @@ export default {
423
482
  this.currentHistoryId = historyId
424
483
  const data = await inspectionService.switchToHistoryDetail(historyId, this.currentId, this.f_userinfo_id)
425
484
  this.inspectionData = { ...data, historyList: this.inspectionData.historyList || data.historyList || [] }
485
+ this.allPhotoList = this._rebuildAllPhotoList()
426
486
  } catch (error) {
427
487
  this.$message.error('切换历史记录失败')
428
488
  } finally {
@@ -479,11 +539,99 @@ export default {
479
539
  this.$message.info('录音播放功能待实现')
480
540
  },
481
541
 
542
+ /** 打开图片预览:先按固定顺序重新构建全量列表,再定位点击的图片位置 */
482
543
  handlePreviewPhotos (photos, current = 0) {
544
+ if (!Array.isArray(photos) || photos.length === 0) return
545
+ const urls = typeof photos[0] === 'string' ? photos : photos.map(p => p?.url || p?.imageUrl).filter(Boolean)
546
+ this.allPhotoList = this._rebuildAllPhotoList()
547
+ const merged = [...this.allPhotoList]
548
+ urls.forEach(url => {
549
+ if (url && merged.indexOf(url) === -1) merged.push(url)
550
+ })
551
+ this.allPhotoList = merged
552
+ const clickedUrl = urls[Math.min(current, urls.length - 1)]
553
+ const globalIdx = clickedUrl ? merged.indexOf(clickedUrl) : 0
483
554
  this.previewModal = {
484
555
  visible: true,
485
- images: Array.isArray(photos) ? (typeof photos[0] === 'string' ? photos : photos.map(p => p?.url || p?.imageUrl).filter(Boolean)) : [photos],
486
- current
556
+ images: merged,
557
+ current: globalIdx >= 0 ? globalIdx : 0
558
+ }
559
+ },
560
+
561
+ /** 点击"拍照签名信息"区域中的 FileImageGroup:合并到全量列表后打开预览 */
562
+ handleFormFileImageClick ({ index, url, fileData, allUrls }) {
563
+ const urls = allUrls.filter(Boolean)
564
+ if (urls.length > 0) {
565
+ this.handlePreviewPhotos(urls, Math.min(index, urls.length - 1))
566
+ }
567
+ },
568
+
569
+ /** 通过函数 ref 收集"拍照签名信息"区域的 FileImageGroup 实例 */
570
+ registerFormFig (fig) {
571
+ if (!this.formFigInstances) this.formFigInstances = []
572
+ if (fig && this.formFigInstances.indexOf(fig) === -1) {
573
+ this.formFigInstances.push(fig)
574
+ }
575
+ },
576
+
577
+ /** 收集"拍照签名信息"区域中所有 FileImageGroup 的已解析 URL + 表单字段原始 url */
578
+ _collectFormFigPhotoUrls () {
579
+ const urls = []
580
+ ;(this.formFigInstances || []).forEach(fig => {
581
+ if (fig && fig.getAllResolvedUrls) {
582
+ fig.getAllResolvedUrls().forEach(u => { if (u && urls.indexOf(u) === -1) urls.push(u) })
583
+ }
584
+ })
585
+ // 兜底:直接取表单字段中的 url(未通过 FIG 解析的 fileId 在此处无法获取)
586
+ const form = this.inspectionData && this.inspectionData.signatureForm
587
+ if (form && form.formConfig && form.formConfig.formJson) {
588
+ form.formConfig.formJson.forEach(field => {
589
+ if (!this.shouldShowField || !this.shouldShowField(field)) return
590
+ const files = field.type === 'file'
591
+ ? (this.getFieldFiles && this.getFieldFiles(field.model) || [])
592
+ : ((field.type === 'image' || field.type === 'signature')
593
+ ? (this.getFieldImages && this.getFieldImages(field.model) || [])
594
+ : [])
595
+ files.forEach(f => {
596
+ const u = f && (f.url || f.imageUrl)
597
+ if (u && urls.indexOf(u) === -1) urls.push(u)
598
+ })
599
+ })
600
+ }
601
+ return urls
602
+ },
603
+
604
+ /** 按固定顺序构建全量图片列表:附件 → 子组件(隐患、设备、AI、检查项)→ 签名表单 */
605
+ _rebuildAllPhotoList () {
606
+ const list = []
607
+ const att = this.inspectionData.attachments
608
+ if (att) {
609
+ if (att.signature?.imageUrl && list.indexOf(att.signature.imageUrl) === -1) list.push(att.signature.imageUrl)
610
+ if (Array.isArray(att.refusephotos)) {
611
+ att.refusephotos.forEach(p => { if (p?.imageUrl && list.indexOf(p.imageUrl) === -1) list.push(p.imageUrl) })
612
+ }
613
+ }
614
+ ;['hazardPhotos', 'deviceStatus', 'aiDeviceStatus', 'checkItems'].forEach(ref => {
615
+ const child = this.$refs[ref]
616
+ if (child && child.getAllPhotoUrls) {
617
+ const urls = child.getAllPhotoUrls()
618
+ urls.forEach(u => { if (u && list.indexOf(u) === -1) list.push(u) })
619
+ }
620
+ })
621
+ const formUrls = this._collectFormFigPhotoUrls()
622
+ formUrls.forEach(u => { if (u && list.indexOf(u) === -1) list.push(u) })
623
+ return list
624
+ },
625
+
626
+ goPrevPhoto () {
627
+ if (this.previewModal.current > 0) {
628
+ this.previewModal.current--
629
+ }
630
+ },
631
+
632
+ goNextPhoto () {
633
+ if (this.previewModal.current < this.allPhotoList.length - 1) {
634
+ this.previewModal.current++
487
635
  }
488
636
  },
489
637
 
@@ -619,6 +767,8 @@ export default {
619
767
  hazardList: [],
620
768
  attachments: { signature: null, recordings: [], filePhotos: [], imagePhotos: [] }
621
769
  }
770
+ this.allPhotoList = []
771
+ this.formFigInstances = []
622
772
  },
623
773
 
624
774
  /**
@@ -832,4 +982,41 @@ export default {
832
982
  }
833
983
  }
834
984
  }
985
+
986
+ /* 预览模态框底部缩略图条 */
987
+ .thumbnail-strip {
988
+ display: flex;
989
+ justify-content: center;
990
+ gap: 6px;
991
+ margin-top: 12px;
992
+ padding: 0 20px;
993
+ flex-wrap: nowrap;
994
+ overflow-x: auto;
995
+ max-width: 100vw;
996
+
997
+ &::-webkit-scrollbar { height: 4px; }
998
+ &::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.3); border-radius: 2px; }
999
+ }
1000
+
1001
+ .thumbnail-item {
1002
+ flex-shrink: 0;
1003
+ width: 48px;
1004
+ height: 48px;
1005
+ border-radius: 4px;
1006
+ overflow: hidden;
1007
+ cursor: pointer;
1008
+ border: 2px solid transparent;
1009
+ opacity: 0.6;
1010
+ transition: all 0.2s ease;
1011
+
1012
+ &:hover { opacity: 0.9; border-color: rgba(255,255,255,0.4); }
1013
+ &.active { opacity: 1; border-color: #1890ff; }
1014
+
1015
+ img {
1016
+ width: 100%;
1017
+ height: 100%;
1018
+ object-fit: cover;
1019
+ display: block;
1020
+ }
1021
+ }
835
1022
  </style>