imeik-bizui 2.7.8 → 2.8.0

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.
@@ -279,3 +279,51 @@ export function getLizhenBasePriceDetail(data) {
279
279
  data
280
280
  })
281
281
  }
282
+
283
+ /**
284
+ * 个人预算概述
285
+ * POST /application-center-admin/openApi/personBudget/overview
286
+ */
287
+ export function getPersonBudgetOverview(data) {
288
+ return request({
289
+ url: '/application-center-admin/openApi/personBudget/overview',
290
+ method: 'post',
291
+ data
292
+ })
293
+ }
294
+
295
+ /**
296
+ * 个人预算详情(概述 + 使用/业绩明细分页快照)
297
+ * POST /application-center-admin/openApi/personBudget/detail
298
+ */
299
+ export function getPersonBudgetDetail(data) {
300
+ return request({
301
+ url: '/application-center-admin/openApi/personBudget/detail',
302
+ method: 'post',
303
+ data
304
+ })
305
+ }
306
+
307
+ /**
308
+ * 个人预算使用明细分页
309
+ * POST /application-center-admin/openApi/personBudget/usagePage
310
+ */
311
+ export function getPersonBudgetUsagePage(data) {
312
+ return request({
313
+ url: '/application-center-admin/openApi/personBudget/usagePage',
314
+ method: 'post',
315
+ data
316
+ })
317
+ }
318
+
319
+ /**
320
+ * 个人预算业绩明细分页
321
+ * POST /application-center-admin/openApi/personBudget/performancePage
322
+ */
323
+ export function getPersonBudgetPerformancePage(data) {
324
+ return request({
325
+ url: '/application-center-admin/openApi/personBudget/performancePage',
326
+ method: 'post',
327
+ data
328
+ })
329
+ }
@@ -81,3 +81,39 @@ export function getProductPriceDetail(data) {
81
81
  data
82
82
  })
83
83
  }
84
+
85
+ /**
86
+ * 个人预算概述(实时不传 applyNumber;查看只传 applyNumber)
87
+ * POST /mms/admin/custom/getPersonBudgetOverview
88
+ */
89
+ export function getPersonBudgetOverview(data) {
90
+ return request({
91
+ url: '/mms/admin/custom/getPersonBudgetOverview',
92
+ method: 'post',
93
+ data
94
+ })
95
+ }
96
+
97
+ /**
98
+ * 个人预算使用明细独立分页
99
+ * POST /mms/admin/custom/getPersonBudgetUsagePage
100
+ */
101
+ export function getPersonBudgetUsagePage(data) {
102
+ return request({
103
+ url: '/mms/admin/custom/getPersonBudgetUsagePage',
104
+ method: 'post',
105
+ data
106
+ })
107
+ }
108
+
109
+ /**
110
+ * 个人预算业绩明细独立分页
111
+ * POST /mms/admin/custom/getPersonBudgetPerformancePage
112
+ */
113
+ export function getPersonBudgetPerformancePage(data) {
114
+ return request({
115
+ url: '/mms/admin/custom/getPersonBudgetPerformancePage',
116
+ method: 'post',
117
+ data
118
+ })
119
+ }
@@ -720,6 +720,14 @@ export default {
720
720
  background-color: transparent !important;
721
721
  }
722
722
 
723
+ /*
724
+ * 右固定列的左分割线原本是前一列的 border-right,属于主表;
725
+ * 列宽有小数时会被固定层盖住,故由固定列自绘左线
726
+ */
727
+ .el-table__fixed-right .el-table__cell:not(.is-hidden):not(.gutter) {
728
+ box-shadow: inset 1px 0 0 #eef0f3;
729
+ }
730
+
723
731
  .el-table__header-wrapper,
724
732
  .el-table__body-wrapper {
725
733
  width: 100% !important;
@@ -0,0 +1,121 @@
1
+ # 个人预算看板(FieldPersonBudgetBoard)
2
+
3
+ > 范围:物资系统 · 会议系统 · 申请中心 · **imeik-bizui**
4
+ > 目标:bizui 只负责「选接口 + 拉数 + 看板 UI」;**请求体组装在业务系统**
5
+ > 对齐:`FieldFloorPriceBoard`(底价看板)同一套分层
6
+
7
+ ---
8
+
9
+ ## 一、目录结构
10
+
11
+ ```
12
+ imeik-bizui/src/bizui/
13
+ ├── api/mms.js # 物资/会议 getPersonBudgetOverview / UsagePage / PerformancePage
14
+ ├── api/applycenter.js # 申请中心 openApi/personBudget/*
15
+ └── src/
16
+ └── FieldPersonBudgetBoard/
17
+ ├── index.vue # scene → 接口映射 + Dashboard
18
+ ├── README.md
19
+ └── core/ # Overview / Dashboard / Detail / format / mock
20
+ ```
21
+
22
+ `Vue.use(bizui)` 后:
23
+
24
+ ```vue
25
+ <FieldPersonBudgetBoard
26
+ scene="supplies"
27
+ :request-payload="payload"
28
+ :show-detail-link="hasPerm"
29
+ @submit-blocked="onBlocked"
30
+ />
31
+ ```
32
+
33
+ 移动端想保留弹窗:加 `narrow-detail-mode="dialog"`(默认 `inline`,在看板下方展开)。
34
+
35
+ `scene`:`supplies` | `meeting` | `apply` / `applyCenter`。
36
+
37
+ 请求体组装在业务系统(如物资 / 申请中心 `buildRequestPayload.js`)。
38
+
39
+ ---
40
+
41
+ ## 二、分层原则
42
+
43
+ 1. **接口**在 `api/mms.js`(物资/会议)与 `api/applycenter.js`(申请中心)。
44
+ 2. **对外只注册** `FieldPersonBudgetBoard`;`index.vue` 按 `scene` 选 fetch。
45
+ 3. **宿主组装** `requestPayload` + 权限;不写 debounce / 看板 UI。
46
+ 4. **bizui 不解析**各域表单字段(费用承担人、snapshotList、整表 formDataJson 等)。
47
+
48
+ ```
49
+ 业务系统:表单字段 → requestPayload
50
+ ↓
51
+ bizui: scene 选接口 → Dashboard 拉数 → Overview / Detail
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 三、标准契约
57
+
58
+ | 类型 | 内容 |
59
+ | --- | --- |
60
+ | Props | `scene`、`requestPayload`、`showDetailLink`、`narrowDetailMode`、`useMock`、`emptyText`、`debounceMs` |
61
+ | Events | `submit-blocked`:`{ blocked, warn }`(禁止提交 / 预警) |
62
+ | 明细形态 | 宽屏始终弹窗;窄屏(≤768px)由 `narrowDetailMode`:`inline`(默认)或 `dialog` |
63
+ | 物资/会议 · 查看 | `{ applyNumber }`(真实申请单号,不是 objectCode) |
64
+ | 物资/会议 · 实时 | `{ costBearEmployeeNo, costType, materialList[] }`;`materialList` **允许 `[]`**;可选 `budgetDate` |
65
+ | 申请中心 | `{ forceRefresh, formDataJson?, formDataNumber? }`(`PersonBudgetQueryDTO`);明细另带 `usageCurrPage` / `performanceCurrPage` 等 |
66
+ | 编辑态(物资) | **不传** `applyNumber`(即使草稿已有单号) |
67
+
68
+ ---
69
+
70
+ ## 四、scene → 接口
71
+
72
+ | 域 | scene | 接口 |
73
+ | --- | --- | --- |
74
+ | 物资 | `supplies` | `mms.getPersonBudgetOverview` / `UsagePage` / `PerformancePage` |
75
+ | 会议 | `meeting` | 同上(传参同形) |
76
+ | 申请中心 | `apply` / `applyCenter` | `applycenter` `openApi/personBudget/overview|usagePage|performancePage` |
77
+
78
+ ---
79
+
80
+ ## 五、业务侧薄封装示例
81
+
82
+ ### 物资
83
+
84
+ ```js
85
+ export function buildSuppliesPersonBudgetPayload(ctx) {
86
+ if (ctx.isView) {
87
+ const no = String(ctx.applyNumber || '').trim()
88
+ return no ? { applyNumber: no } : null
89
+ }
90
+ const employeeNo = String(ctx.costBearEmployeeNo || '').trim()
91
+ const type = ctx.costType != null && ctx.costType !== '' ? String(ctx.costType) : ''
92
+ if (!employeeNo || !type) return null
93
+ return {
94
+ costBearEmployeeNo: employeeNo,
95
+ costType: type,
96
+ materialList: mapMaterialList(ctx.snapshotList) // 允许 []
97
+ }
98
+ }
99
+ ```
100
+
101
+ ### 申请中心
102
+
103
+ ```js
104
+ export function buildApplyPersonBudgetPayload(ctx) {
105
+ const forceRefresh = !!ctx.forceRefresh
106
+ const formDataNumber = String(ctx.formDataNumber || '').trim()
107
+ const formDataJson = typeof ctx.formData === 'string'
108
+ ? ctx.formData
109
+ : JSON.stringify(ctx.formData || {})
110
+ if (forceRefresh) {
111
+ return hasMeaningfulFormData(ctx.formData) ? { forceRefresh: true, formDataJson } : null
112
+ }
113
+ return formDataNumber ? { forceRefresh: false, formDataNumber, formDataJson } : null
114
+ }
115
+ ```
116
+
117
+ ---
118
+
119
+ ## 六、一句话
120
+
121
+ **业务侧拼好 `requestPayload`;bizui 按 `scene` 调对应个人预算接口并展示。**
@@ -0,0 +1,295 @@
1
+ <!--
2
+ 个人预算看板编排:防抖拉概述、宽屏弹窗 / 窄屏就地展开、禁止态事件
3
+ 对齐底价 FieldFloorPriceBoard:≤768 默认 inline
4
+
5
+ 不负责:各业务域字段映射、拼请求体 —— 由业务系统组装后经 FieldPersonBudgetBoard 传入。
6
+ -->
7
+ <template>
8
+ <div class="person-budget-dashboard">
9
+ <PersonBudgetOverview
10
+ :overview="overview"
11
+ :loading="loading"
12
+ :error="error"
13
+ :empty-text="emptyText"
14
+ :show-detail-link="showDetailLink && !!overview && !useDemo"
15
+ :detail-link-text="detailLinkText"
16
+ @view-detail="onViewDetail"
17
+ />
18
+ <PersonBudgetDetailPanel
19
+ v-if="useInlineDetail && inlineOpen"
20
+ variant="inline"
21
+ :active="inlineOpen"
22
+ :overview="overview"
23
+ :request-payload="requestPayload"
24
+ :fetch-usage-page="fetchUsagePage"
25
+ :fetch-performance-page="fetchPerformancePage"
26
+ @collapse="collapseInline"
27
+ />
28
+ <PersonBudgetDetailDialog
29
+ :visible.sync="detailVisible"
30
+ :overview="overview"
31
+ :request-payload="requestPayload"
32
+ :fetch-usage-page="fetchUsagePage"
33
+ :fetch-performance-page="fetchPerformancePage"
34
+ />
35
+ </div>
36
+ </template>
37
+
38
+ <script>
39
+ import { BUDGET_STATUS, normalizeBudgetStatus } from './format'
40
+ import { createDemoOverview } from './mock'
41
+ import PersonBudgetOverview from './PersonBudgetOverview.vue'
42
+ import PersonBudgetDetailDialog from './PersonBudgetDetailDialog.vue'
43
+ import PersonBudgetDetailPanel from './PersonBudgetDetailPanel.vue'
44
+
45
+ const NARROW_BREAKPOINT = 768
46
+
47
+ function debounce(fn, wait) {
48
+ let timer = null
49
+ const wrapped = function() {
50
+ const ctx = this
51
+ const args = arguments
52
+ if (timer) clearTimeout(timer)
53
+ timer = setTimeout(() => {
54
+ timer = null
55
+ fn.apply(ctx, args)
56
+ }, wait)
57
+ }
58
+ wrapped.cancel = function() {
59
+ if (timer) clearTimeout(timer)
60
+ timer = null
61
+ }
62
+ return wrapped
63
+ }
64
+
65
+ function payloadSignature(payload) {
66
+ try {
67
+ return JSON.stringify(payload || null)
68
+ } catch (e) {
69
+ return String(payload)
70
+ }
71
+ }
72
+
73
+ export default {
74
+ name: 'PersonBudgetDashboard',
75
+ components: {
76
+ PersonBudgetOverview,
77
+ PersonBudgetDetailDialog,
78
+ PersonBudgetDetailPanel
79
+ },
80
+ props: {
81
+ requestPayload: {
82
+ type: Object,
83
+ default: null
84
+ },
85
+ showDetailLink: {
86
+ type: Boolean,
87
+ default: true
88
+ },
89
+ emptyText: {
90
+ type: String,
91
+ default: '请完善费用承担人和费用类型后查看个人预算'
92
+ },
93
+ debounceMs: {
94
+ type: Number,
95
+ default: 320
96
+ },
97
+ /**
98
+ * 窄屏明细形态:inline 看板下方展开(默认)| dialog 仍用弹窗
99
+ */
100
+ narrowDetailMode: {
101
+ type: String,
102
+ default: 'inline',
103
+ validator(v) {
104
+ return ['inline', 'dialog'].includes(v)
105
+ }
106
+ },
107
+ /** (payload) => Promise<{ code, data, message }>;useDemo 时可为空 */
108
+ fetchOverview: {
109
+ type: Function,
110
+ default: null
111
+ },
112
+ fetchUsagePage: {
113
+ type: Function,
114
+ default: null
115
+ },
116
+ fetchPerformancePage: {
117
+ type: Function,
118
+ default: null
119
+ },
120
+ /** true 时不调接口,展示演示概述(配置态) */
121
+ useDemo: {
122
+ type: Boolean,
123
+ default: false
124
+ }
125
+ },
126
+ data() {
127
+ return {
128
+ loading: false,
129
+ error: '',
130
+ overview: null,
131
+ detailVisible: false,
132
+ inlineOpen: false,
133
+ isNarrow: false,
134
+ reqSeq: 0,
135
+ debouncedFetch: null
136
+ }
137
+ },
138
+ computed: {
139
+ useInlineDetail() {
140
+ return this.isNarrow && this.narrowDetailMode === 'inline'
141
+ },
142
+ detailLinkText() {
143
+ if (!this.useInlineDetail) return '查看详情 →'
144
+ return this.inlineOpen ? '收起详情 ↑' : '查看详情 ↓'
145
+ }
146
+ },
147
+ watch: {
148
+ requestPayload: {
149
+ deep: true,
150
+ handler() {
151
+ if (this.debouncedFetch) this.debouncedFetch()
152
+ }
153
+ },
154
+ useDemo: {
155
+ immediate: true,
156
+ handler(val) {
157
+ if (val) {
158
+ this.overview = createDemoOverview()
159
+ this.error = ''
160
+ this.loading = false
161
+ this.emitSubmitBlocked(false, false)
162
+ return
163
+ }
164
+ if (this.debouncedFetch) this.debouncedFetch()
165
+ }
166
+ },
167
+ useInlineDetail(inline) {
168
+ if (inline && this.detailVisible) {
169
+ this.detailVisible = false
170
+ this.inlineOpen = true
171
+ } else if (!inline) {
172
+ this.inlineOpen = false
173
+ }
174
+ },
175
+ overview(val) {
176
+ if (!val && this.inlineOpen) this.inlineOpen = false
177
+ }
178
+ },
179
+ created() {
180
+ this.debouncedFetch = debounce(() => {
181
+ this.fetchOverviewNow()
182
+ }, this.debounceMs)
183
+ if (!this.useDemo) this.debouncedFetch()
184
+ },
185
+ mounted() {
186
+ this.updateNarrow()
187
+ if (typeof window !== 'undefined') {
188
+ window.addEventListener('resize', this.updateNarrow)
189
+ }
190
+ },
191
+ beforeDestroy() {
192
+ if (this.debouncedFetch && this.debouncedFetch.cancel) {
193
+ this.debouncedFetch.cancel()
194
+ }
195
+ if (typeof window !== 'undefined') {
196
+ window.removeEventListener('resize', this.updateNarrow)
197
+ }
198
+ this.emitSubmitBlocked(false, false)
199
+ },
200
+ methods: {
201
+ updateNarrow() {
202
+ if (typeof window === 'undefined') return
203
+ this.isNarrow = window.innerWidth <= NARROW_BREAKPOINT
204
+ },
205
+ emitSubmitBlocked(blocked, warn) {
206
+ this.$emit('submit-blocked', { blocked: !!blocked, warn: !!warn })
207
+ },
208
+ clearBoard() {
209
+ this.overview = null
210
+ this.error = ''
211
+ this.loading = false
212
+ this.inlineOpen = false
213
+ this.emitSubmitBlocked(false, false)
214
+ },
215
+ async fetchOverviewNow() {
216
+ if (this.useDemo) {
217
+ this.overview = createDemoOverview()
218
+ this.error = ''
219
+ this.loading = false
220
+ this.emitSubmitBlocked(false, false)
221
+ return
222
+ }
223
+
224
+ const payload = this.requestPayload
225
+ const sig = payloadSignature(payload)
226
+ const seq = ++this.reqSeq
227
+
228
+ if (!payload || typeof this.fetchOverview !== 'function') {
229
+ this.clearBoard()
230
+ return
231
+ }
232
+
233
+ this.loading = true
234
+ this.error = ''
235
+ try {
236
+ const res = await this.fetchOverview(payload)
237
+ if (seq !== this.reqSeq || sig !== payloadSignature(this.requestPayload)) return
238
+
239
+ if (!res || res.code !== 200) {
240
+ this.overview = null
241
+ this.error = (res && res.message) || '个人预算概述获取失败'
242
+ this.emitSubmitBlocked(false, false)
243
+ return
244
+ }
245
+
246
+ this.overview = res.data || null
247
+ const status = normalizeBudgetStatus(this.overview && this.overview.status)
248
+ this.emitSubmitBlocked(
249
+ status === BUDGET_STATUS.FORBID,
250
+ status === BUDGET_STATUS.WARN
251
+ )
252
+ } catch (e) {
253
+ if (seq !== this.reqSeq) return
254
+ this.overview = null
255
+ this.error = (e && e.message) || '个人预算概述获取失败'
256
+ this.emitSubmitBlocked(false, false)
257
+ } finally {
258
+ if (seq === this.reqSeq) this.loading = false
259
+ }
260
+ },
261
+ onViewDetail() {
262
+ if (!this.overview) return
263
+ // 配置态演示:无真实 requestPayload / fetch,不允许进明细
264
+ if (this.useDemo || !this.requestPayload) return
265
+ if (typeof this.fetchUsagePage !== 'function' || typeof this.fetchPerformancePage !== 'function') {
266
+ return
267
+ }
268
+ if (this.useInlineDetail) {
269
+ this.toggleInline()
270
+ return
271
+ }
272
+ this.detailVisible = true
273
+ },
274
+ toggleInline() {
275
+ this.inlineOpen = !this.inlineOpen
276
+ },
277
+ collapseInline() {
278
+ this.inlineOpen = false
279
+ this.$nextTick(() => {
280
+ const el = this.$el
281
+ if (!el || typeof el.getBoundingClientRect !== 'function') return
282
+ if (el.getBoundingClientRect().top < 0) el.scrollIntoView({ block: 'start' })
283
+ })
284
+ }
285
+ }
286
+ }
287
+ </script>
288
+
289
+ <style lang="scss" scoped>
290
+ .person-budget-dashboard {
291
+ width: 100%;
292
+ max-width: 100%;
293
+ min-width: 0;
294
+ }
295
+ </style>
@@ -0,0 +1,84 @@
1
+ <!--
2
+ 个人预算详情弹窗(宽屏)
3
+ -->
4
+ <template>
5
+ <ImDialog
6
+ :visible.sync="dialogVisible"
7
+ custom-class="person-budget-detail-dialog-wrap"
8
+ title="个人预算详情"
9
+ :width="dialogWidth"
10
+ height="auto"
11
+ top="3vh"
12
+ container-style="maxHeight: 90vh;overflow-y: auto;"
13
+ append-to-body
14
+ destroy-on-close
15
+ hide-footer
16
+ @closeDialog="onClosed"
17
+ >
18
+ <PersonBudgetDetailPanel
19
+ variant="dialog"
20
+ :active="visible"
21
+ :overview="overview"
22
+ :request-payload="requestPayload"
23
+ :fetch-usage-page="fetchUsagePage"
24
+ :fetch-performance-page="fetchPerformancePage"
25
+ />
26
+ </ImDialog>
27
+ </template>
28
+
29
+ <script>
30
+ import PersonBudgetDetailPanel from './PersonBudgetDetailPanel.vue'
31
+
32
+ export default {
33
+ name: 'PersonBudgetDetailDialog',
34
+ components: {
35
+ PersonBudgetDetailPanel
36
+ },
37
+ props: {
38
+ visible: { type: Boolean, default: false },
39
+ overview: { type: Object, default: null },
40
+ requestPayload: { type: Object, default: null },
41
+ fetchUsagePage: { type: Function, required: true },
42
+ fetchPerformancePage: { type: Function, required: true }
43
+ },
44
+ data() {
45
+ return {
46
+ viewportWidth: typeof window !== 'undefined' ? window.innerWidth : 1360
47
+ }
48
+ },
49
+ computed: {
50
+ dialogVisible: {
51
+ get() {
52
+ return this.visible
53
+ },
54
+ set(v) {
55
+ this.$emit('update:visible', v)
56
+ }
57
+ },
58
+ dialogWidth() {
59
+ const w = this.viewportWidth || 1360
60
+ return `${Math.min(1360, Math.max(w - 48, 720))}px`
61
+ }
62
+ },
63
+ mounted() {
64
+ this.onResize = () => {
65
+ this.viewportWidth = window.innerWidth
66
+ }
67
+ window.addEventListener('resize', this.onResize)
68
+ },
69
+ beforeDestroy() {
70
+ if (this.onResize) window.removeEventListener('resize', this.onResize)
71
+ },
72
+ methods: {
73
+ onClosed() {
74
+ this.$emit('update:visible', false)
75
+ }
76
+ }
77
+ }
78
+ </script>
79
+
80
+ <style lang="scss">
81
+ .el-dialog.person-budget-detail-dialog-wrap {
82
+ max-width: calc(100vw - 48px);
83
+ }
84
+ </style>