imeik-bizui 2.7.7 → 2.7.8

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.
@@ -30,6 +30,8 @@ imeik-bizui/src/bizui/
30
30
  />
31
31
  ```
32
32
 
33
+ 移动端想保留弹窗:加 `narrow-detail-mode="dialog"`(默认 `inline`,在看板下方展开)。
34
+
33
35
  `scene`:`supplies` | `meeting` | `apply` / `applyCenter`。
34
36
 
35
37
  请求体组装在业务系统(如会议 `buildFloorPricePayload.js`)。
@@ -55,7 +57,8 @@ bizui: scene 选接口 → Dashboard 拉数 → Board / Detail
55
57
 
56
58
  | 类型 | 内容 |
57
59
  | --- | --- |
58
- | Props | `scene`、`requestPayload`、`customerName`、`showDetailLink`、`useMock`、`emptyText` |
60
+ | Props | `scene`、`requestPayload`、`customerName`、`showDetailLink`、`narrowDetailMode`、`useMock`、`emptyText` |
61
+ | 明细形态 | 宽屏始终弹窗;窄屏 / 移动端(≤768px)由 `narrowDetailMode` 决定:`inline`(默认,看板下方展开)或 `dialog`(弹窗) |
59
62
  | 物资入参 | `customUniqueCode`、`businessLineNumber`、`isGroupCustomer`、`applyDeptCode`、`costBearDeptCode`、`costType`、`materialList[]`(允许 `[]`) |
60
63
  | 会议入参 | `meetTypeCode`、`meetFormJson`、`customUniqueCode`、`forceRefresh`;`meetCode` 有值才传 |
61
64
  | 申请中心入参 | 见丽臻底价接口文档 |
@@ -19,7 +19,7 @@
19
19
  <div v-if="error" class="fpb-msg fpb-msg--error">{{ error }}</div>
20
20
 
21
21
  <div v-else-if="hasContent" :class="['fpb-panel', panelThemeClass]" role="status">
22
- <div class="fpb-result">
22
+ <div :class="['fpb-result', { 'fpb-result--with-link': showDetailLink }]">
23
23
  <div class="fpb-result__main">
24
24
  <div class="fpb-result__heading">
25
25
  <img
@@ -1083,35 +1083,125 @@ export default {
1083
1083
 
1084
1084
  @media (max-width: 640px) {
1085
1085
  .fpb-panel {
1086
- padding: 16px;
1086
+ padding: 12px;
1087
1087
  }
1088
1088
 
1089
+ /* 链接钉在标题行右侧,说明文字可占满整行 */
1089
1090
  .fpb-result {
1090
- gap: 14px;
1091
- flex-wrap: wrap;
1091
+ position: relative;
1092
+ display: block;
1093
+ }
1094
+
1095
+ .fpb-result__heading {
1096
+ gap: 6px 8px;
1097
+ margin-bottom: 4px;
1098
+ }
1099
+
1100
+ .fpb-result--with-link .fpb-result__heading {
1101
+ padding-right: 104px;
1102
+ }
1103
+
1104
+ .fpb-mark {
1105
+ width: 22px;
1106
+ height: 22px;
1107
+ }
1108
+
1109
+ .fpb-result__title {
1110
+ font-size: 15px;
1111
+ line-height: 22px;
1112
+ }
1113
+
1114
+ .fpb-badge {
1115
+ min-height: 22px;
1116
+ padding: 0 8px;
1117
+ border-radius: 4px;
1118
+ font-size: 12px;
1119
+ line-height: 18px;
1120
+ }
1121
+
1122
+ .fpb-result__desc {
1123
+ font-size: 12px;
1124
+ line-height: 18px;
1092
1125
  }
1093
1126
 
1094
1127
  .fpb-link {
1095
- margin-left: auto;
1128
+ position: absolute;
1129
+ top: 0;
1130
+ right: 0;
1131
+ min-height: 22px;
1132
+ font-size: 13px;
1133
+ line-height: 22px;
1134
+ }
1135
+
1136
+ .fpb-list {
1137
+ gap: 8px;
1138
+ margin-top: 10px;
1139
+ }
1140
+
1141
+ .fpb-card {
1142
+ padding: 10px 12px;
1143
+ border-radius: 6px;
1144
+ }
1145
+
1146
+ .fpb-card__topline {
1147
+ gap: 6px 8px;
1148
+ margin-bottom: 8px;
1149
+ }
1150
+
1151
+ .fpb-card-title {
1152
+ font-size: 14px;
1153
+ line-height: 20px;
1154
+ }
1155
+
1156
+ .fpb-card__tag {
1157
+ min-height: 20px;
1158
+ padding: 0 6px;
1159
+ font-size: 11px;
1096
1160
  }
1097
1161
 
1162
+ /* 与下方「申请前 / 申请后」重复,窄屏省掉这一行 */
1098
1163
  .fpb-card__shift {
1099
- margin-left: 0;
1100
- width: 100%;
1164
+ display: none;
1101
1165
  }
1102
1166
 
1103
1167
  .fpb-price-grid {
1104
1168
  grid-template-columns: repeat(2, minmax(0, 1fr));
1169
+ gap: 6px 12px;
1170
+ }
1171
+
1172
+ .fpb-price-item {
1173
+ dt {
1174
+ margin-bottom: 1px;
1175
+ font-size: 11px;
1176
+ line-height: 16px;
1177
+ }
1178
+
1179
+ dd {
1180
+ font-size: 14px;
1181
+ line-height: 20px;
1182
+
1183
+ &.is-strong {
1184
+ font-size: 15px;
1185
+ }
1186
+ }
1105
1187
  }
1106
1188
 
1107
1189
  .fpb-footer {
1108
1190
  flex-direction: column;
1109
- gap: 8px;
1191
+ gap: 4px;
1192
+ margin-top: 10px;
1193
+ padding-top: 8px;
1194
+ }
1195
+
1196
+ .fpb-footer__note {
1197
+ font-size: 11px;
1198
+ line-height: 16px;
1110
1199
  }
1111
1200
 
1112
1201
  .fpb-footer__meta {
1113
1202
  justify-content: flex-start;
1114
1203
  flex-wrap: wrap;
1204
+ line-height: 18px;
1115
1205
  }
1116
1206
  }
1117
1207
  </style>
@@ -1,7 +1,7 @@
1
1
  <!--
2
2
  通用底价看板(展示 + 拉数编排)
3
3
 
4
- 职责:防抖请求、normalize、看板/明细 UI。
4
+ 职责:防抖请求、normalize、看板/明细 UI(宽屏弹窗;≤ NARROW_BREAKPOINT 按 narrowDetailMode 就地展开或弹窗)。
5
5
  不负责:各业务域字段映射、拼请求体 —— 由业务系统组装后经 FieldFloorPriceBoard 传入。
6
6
 
7
7
  推荐:业务侧挂 <FieldFloorPriceBoard scene="..." :request-payload="..." />
@@ -24,11 +24,23 @@
24
24
  :year="board.year"
25
25
  :footer-text="board.footerText"
26
26
  :show-detail-link="showDetailLink"
27
+ :detail-link-text="detailLinkText"
27
28
  :use-demo="useDemo"
28
29
  :empty-text="emptyText"
29
30
  @view-detail="onViewDetail"
30
31
  @toggle-demo="onToggleDemo"
31
32
  />
33
+ <FloorPriceDetailInline
34
+ v-if="useInlineDetail && inlineOpen"
35
+ :loading="detailLoading"
36
+ :error="detailError"
37
+ :title="detailView.title"
38
+ :summary-columns="detailView.summaryColumns"
39
+ :summary-rows="detailView.summaryRows"
40
+ :sections="detailView.sections"
41
+ :formulas="detailView.formulas"
42
+ @collapse="collapseInline"
43
+ />
32
44
  <FloorPriceDetailDialog
33
45
  :visible.sync="detailVisible"
34
46
  :loading="detailLoading"
@@ -45,9 +57,10 @@
45
57
  <script>
46
58
  import FloorPriceBoard from './FloorPriceBoard.vue'
47
59
  import FloorPriceDetailDialog from './FloorPriceDetailDialog.vue'
60
+ import FloorPriceDetailInline from './FloorPriceDetailInline.vue'
48
61
  import { createDemoDetail, createDemoOverview, nextDemoState } from './mock'
49
62
  import { normalizeDetail, normalizeOverview } from './normalize'
50
- import { PANEL_STATE } from './utils'
63
+ import { NARROW_BREAKPOINT, PANEL_STATE } from './utils'
51
64
 
52
65
  const EMPTY_DETAIL = {
53
66
  title: '底价明细',
@@ -79,7 +92,8 @@ export default {
79
92
  name: 'FloorPriceDashboard',
80
93
  components: {
81
94
  FloorPriceBoard,
82
- FloorPriceDetailDialog
95
+ FloorPriceDetailDialog,
96
+ FloorPriceDetailInline
83
97
  },
84
98
  props: {
85
99
  // ——— 受控:外部已拉好数 ———
@@ -109,6 +123,14 @@ export default {
109
123
  title: { type: String, default: '底价信息' },
110
124
  showTitle: { type: Boolean, default: false },
111
125
  showDetailLink: { type: Boolean, default: true },
126
+ /** 窄屏(≤ NARROW_BREAKPOINT)明细形态:inline 看板下方展开 | dialog 弹窗;宽屏恒为弹窗 */
127
+ narrowDetailMode: {
128
+ type: String,
129
+ default: 'inline',
130
+ validator(v) {
131
+ return ['inline', 'dialog'].includes(v)
132
+ }
133
+ },
112
134
  useDemo: { type: Boolean, default: false },
113
135
  customerName: { type: String, default: '' },
114
136
  /** 无概览数据时的空态文案 */
@@ -123,6 +145,10 @@ export default {
123
145
  detailLoading: false,
124
146
  detailError: '',
125
147
  innerDetail: null,
148
+ /** 已加载明细对应的概览入参签名;与当前不一致时视为过期 */
149
+ detailLoadedSig: null,
150
+ isNarrow: false,
151
+ inlineOpen: false,
126
152
  debouncedFetch: null,
127
153
  /** demo 三态:blocked → pass → exempt → … */
128
154
  demoState: PANEL_STATE.BLOCKED
@@ -175,6 +201,13 @@ export default {
175
201
  },
176
202
  overviewSignature() {
177
203
  return JSON.stringify(this.overviewPayload || null)
204
+ },
205
+ useInlineDetail() {
206
+ return this.isNarrow && this.narrowDetailMode === 'inline'
207
+ },
208
+ detailLinkText() {
209
+ if (!this.useInlineDetail) return '查看底价明细 →'
210
+ return this.inlineOpen ? '收起底价明细 ↑' : '查看底价明细 ↓'
178
211
  }
179
212
  },
180
213
  watch: {
@@ -192,24 +225,47 @@ export default {
192
225
  } else if (this.selfFetch) {
193
226
  this.scheduleFetch()
194
227
  }
228
+ },
229
+ /** 跨断点或切换配置时两种形态互换:已开弹窗 → 就地展开;离开内联不自动弹窗 */
230
+ useInlineDetail(inline) {
231
+ if (inline && this.detailVisible) {
232
+ this.detailVisible = false
233
+ this.inlineOpen = true
234
+ } else if (!inline) {
235
+ this.inlineOpen = false
236
+ }
195
237
  }
196
238
  },
197
239
  created() {
240
+ this._detailReqSeq = 0
198
241
  this.debouncedFetch = debounce(this.runFetch, this.debounceMs)
199
242
  if (this.selfFetch) this.debouncedFetch()
200
243
  },
244
+ mounted() {
245
+ this.updateNarrow()
246
+ if (typeof window !== 'undefined') {
247
+ window.addEventListener('resize', this.updateNarrow)
248
+ }
249
+ },
201
250
  beforeDestroy() {
202
251
  if (this.debouncedFetch && this.debouncedFetch.cancel) {
203
252
  this.debouncedFetch.cancel()
204
253
  }
254
+ if (typeof window !== 'undefined') {
255
+ window.removeEventListener('resize', this.updateNarrow)
256
+ }
205
257
  },
206
258
  methods: {
259
+ updateNarrow() {
260
+ if (typeof window === 'undefined') return
261
+ this.isNarrow = window.innerWidth <= NARROW_BREAKPOINT
262
+ },
207
263
  /** Mock 按钮:循环切换 禁止 → 通过 → 免于监控 */
208
264
  onToggleDemo() {
209
265
  if (!this.useDemo) return
210
266
  this.demoState = nextDemoState(this.demoState)
211
267
  // 明细若已打开,随概览产品行一起刷新汇总
212
- if (this.detailVisible) {
268
+ if (this.detailVisible || this.inlineOpen) {
213
269
  this.innerDetail = createDemoDetail({
214
270
  ...this.board,
215
271
  customerName: this.board.customerName || this.customerName
@@ -246,16 +302,54 @@ export default {
246
302
  } finally {
247
303
  this.innerLoading = false
248
304
  }
305
+ this.syncInlineAfterOverview()
249
306
  },
250
- async onViewDetail() {
307
+ /** 内联已展开时:概览没了就收起,入参变了就重拉明细 */
308
+ syncInlineAfterOverview() {
309
+ if (!this.useInlineDetail || !this.inlineOpen) return
310
+ if (!this.innerOverview) {
311
+ this.inlineOpen = false
312
+ return
313
+ }
314
+ if (this.detailLoadedSig !== this.overviewSignature) this.loadDetailData()
315
+ },
316
+ onViewDetail() {
317
+ if (this.useInlineDetail) {
318
+ this.toggleInline()
319
+ return
320
+ }
321
+ this.detailVisible = true
322
+ this.loadDetailData()
323
+ },
324
+ toggleInline() {
325
+ if (this.inlineOpen) {
326
+ this.inlineOpen = false
327
+ return
328
+ }
329
+ this.inlineOpen = true
330
+ const hasDetail = !!(this.innerDetail || this.detail)
331
+ const stale = this.detailLoadedSig !== this.overviewSignature
332
+ if (this.useDemo || this.detailError || !hasDetail || stale) this.loadDetailData()
333
+ },
334
+ collapseInline() {
335
+ this.inlineOpen = false
336
+ this.$nextTick(() => {
337
+ const el = this.$el
338
+ if (!el || typeof el.getBoundingClientRect !== 'function') return
339
+ if (el.getBoundingClientRect().top < 0) el.scrollIntoView({ block: 'start' })
340
+ })
341
+ },
342
+ async loadDetailData() {
343
+ const reqId = ++this._detailReqSeq
344
+ const isLatest = () => reqId === this._detailReqSeq
345
+ const sig = this.overviewSignature
251
346
  this.detailError = ''
252
- // 先进入 loading,再打开弹窗,避免首帧只有公式行的窄框
347
+ // 先进入 loading 再展示,避免首帧只有公式行的窄框
253
348
  const willFetch =
254
349
  this.useDemo ||
255
350
  typeof this.fetchDetail === 'function' ||
256
351
  typeof this.loadDetail === 'function'
257
352
  if (willFetch) this.detailLoading = true
258
- this.detailVisible = true
259
353
 
260
354
  if (this.useDemo) {
261
355
  try {
@@ -263,6 +357,7 @@ export default {
263
357
  ...this.board,
264
358
  customerName: this.board.customerName || this.customerName
265
359
  })
360
+ this.detailLoadedSig = sig
266
361
  } finally {
267
362
  this.detailLoading = false
268
363
  }
@@ -280,24 +375,26 @@ export default {
280
375
  const detailPayload = this.detailPayload
281
376
  if (!detailPayload) {
282
377
  this.innerDetail = base
283
- this.detailLoading = false
378
+ this.detailLoadedSig = sig
284
379
  return
285
380
  }
286
381
  const res = await this.fetchDetail(detailPayload)
382
+ if (!isLatest()) return
287
383
  if (res && res.code === 200) {
288
384
  // 保留接口 customName,勿用空 prop 覆盖
289
385
  this.innerDetail = {
290
386
  ...base,
291
387
  ...(res.data || {})
292
388
  }
389
+ this.detailLoadedSig = sig
293
390
  } else {
294
391
  this.innerDetail = null
295
392
  throw new Error((res && res.message) || '底价明细获取失败')
296
393
  }
297
394
  } catch (e) {
298
- this.detailError = (e && e.message) || '底价明细获取失败'
395
+ if (isLatest()) this.detailError = (e && e.message) || '底价明细获取失败'
299
396
  } finally {
300
- this.detailLoading = false
397
+ if (isLatest()) this.detailLoading = false
301
398
  }
302
399
  return
303
400
  }
@@ -305,13 +402,16 @@ export default {
305
402
  if (typeof this.loadDetail === 'function') {
306
403
  try {
307
404
  const data = await this.loadDetail({ overview: this.board, raw: this.rawOverview })
405
+ if (!isLatest()) return
308
406
  this.innerDetail = data || null
309
- if (!data) this.detailError = '暂无底价明细'
407
+ if (data) this.detailLoadedSig = sig
408
+ else this.detailError = '暂无底价明细'
310
409
  } catch (e) {
410
+ if (!isLatest()) return
311
411
  this.innerDetail = null
312
412
  this.detailError = (e && e.message) || '底价明细获取失败'
313
413
  } finally {
314
- this.detailLoading = false
414
+ if (isLatest()) this.detailLoading = false
315
415
  }
316
416
  return
317
417
  }
@@ -331,5 +431,7 @@ export default {
331
431
  padding: 0;
332
432
  box-sizing: border-box;
333
433
  min-width: 0;
434
+ /* 收起明细回滚时给宿主固定顶栏留位 */
435
+ scroll-margin-top: 80px;
334
436
  }
335
437
  </style>
@@ -172,8 +172,8 @@
172
172
  </template>
173
173
 
174
174
  <script>
175
- import { DEFAULT_DETAIL_FORMULAS, formatNumber, formatSignedNumber } from './utils'
176
- import { getDisplayMode, subscribeDisplayMode } from './displayPrefs'
175
+ import { NARROW_BREAKPOINT } from './utils'
176
+ import detailFormat from './detailFormat'
177
177
  import FloorPriceSectionPager from './FloorPriceSectionPager.vue'
178
178
 
179
179
  export default {
@@ -181,6 +181,7 @@ export default {
181
181
  components: {
182
182
  FloorPriceSectionPager
183
183
  },
184
+ mixins: [detailFormat],
184
185
  props: {
185
186
  visible: {
186
187
  type: Boolean,
@@ -253,8 +254,7 @@ export default {
253
254
  data() {
254
255
  return {
255
256
  sectionPages: {},
256
- isNarrow: false,
257
- displayMode: getDisplayMode()
257
+ isNarrow: false
258
258
  }
259
259
  },
260
260
  computed: {
@@ -288,22 +288,6 @@ export default {
288
288
  const headH = this.isNarrow ? 36 : 38
289
289
  return headH + size * rowH
290
290
  },
291
- formulaList() {
292
- if (Array.isArray(this.formulas)) return this.formulas
293
- return DEFAULT_DETAIL_FORMULAS
294
- },
295
- /** 拆成「指标名 = 表达式」,便于加粗高亮 */
296
- formulaItems() {
297
- return (this.formulaList || []).map((line) => {
298
- const text = String(line || '').trim()
299
- const idx = text.indexOf('=')
300
- if (idx < 0) return { key: text, expr: '' }
301
- return {
302
- key: text.slice(0, idx).trim(),
303
- expr: text.slice(idx + 1).trim()
304
- }
305
- }).filter((item) => item.key)
306
- },
307
291
  /** 有汇总或分区时视为已有明细内容(可在旧数据上盖 loading) */
308
292
  hasDetailContent() {
309
293
  return (this.summaryColumns && this.summaryColumns.length > 0) ||
@@ -350,20 +334,16 @@ export default {
350
334
  if (typeof window !== 'undefined') {
351
335
  window.addEventListener('resize', this.updateNarrow)
352
336
  }
353
- this._unsubDisplayMode = subscribeDisplayMode((mode) => {
354
- this.displayMode = mode
355
- })
356
337
  },
357
338
  beforeDestroy() {
358
339
  if (typeof window !== 'undefined') {
359
340
  window.removeEventListener('resize', this.updateNarrow)
360
341
  }
361
- if (this._unsubDisplayMode) this._unsubDisplayMode()
362
342
  },
363
343
  methods: {
364
344
  updateNarrow() {
365
345
  if (typeof window === 'undefined') return
366
- this.isNarrow = window.innerWidth <= 768
346
+ this.isNarrow = window.innerWidth <= NARROW_BREAKPOINT
367
347
  },
368
348
  onClose() {
369
349
  this.$emit('close')
@@ -401,74 +381,6 @@ export default {
401
381
  const start = (page - 1) * size
402
382
  return rows.slice(start, start + size)
403
383
  },
404
- formatCell(value, col = {}) {
405
- if (col.type === 'number' || col.type === 'highlight' || col.digits != null) {
406
- return formatNumber(value, {
407
- digits: col.digits != null ? col.digits : null,
408
- mode: this.displayMode,
409
- fallback: '-'
410
- })
411
- }
412
- if (value === undefined || value === null || value === '') return '-'
413
- return String(value)
414
- },
415
- formatGapCell(value, col = {}) {
416
- return formatSignedNumber(value, {
417
- digits: col.digits != null ? col.digits : null,
418
- mode: this.displayMode,
419
- fallback: '-'
420
- })
421
- },
422
- formatFooterValue(v) {
423
- if (v == null) return ''
424
- if (typeof v !== 'object') return String(v)
425
- if (v.kind === 'qty') {
426
- const n = v.value == null ? '-' : String(v.value)
427
- return v.label ? `${v.label} ${n}` : n
428
- }
429
- const body = formatNumber(v.value, { mode: this.displayMode, fallback: '-' })
430
- return v.label ? `${v.label} ${body}` : body
431
- },
432
- gapClass(value, row) {
433
- const n = Number(value != null ? value : row && row.gap)
434
- if (Number.isNaN(n) || n >= 0) return ''
435
- return 'fpd-td--danger'
436
- },
437
- statusText(row, col) {
438
- if (col.statusMap && row[col.prop] != null && col.statusMap[row[col.prop]]) {
439
- return col.statusMap[row[col.prop]]
440
- }
441
- if (row.statusLabel) return row.statusLabel
442
- return row.hit ? '禁止' : '通过'
443
- },
444
- statusClass(row) {
445
- if (row.statusTone === 'danger' || row.statusTone === 'ok') {
446
- return row.statusTone === 'danger' ? 'fpd-status--danger' : 'fpd-status--ok'
447
- }
448
- return row.hit ? 'fpd-status--danger' : 'fpd-status--ok'
449
- },
450
- highlightClass(row, col) {
451
- const tone = col.highlightTone || row[col.toneField] || 'current'
452
- if (tone === 'danger') return 'fpd-td--danger fpd-td--bold'
453
- if (tone === 'ok') return 'fpd-td--ok fpd-td--bold'
454
- return 'fpd-td--current fpd-td--bold'
455
- },
456
- /** 申请后底价:低于标准时标红 */
457
- summaryCellClass(row, col) {
458
- const classes = []
459
- if (col.bold) classes.push('fpd-td--bold')
460
- if (col.prop === 'afterPrice' && row && row.hit) {
461
- classes.push('fpd-td--danger')
462
- }
463
- return classes
464
- },
465
- cellToneClass(row, col) {
466
- if (row.current || row.isCurrent || (col.currentField && row[col.currentField])) {
467
- return 'fpd-td--current'
468
- }
469
- if (col.tone === 'danger') return 'fpd-td--danger'
470
- return ''
471
- },
472
384
  sectionThemeClass(section) {
473
385
  const t = section.theme || 'default'
474
386
  return `fpd-section--${t}`
@@ -1004,6 +916,19 @@ export default {
1004
916
  color: #4e5969;
1005
917
  }
1006
918
 
919
+ /* 三栏并排放不下各表最小列宽:首区独占一行,其余两区并排 */
920
+ @media (min-width: 1101px) and (max-width: 1360px) {
921
+ .fpd-sections,
922
+ .fpd-skeleton__sections {
923
+ grid-template-columns: repeat(2, minmax(0, 1fr));
924
+ }
925
+
926
+ .fpd-section:first-child:nth-last-child(3),
927
+ .fpd-skeleton__section:first-child {
928
+ grid-column: 1 / -1;
929
+ }
930
+ }
931
+
1007
932
  @media (max-width: 1100px) {
1008
933
  .fpd-sections,
1009
934
  .fpd-skeleton__sections {