imeik-bizui 2.7.6 → 2.7.7

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.
@@ -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
@@ -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 {
@@ -142,7 +142,7 @@
142
142
  :key="i"
143
143
  class="fpd-section__footer-item"
144
144
  >
145
- {{ v }}
145
+ {{ formatFooterValue(v) }}
146
146
  </span>
147
147
  </template>
148
148
  <template v-else>{{ section.footer.text }}</template>
@@ -173,6 +173,7 @@
173
173
 
174
174
  <script>
175
175
  import { DEFAULT_DETAIL_FORMULAS, formatNumber, formatSignedNumber } from './utils'
176
+ import { getDisplayMode, subscribeDisplayMode } from './displayPrefs'
176
177
  import FloorPriceSectionPager from './FloorPriceSectionPager.vue'
177
178
 
178
179
  export default {
@@ -252,7 +253,8 @@ export default {
252
253
  data() {
253
254
  return {
254
255
  sectionPages: {},
255
- isNarrow: false
256
+ isNarrow: false,
257
+ displayMode: getDisplayMode()
256
258
  }
257
259
  },
258
260
  computed: {
@@ -348,11 +350,15 @@ export default {
348
350
  if (typeof window !== 'undefined') {
349
351
  window.addEventListener('resize', this.updateNarrow)
350
352
  }
353
+ this._unsubDisplayMode = subscribeDisplayMode((mode) => {
354
+ this.displayMode = mode
355
+ })
351
356
  },
352
357
  beforeDestroy() {
353
358
  if (typeof window !== 'undefined') {
354
359
  window.removeEventListener('resize', this.updateNarrow)
355
360
  }
361
+ if (this._unsubDisplayMode) this._unsubDisplayMode()
356
362
  },
357
363
  methods: {
358
364
  updateNarrow() {
@@ -397,9 +403,9 @@ export default {
397
403
  },
398
404
  formatCell(value, col = {}) {
399
405
  if (col.type === 'number' || col.type === 'highlight' || col.digits != null) {
400
- // 金额按接口原样展示(不千分位、不截断小数)
401
406
  return formatNumber(value, {
402
407
  digits: col.digits != null ? col.digits : null,
408
+ mode: this.displayMode,
403
409
  fallback: '-'
404
410
  })
405
411
  }
@@ -409,9 +415,20 @@ export default {
409
415
  formatGapCell(value, col = {}) {
410
416
  return formatSignedNumber(value, {
411
417
  digits: col.digits != null ? col.digits : null,
418
+ mode: this.displayMode,
412
419
  fallback: '-'
413
420
  })
414
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
+ },
415
432
  gapClass(value, row) {
416
433
  const n = Number(value != null ? value : row && row.gap)
417
434
  if (Number.isNaN(n) || n >= 0) return ''
@@ -0,0 +1,62 @@
1
+ /**
2
+ * 底价价格展示偏好(仅内存,不持久化;刷新页面恢复默认策略)
3
+ *
4
+ * fixed2 — 默认:一律两位小数
5
+ * padMin2 — 托底:不足两位补 0,两位及以上原样
6
+ * raw — 接口原始字符串 / Number 原样
7
+ */
8
+
9
+ export const DISPLAY_MODE = {
10
+ FIXED_2: 'fixed2',
11
+ PAD_MIN_2: 'padMin2',
12
+ RAW: 'raw'
13
+ }
14
+
15
+ export const DEFAULT_DISPLAY_MODE = DISPLAY_MODE.FIXED_2
16
+
17
+ export const DISPLAY_MODE_OPTIONS = [
18
+ { value: DISPLAY_MODE.FIXED_2, label: '固定两位小数', desc: '201 → 201.00,1442.7875 → 1442.79' },
19
+ { value: DISPLAY_MODE.PAD_MIN_2, label: '不足两位补齐', desc: '201 → 201.00,1442.7875 保持原样' },
20
+ { value: DISPLAY_MODE.RAW, label: '展示原始数据', desc: '接口返回什么就显示什么' }
21
+ ]
22
+
23
+ const listeners = new Set()
24
+
25
+ let currentMode = DEFAULT_DISPLAY_MODE
26
+ let settingsUnlocked = false
27
+
28
+ export function getDisplayMode() {
29
+ return currentMode
30
+ }
31
+
32
+ export function setDisplayMode(mode) {
33
+ const next = Object.values(DISPLAY_MODE).includes(mode) ? mode : DEFAULT_DISPLAY_MODE
34
+ currentMode = next
35
+ listeners.forEach((fn) => {
36
+ try {
37
+ fn(next)
38
+ } catch (e) {
39
+ /* ignore */
40
+ }
41
+ })
42
+ return next
43
+ }
44
+
45
+ export function subscribeDisplayMode(fn) {
46
+ if (typeof fn !== 'function') return () => {}
47
+ listeners.add(fn)
48
+ return () => listeners.delete(fn)
49
+ }
50
+
51
+ export function isSettingsUnlocked() {
52
+ return settingsUnlocked
53
+ }
54
+
55
+ export function unlockSettings() {
56
+ settingsUnlocked = true
57
+ return true
58
+ }
59
+
60
+ export function lockSettings() {
61
+ settingsUnlocked = false
62
+ }
@@ -8,7 +8,6 @@ import {
8
8
  DEFAULT_STATUS_LABEL,
9
9
  buildResultCopy,
10
10
  computePriceGap,
11
- formatNumber,
12
11
  resolvePanelState,
13
12
  roundMoney
14
13
  } from './utils'
@@ -151,7 +150,7 @@ function buildSections(data = {}) {
151
150
  footer: {
152
151
  label: '合计(含本次)',
153
152
  theme: 'purple',
154
- values: [`金额 ${formatNumber(sumBy(saleRows, 'amount'))}`]
153
+ values: [{ kind: 'money', label: '金额', value: sumBy(saleRows, 'amount') }]
155
154
  }
156
155
  })
157
156
  }
@@ -172,8 +171,8 @@ function buildSections(data = {}) {
172
171
  label: '合计(含本次)',
173
172
  theme: 'danger',
174
173
  values: [
175
- `数量 ${sumBy(nonSaleRows, 'qty')}`,
176
- `折算费用 ${formatNumber(sumBy(nonSaleRows, 'amount'))}`
174
+ { kind: 'qty', label: '数量', value: sumBy(nonSaleRows, 'qty') },
175
+ { kind: 'money', label: '折算费用', value: sumBy(nonSaleRows, 'amount') }
177
176
  ]
178
177
  }
179
178
  })
@@ -193,7 +192,7 @@ function buildSections(data = {}) {
193
192
  footer: {
194
193
  label: '合计(含本次)',
195
194
  theme: 'danger',
196
- values: [formatNumber(sumBy(expenseRows, 'costAmount'))]
195
+ values: [{ kind: 'money', value: sumBy(expenseRows, 'costAmount') }]
197
196
  }
198
197
  })
199
198
  }