imeik-bizui 2.7.6 → 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
  | 申请中心入参 | 见丽臻底价接口文档 |
@@ -7,7 +7,7 @@
7
7
  <div v-if="visible" class="floor-price-board">
8
8
  <div v-if="showTitle" class="fpb-title">
9
9
  <span class="fpb-title__bar" />
10
- <span class="fpb-title__text">{{ title }}</span>
10
+ <span class="fpb-title__text" @click="onSecretTitleClick">{{ title }}</span>
11
11
  </div>
12
12
 
13
13
  <div
@@ -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
@@ -28,7 +28,7 @@
28
28
  :alt="resolvedStatusLabel"
29
29
  aria-hidden="true"
30
30
  >
31
- <h2 class="fpb-result__title">{{ resolvedResultTitle }}</h2>
31
+ <h2 class="fpb-result__title" @click="onSecretTitleClick">{{ resolvedResultTitle }}</h2>
32
32
  <span class="fpb-badge">{{ resolvedStatusLabel }}</span>
33
33
  <button
34
34
  v-if="useDemo"
@@ -39,6 +39,52 @@
39
39
  >
40
40
  Mock
41
41
  </button>
42
+ <el-popover
43
+ v-if="settingsUnlocked"
44
+ v-model="settingsOpen"
45
+ placement="bottom-start"
46
+ width="300"
47
+ trigger="click"
48
+ >
49
+ <div class="fpb-settings">
50
+ <div class="fpb-settings__head">
51
+ <span class="fpb-settings__title">价格展示</span>
52
+ <span class="fpb-settings__hint">临时生效,刷新恢复默认</span>
53
+ </div>
54
+ <div
55
+ v-for="opt in displayModeOptions"
56
+ :key="opt.value"
57
+ :class="['fpb-settings__option', { 'is-active': displayMode === opt.value }]"
58
+ role="radio"
59
+ :aria-checked="displayMode === opt.value ? 'true' : 'false'"
60
+ @click="selectDisplayMode(opt.value)"
61
+ >
62
+ <span class="fpb-settings__radio" />
63
+ <span class="fpb-settings__body">
64
+ <span class="fpb-settings__label">
65
+ {{ opt.label }}
66
+ <em v-if="opt.value === defaultDisplayMode" class="fpb-settings__tag">默认</em>
67
+ </span>
68
+ <span class="fpb-settings__desc">{{ opt.desc }}</span>
69
+ </span>
70
+ <i v-if="displayMode === opt.value" class="el-icon-check fpb-settings__check" />
71
+ </div>
72
+ <div class="fpb-settings__foot">
73
+ <button type="button" class="fpb-settings__link" @click="onLockSettings">
74
+ 隐藏设置入口
75
+ </button>
76
+ </div>
77
+ </div>
78
+ <button
79
+ slot="reference"
80
+ type="button"
81
+ class="fpb-settings-trigger"
82
+ title="价格展示设置"
83
+ aria-label="价格展示设置"
84
+ >
85
+ <i class="el-icon-setting" />
86
+ </button>
87
+ </el-popover>
42
88
  </div>
43
89
  <p v-if="resolvedResultDescription" class="fpb-result__desc">
44
90
  {{ resolvedResultDescription }}
@@ -105,9 +151,9 @@
105
151
  <dt>与标准差额</dt>
106
152
  <dd
107
153
  :class="{ 'is-danger': row.hit, 'is-ok': !row.hit && row.gap > 0 }"
108
- :title="row.gapLabel || '-'"
154
+ :title="gapLabelOf(row)"
109
155
  >
110
- {{ row.gapLabel || '-' }}
156
+ {{ gapLabelOf(row) }}
111
157
  </dd>
112
158
  </div>
113
159
  </dl>
@@ -146,9 +192,20 @@ import {
146
192
  DEFAULT_STATUS_LABEL,
147
193
  PANEL_STATE,
148
194
  buildResultCopy,
195
+ formatGapLabel,
149
196
  formatMoney,
150
197
  formatNumber
151
198
  } from './utils'
199
+ import {
200
+ DEFAULT_DISPLAY_MODE,
201
+ DISPLAY_MODE_OPTIONS,
202
+ getDisplayMode,
203
+ isSettingsUnlocked,
204
+ lockSettings,
205
+ setDisplayMode,
206
+ subscribeDisplayMode,
207
+ unlockSettings
208
+ } from './displayPrefs'
152
209
  import FloorPriceSectionPager from './FloorPriceSectionPager.vue'
153
210
  import iconBlocked from '../assets/icon-blocked.png'
154
211
  import iconPass from '../assets/icon-pass.png'
@@ -160,6 +217,9 @@ const STATUS_ICON = {
160
217
  [PANEL_STATE.EXEMPT]: iconExempt
161
218
  }
162
219
 
220
+ const SECRET_CLICK_NEED = 5
221
+ const SECRET_CLICK_WINDOW_MS = 2500
222
+
163
223
  export default {
164
224
  name: 'FloorPriceBoard',
165
225
  components: {
@@ -248,7 +308,7 @@ export default {
248
308
  type: Boolean,
249
309
  default: false
250
310
  },
251
- /** 价格/金额小数位;默认 null 不强制,跟随接口原值 */
311
+ /** 价格/金额小数位;显式传入时覆盖 displayMode */
252
312
  numberDigits: {
253
313
  type: Number,
254
314
  default: null
@@ -264,7 +324,14 @@ export default {
264
324
  },
265
325
  data() {
266
326
  return {
267
- productPage: 1
327
+ productPage: 1,
328
+ displayMode: getDisplayMode(),
329
+ displayModeOptions: DISPLAY_MODE_OPTIONS,
330
+ defaultDisplayMode: DEFAULT_DISPLAY_MODE,
331
+ settingsUnlocked: isSettingsUnlocked(),
332
+ settingsOpen: false,
333
+ secretClickCount: 0,
334
+ secretClickTimer: null
268
335
  }
269
336
  },
270
337
  computed: {
@@ -314,8 +381,14 @@ export default {
314
381
  )
315
382
  },
316
383
  applyTotalFeeText() {
384
+ // 依赖 displayMode,切换展示偏好时同步刷新
385
+ const mode = this.displayMode
317
386
  if (this.applyTotalFee == null || this.applyTotalFee === '') return ''
318
- return `${formatNumber(this.applyTotalFee)} 元`
387
+ return `${formatNumber(this.applyTotalFee, {
388
+ digits: this.numberDigits,
389
+ mode,
390
+ fallback: '-'
391
+ })} 元`
319
392
  },
320
393
  yearText() {
321
394
  if (this.year == null || this.year === '') return ''
@@ -349,9 +422,36 @@ export default {
349
422
  if (this.productPage > maxPage) this.productPage = 1
350
423
  }
351
424
  },
425
+ created() {
426
+ this._unsubDisplayMode = subscribeDisplayMode((mode) => {
427
+ this.displayMode = mode
428
+ })
429
+ },
430
+ beforeDestroy() {
431
+ if (this._unsubDisplayMode) this._unsubDisplayMode()
432
+ if (this.secretClickTimer) clearTimeout(this.secretClickTimer)
433
+ },
352
434
  methods: {
435
+ formatNumber(v) {
436
+ return formatNumber(v, {
437
+ digits: this.numberDigits,
438
+ mode: this.displayMode,
439
+ fallback: '-'
440
+ })
441
+ },
353
442
  formatMoney(v) {
354
- return formatMoney(v, { digits: this.numberDigits, fallback: '-' })
443
+ return formatMoney(v, {
444
+ digits: this.numberDigits,
445
+ mode: this.displayMode,
446
+ fallback: '-'
447
+ })
448
+ },
449
+ gapLabelOf(row) {
450
+ if (!row) return '-'
451
+ if (row.gap === undefined || row.gap === null || row.gap === '') {
452
+ return row.gapLabel || '-'
453
+ }
454
+ return formatGapLabel(row.gap, { mode: this.displayMode }) || row.gapLabel || '-'
355
455
  },
356
456
  shiftAria(row) {
357
457
  if (!row) return ''
@@ -365,6 +465,39 @@ export default {
365
465
  },
366
466
  onProductPage(page) {
367
467
  this.productPage = page
468
+ },
469
+ /** 连点标题 5 次:未解锁则显示设置入口,已解锁则隐藏 */
470
+ onSecretTitleClick() {
471
+ this.secretClickCount += 1
472
+ if (this.secretClickTimer) clearTimeout(this.secretClickTimer)
473
+ this.secretClickTimer = setTimeout(() => {
474
+ this.secretClickCount = 0
475
+ this.secretClickTimer = null
476
+ }, SECRET_CLICK_WINDOW_MS)
477
+ if (this.secretClickCount < SECRET_CLICK_NEED) return
478
+ this.secretClickCount = 0
479
+ if (this.secretClickTimer) {
480
+ clearTimeout(this.secretClickTimer)
481
+ this.secretClickTimer = null
482
+ }
483
+ if (this.settingsUnlocked) {
484
+ this.onLockSettings()
485
+ return
486
+ }
487
+ unlockSettings()
488
+ this.settingsUnlocked = true
489
+ // popover 的 reference 刚渲染出来,需等 DOM 就绪后再打开
490
+ this.$nextTick(() => {
491
+ this.settingsOpen = true
492
+ })
493
+ },
494
+ selectDisplayMode(mode) {
495
+ this.displayMode = setDisplayMode(mode)
496
+ },
497
+ onLockSettings() {
498
+ lockSettings()
499
+ this.settingsOpen = false
500
+ this.settingsUnlocked = false
368
501
  }
369
502
  }
370
503
  }
@@ -372,6 +505,7 @@ export default {
372
505
 
373
506
  <style lang="scss" scoped>
374
507
  .floor-price-board {
508
+ position: relative;
375
509
  width: 100%;
376
510
  max-width: 100%;
377
511
  min-width: 0;
@@ -415,6 +549,8 @@ export default {
415
549
  font-weight: 600;
416
550
  color: var(--fpb-text);
417
551
  line-height: 1;
552
+ cursor: default;
553
+ user-select: none;
418
554
  }
419
555
 
420
556
  .fpb-body {
@@ -518,6 +654,181 @@ export default {
518
654
  font-size: 17px;
519
655
  line-height: 24px;
520
656
  font-weight: 600;
657
+ cursor: default;
658
+ user-select: none;
659
+ }
660
+
661
+ .fpb-settings-trigger {
662
+ display: inline-flex;
663
+ align-items: center;
664
+ justify-content: center;
665
+ width: 24px;
666
+ height: 24px;
667
+ padding: 0;
668
+ margin: 0;
669
+ border: none;
670
+ border-radius: 50%;
671
+ background: transparent;
672
+ color: var(--fpb-tertiary);
673
+ font-size: 16px;
674
+ cursor: pointer;
675
+ transition: color 0.15s, background-color 0.15s;
676
+
677
+ &:hover,
678
+ &:focus-visible {
679
+ color: var(--fpb-brand);
680
+ background: rgba(51, 112, 255, 0.08);
681
+ outline: none;
682
+ }
683
+ }
684
+
685
+ /*
686
+ * popover 内容:不用原生 label/input,避免宿主全局样式导致文字重叠。
687
+ * popover 挂在 body 下,拿不到 .floor-price-board 上的变量,需在此重新声明。
688
+ */
689
+ .fpb-settings {
690
+ --fpb-brand: #3370ff;
691
+ font-size: 13px;
692
+ line-height: 20px;
693
+ color: #1f2329;
694
+ }
695
+
696
+ .fpb-settings__head {
697
+ display: flex;
698
+ align-items: baseline;
699
+ justify-content: space-between;
700
+ padding: 0 4px 8px;
701
+ }
702
+
703
+ .fpb-settings__title {
704
+ font-size: 14px;
705
+ font-weight: 600;
706
+ }
707
+
708
+ .fpb-settings__hint {
709
+ font-size: 12px;
710
+ color: #8f959e;
711
+ }
712
+
713
+ .fpb-settings__option {
714
+ display: flex;
715
+ align-items: flex-start;
716
+ gap: 10px;
717
+ padding: 8px 10px;
718
+ border: 1px solid transparent;
719
+ border-radius: 6px;
720
+ cursor: pointer;
721
+ transition: background-color 0.15s, border-color 0.15s;
722
+
723
+ & + & {
724
+ margin-top: 4px;
725
+ }
726
+
727
+ &:hover {
728
+ background: #f5f7fa;
729
+ }
730
+
731
+ &.is-active {
732
+ border-color: var(--fpb-brand);
733
+ background: #f0f5ff;
734
+ }
735
+ }
736
+
737
+ .fpb-settings__radio {
738
+ flex: 0 0 auto;
739
+ position: relative;
740
+ width: 16px;
741
+ height: 16px;
742
+ margin-top: 2px;
743
+ border: 1px solid #c0c4cc;
744
+ border-radius: 50%;
745
+ box-sizing: border-box;
746
+ background: #fff;
747
+ transition: border-color 0.15s, background-color 0.15s;
748
+
749
+ .is-active & {
750
+ border-color: var(--fpb-brand);
751
+ background: var(--fpb-brand);
752
+
753
+ &::after {
754
+ content: '';
755
+ position: absolute;
756
+ top: 50%;
757
+ left: 50%;
758
+ width: 6px;
759
+ height: 6px;
760
+ margin: -3px 0 0 -3px;
761
+ border-radius: 50%;
762
+ background: #fff;
763
+ }
764
+ }
765
+ }
766
+
767
+ .fpb-settings__body {
768
+ flex: 1 1 auto;
769
+ display: flex;
770
+ flex-direction: column;
771
+ gap: 2px;
772
+ min-width: 0;
773
+ }
774
+
775
+ .fpb-settings__label {
776
+ display: flex;
777
+ align-items: center;
778
+ gap: 6px;
779
+ font-weight: 500;
780
+
781
+ .is-active & {
782
+ color: var(--fpb-brand);
783
+ font-weight: 600;
784
+ }
785
+ }
786
+
787
+ .fpb-settings__check {
788
+ flex: 0 0 auto;
789
+ align-self: center;
790
+ color: var(--fpb-brand);
791
+ font-size: 16px;
792
+ font-weight: 700;
793
+ }
794
+
795
+ .fpb-settings__tag {
796
+ padding: 0 5px;
797
+ border-radius: 3px;
798
+ background: #eef0f3;
799
+ color: #646a73;
800
+ font-size: 11px;
801
+ font-style: normal;
802
+ font-weight: 400;
803
+ line-height: 16px;
804
+ }
805
+
806
+ .fpb-settings__desc {
807
+ font-size: 12px;
808
+ line-height: 18px;
809
+ color: #8f959e;
810
+ font-variant-numeric: tabular-nums;
811
+ }
812
+
813
+ .fpb-settings__foot {
814
+ display: flex;
815
+ justify-content: flex-end;
816
+ margin-top: 6px;
817
+ padding: 8px 4px 0;
818
+ border-top: 1px solid #f0f2f5;
819
+ }
820
+
821
+ .fpb-settings__link {
822
+ padding: 0;
823
+ border: none;
824
+ background: transparent;
825
+ color: #8f959e;
826
+ font-size: 12px;
827
+ cursor: pointer;
828
+
829
+ &:hover {
830
+ color: var(--fpb-brand);
831
+ }
521
832
  }
522
833
 
523
834
  .fpb-badge {
@@ -772,35 +1083,125 @@ export default {
772
1083
 
773
1084
  @media (max-width: 640px) {
774
1085
  .fpb-panel {
775
- padding: 16px;
1086
+ padding: 12px;
776
1087
  }
777
1088
 
1089
+ /* 链接钉在标题行右侧,说明文字可占满整行 */
778
1090
  .fpb-result {
779
- gap: 14px;
780
- 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;
781
1125
  }
782
1126
 
783
1127
  .fpb-link {
784
- margin-left: auto;
1128
+ position: absolute;
1129
+ top: 0;
1130
+ right: 0;
1131
+ min-height: 22px;
1132
+ font-size: 13px;
1133
+ line-height: 22px;
785
1134
  }
786
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;
1160
+ }
1161
+
1162
+ /* 与下方「申请前 / 申请后」重复,窄屏省掉这一行 */
787
1163
  .fpb-card__shift {
788
- margin-left: 0;
789
- width: 100%;
1164
+ display: none;
790
1165
  }
791
1166
 
792
1167
  .fpb-price-grid {
793
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
+ }
794
1187
  }
795
1188
 
796
1189
  .fpb-footer {
797
1190
  flex-direction: column;
798
- 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;
799
1199
  }
800
1200
 
801
1201
  .fpb-footer__meta {
802
1202
  justify-content: flex-start;
803
1203
  flex-wrap: wrap;
1204
+ line-height: 18px;
804
1205
  }
805
1206
  }
806
1207
  </style>