@ebiz/designer-components 0.1.21 → 0.1.22

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.
@@ -0,0 +1,348 @@
1
+ <template>
2
+ <div class="ebiz-detail-view" v-loading="loading">
3
+ <!-- 空状态 -->
4
+ <div v-if="!data || Object.keys(data).length === 0" class="empty-state">
5
+ <t-icon name="info-circle" size="48px" />
6
+ <p>暂无数据</p>
7
+ </div>
8
+
9
+ <!-- 错误状态 -->
10
+ <div v-else-if="error" class="error-state">
11
+ <t-icon name="close-circle" size="48px" />
12
+ <p>{{ error }}</p>
13
+ <t-button theme="primary" @click="handleRetry">重试</t-button>
14
+ </div>
15
+
16
+ <!-- 正常内容 -->
17
+ <div v-else class="detail-content">
18
+ <div v-for="group in groupedItems" :key="group.groupName" class="detail-group">
19
+ <!-- 组标题 -->
20
+ <div v-if="group.groupName" class="group-title">
21
+ <div class="group-title-bar"></div>
22
+ <span class="group-title-text">{{ group.groupName }}</span>
23
+ </div>
24
+
25
+ <!-- 字段列表 -->
26
+ <div
27
+ class="detail-fields"
28
+ :class="{ 'vertical-layout': layout === 'vertical' }"
29
+ :style="{
30
+ 'grid-template-columns': `repeat(${columns}, 1fr)`,
31
+ gap: `${gap}px`
32
+ }"
33
+ >
34
+ <EbizDetailItem
35
+ v-for="item in group.items"
36
+ :key="item.key"
37
+ :label="item.label"
38
+ :type="item.type"
39
+ :value="getFieldValue(item.key)"
40
+ :required="item.required"
41
+ :description="item.description"
42
+ :layout="layout"
43
+ :labelWidth="labelWidth"
44
+ :labelColor="labelColor"
45
+ :gap="0"
46
+ :fileMode="item.fileMode"
47
+ :showDownload="item.showDownload"
48
+ @download-file="handleDownloadFile"
49
+ @user-click="handleUserClick"
50
+ @link-click="handleLinkClick"
51
+ />
52
+ </div>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </template>
57
+
58
+ <script setup>
59
+ import { computed, defineProps, defineEmits, ref, watch, onMounted } from 'vue'
60
+ import EbizDetailItem from './EbizDetailItem.vue'
61
+
62
+ const props = defineProps({
63
+ // API配置
64
+ apiUrl: {
65
+ type: String,
66
+ default: ''
67
+ },
68
+ apiMethod: {
69
+ type: String,
70
+ default: 'GET',
71
+ validator: (value) => ['GET', 'POST'].includes(value)
72
+ },
73
+ apiParams: {
74
+ type: Object,
75
+ default: () => ({})
76
+ },
77
+ apiHeaders: {
78
+ type: Object,
79
+ default: () => ({})
80
+ },
81
+
82
+ // 数据源(优先级高于API)
83
+ data: {
84
+ type: Object,
85
+ default: () => ({})
86
+ },
87
+
88
+ // 布局配置
89
+ columns: {
90
+ type: Number,
91
+ default: 2,
92
+ validator: (value) => value >= 1 && value <= 6
93
+ },
94
+ layout: {
95
+ type: String,
96
+ default: 'horizontal',
97
+ validator: (value) => ['horizontal', 'vertical'].includes(value)
98
+ },
99
+ gap: {
100
+ type: Number,
101
+ default: 16
102
+ },
103
+ labelWidth: {
104
+ type: Number,
105
+ default: 120
106
+ },
107
+ labelColor: {
108
+ type: String,
109
+ default: '#666666'
110
+ },
111
+
112
+ // 自动加载
113
+ autoLoad: {
114
+ type: Boolean,
115
+ default: true
116
+ }
117
+ })
118
+
119
+ const emits = defineEmits(['download-file', 'user-click', 'link-click', 'data-loaded', 'error'])
120
+
121
+ // 响应式数据
122
+ const loading = ref(false)
123
+ const error = ref('')
124
+ const apiData = ref({})
125
+
126
+ // 计算最终数据源
127
+ const finalData = computed(() => {
128
+ return Object.keys(props.data).length > 0 ? props.data : apiData.value
129
+ })
130
+
131
+ // 获取详情项配置(从插槽中获取)
132
+ const detailItems = ref([])
133
+
134
+ // 按组分组详情项
135
+ const groupedItems = computed(() => {
136
+ const groups = {}
137
+
138
+ detailItems.value.forEach(item => {
139
+ const groupName = item.group || '基本信息'
140
+ if (!groups[groupName]) {
141
+ groups[groupName] = {
142
+ groupName,
143
+ items: []
144
+ }
145
+ }
146
+ groups[groupName].items.push(item)
147
+ })
148
+
149
+ return Object.values(groups)
150
+ })
151
+
152
+ // 获取字段值
153
+ const getFieldValue = (key) => {
154
+ const value = finalData.value[key]
155
+ return value !== undefined ? value : ''
156
+ }
157
+
158
+ // 加载数据
159
+ const loadData = async () => {
160
+ if (!props.apiUrl) return
161
+
162
+ loading.value = true
163
+ error.value = ''
164
+
165
+ try {
166
+ const options = {
167
+ method: props.apiMethod,
168
+ headers: {
169
+ 'Content-Type': 'application/json',
170
+ ...props.apiHeaders
171
+ }
172
+ }
173
+
174
+ if (props.apiMethod === 'POST') {
175
+ options.body = JSON.stringify(props.apiParams)
176
+ } else {
177
+ const params = new URLSearchParams(props.apiParams)
178
+ const url = props.apiUrl + (params.toString() ? '?' + params.toString() : '')
179
+ options.url = url
180
+ }
181
+
182
+ const response = await fetch(props.apiUrl, options)
183
+
184
+ if (!response.ok) {
185
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
186
+ }
187
+
188
+ const result = await response.json()
189
+ apiData.value = result.data || result
190
+ emits('data-loaded', apiData.value)
191
+ } catch (err) {
192
+ error.value = err.message || '数据加载失败'
193
+ emits('error', err)
194
+ } finally {
195
+ loading.value = false
196
+ }
197
+ }
198
+
199
+ // 重试加载
200
+ const handleRetry = () => {
201
+ loadData()
202
+ }
203
+
204
+ // 事件处理
205
+ const handleDownloadFile = (file) => {
206
+ emits('download-file', file)
207
+ }
208
+
209
+ const handleUserClick = (user) => {
210
+ emits('user-click', user)
211
+ }
212
+
213
+ const handleLinkClick = (link) => {
214
+ emits('link-click', link)
215
+ }
216
+
217
+ // 注册详情项
218
+ const registerItem = (item) => {
219
+ detailItems.value.push(item)
220
+ }
221
+
222
+ // 注销详情项
223
+ const unregisterItem = (key) => {
224
+ const index = detailItems.value.findIndex(item => item.key === key)
225
+ if (index > -1) {
226
+ detailItems.value.splice(index, 1)
227
+ }
228
+ }
229
+
230
+ // 监听API参数变化
231
+ watch(
232
+ () => [props.apiUrl, props.apiParams, props.apiHeaders],
233
+ () => {
234
+ if (props.autoLoad && props.apiUrl) {
235
+ loadData()
236
+ }
237
+ },
238
+ { deep: true }
239
+ )
240
+
241
+ // 组件挂载时自动加载
242
+ onMounted(() => {
243
+ if (props.autoLoad && props.apiUrl) {
244
+ loadData()
245
+ }
246
+ })
247
+
248
+ // 暴露方法给父组件
249
+ defineExpose({
250
+ loadData,
251
+ registerItem,
252
+ unregisterItem
253
+ })
254
+ </script>
255
+
256
+ <style scoped>
257
+ .ebiz-detail-view {
258
+ width: 100%;
259
+ background: #ffffff;
260
+ border-radius: 6px;
261
+ padding: 24px;
262
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
263
+ }
264
+
265
+ .empty-state,
266
+ .error-state {
267
+ display: flex;
268
+ flex-direction: column;
269
+ align-items: center;
270
+ justify-content: center;
271
+ padding: 60px 20px;
272
+ color: #999999;
273
+ }
274
+
275
+ .empty-state p,
276
+ .error-state p {
277
+ margin: 16px 0;
278
+ font-size: 16px;
279
+ }
280
+
281
+ .detail-content {
282
+ width: 100%;
283
+ }
284
+
285
+ .detail-group {
286
+ margin-bottom: 32px;
287
+ }
288
+
289
+ .detail-group:last-child {
290
+ margin-bottom: 0;
291
+ }
292
+
293
+ .group-title {
294
+ display: flex;
295
+ align-items: center;
296
+ margin-bottom: 20px;
297
+ padding-bottom: 12px;
298
+ border-bottom: 1px solid #f0f0f0;
299
+ }
300
+
301
+ .group-title-bar {
302
+ width: 4px;
303
+ height: 18px;
304
+ background: #0052d9;
305
+ margin-right: 12px;
306
+ border-radius: 2px;
307
+ }
308
+
309
+ .group-title-text {
310
+ font-size: 18px;
311
+ font-weight: 600;
312
+ color: #333333;
313
+ }
314
+
315
+ .detail-fields {
316
+ display: grid;
317
+ gap: 16px;
318
+ }
319
+
320
+ .detail-fields.vertical-layout {
321
+ grid-template-columns: 1fr;
322
+ }
323
+
324
+ /* 响应式布局 */
325
+ @media (max-width: 768px) {
326
+ .ebiz-detail-view {
327
+ padding: 16px;
328
+ }
329
+
330
+ .detail-fields {
331
+ grid-template-columns: 1fr !important;
332
+ }
333
+
334
+ .group-title-text {
335
+ font-size: 16px;
336
+ }
337
+ }
338
+
339
+ @media (max-width: 480px) {
340
+ .ebiz-detail-view {
341
+ padding: 12px;
342
+ }
343
+
344
+ .detail-group {
345
+ margin-bottom: 24px;
346
+ }
347
+ }
348
+ </style>
@@ -361,7 +361,7 @@ const getOperationTypeText = (approver) => {
361
361
  const getProcessStatusTheme = (status) => {
362
362
  const statusMap = {
363
363
  'CANCELED': 'default',
364
- 'REJECT': 'danger',
364
+ 'REJECTED': 'danger',
365
365
  'APPROVED': "success",
366
366
  'ACTIVE': 'primary',
367
367
  'COMPLETED': 'success',
@@ -374,7 +374,7 @@ const getProcessStatusTheme = (status) => {
374
374
  const getProcessStatusText = (status) => {
375
375
  const statusMap = {
376
376
  'CANCELED': '已取消',
377
- 'REJECT': '已拒绝',
377
+ 'REJECTED': '已拒绝',
378
378
  'APPROVED': "已通过",
379
379
  'ACTIVE': '进行中',
380
380
  'COMPLETED': '已完成',
@@ -383,38 +383,6 @@ const getProcessStatusText = (status) => {
383
383
  return statusMap[status] || status
384
384
  }
385
385
 
386
- // 获取当前步骤索引
387
- const getCurrentStepIndex = () => {
388
- // 已完成节点数量
389
- const completedNodes = state.nodes.filter(node => node.nodeStatus === 'COMPLETED').length
390
- return completedNodes
391
- }
392
-
393
- // 获取节点步骤状态
394
- const getNodeStepStatus = (node) => {
395
- switch (node.nodeStatus) {
396
- case 'COMPLETED':
397
- return 'finish'
398
- case 'ACTIVE':
399
- return 'process'
400
- default:
401
- return 'wait'
402
- }
403
- }
404
-
405
- // 获取节点步骤样式类
406
- const getNodeStepClass = (node) => {
407
- switch (node.nodeStatus) {
408
- case 'ACTIVE':
409
- return 'current-step'
410
- case 'PENDING':
411
- case 'WAITING':
412
- return 'future-step'
413
- default:
414
- return ''
415
- }
416
- }
417
-
418
386
  // 获取节点状态主题
419
387
  const getNodeStatusTheme = (node) => {
420
388
  switch (node.nodeStatus) {
@@ -478,26 +446,6 @@ const isProcessCompleted = () => {
478
446
  return state.processInfo && state.processInfo.processStatus === 'COMPLETED'
479
447
  }
480
448
 
481
- // 获取抄送步骤状态
482
- const getCcStepStatus = () => {
483
- return isProcessCompleted() ? 'finish' : 'wait'
484
- }
485
-
486
- // 获取抄送步骤样式类
487
- const getCcStepClass = () => {
488
- return isProcessCompleted() ? '' : 'future-step'
489
- }
490
-
491
- // 获取抄送状态样式类
492
- const getCcStatusClass = () => {
493
- return isProcessCompleted() ? 'cc-status' : 'cc-pending'
494
- }
495
-
496
- // 获取抄送状态文本
497
- const getCcStatusText = () => {
498
- return isProcessCompleted() ? '已抄送' : '待抄送'
499
- }
500
-
501
449
  // 获取简单步骤图标类
502
450
  const getSimpleStepIconClass = (node) => {
503
451
  switch (node.nodeStatus) {
package/src/index.js CHANGED
@@ -96,6 +96,8 @@ import EbizMobileMeetingRoomSelector from './components/EbizMobileMeetingRoomSel
96
96
  import EbizDropdown from './components/EbizDropdown.vue'
97
97
  import EbizDropdownItem from './components/EbizDropdownItem.vue'
98
98
  import EbizFileList from './components/EbizFileList.vue'
99
+ import EbizDetailView from './components/EbizDetailView.vue'
100
+ import EbizDetailItem from './components/EbizDetailItem.vue'
99
101
 
100
102
  // 导出组件
101
103
  export {
@@ -231,5 +233,8 @@ export {
231
233
  // 新增 EbizMobileMeetingRoomSelector 组件
232
234
  EbizMobileMeetingRoomSelector,
233
235
  // 新增 EbizSApprovalProcess 组件
234
- EbizSApprovalProcess
236
+ EbizSApprovalProcess,
237
+ // 详情页组件
238
+ EbizDetailView,
239
+ EbizDetailItem
235
240
  }
@@ -382,6 +382,12 @@ const routes = [
382
382
  name: 'FileListDemo',
383
383
  component: () => import('../views/EbizFileListDemo.vue'),
384
384
  meta: { title: 'Ebiz文件列表组件示例' }
385
+ },
386
+ {
387
+ path: '/detail-view-demo',
388
+ name: 'DetailViewDemo',
389
+ component: () => import('../views/EbizDetailViewDemo.vue'),
390
+ meta: { title: 'Ebiz详情页组件示例' }
385
391
  }
386
392
  ]
387
393