@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.
- package/dist/designer-components.css +1 -1
- package/dist/index.mjs +7827 -7414
- package/package.json +1 -1
- package/src/components/EbizDetailItem.vue +442 -0
- package/src/components/EbizDetailView.md +439 -0
- package/src/components/EbizDetailView.vue +348 -0
- package/src/components/EbizSApprovalProcess.vue +2 -54
- package/src/index.js +6 -1
- package/src/router/index.js +6 -0
- package/src/views/EbizDetailViewDemo.vue +296 -0
- package/src/views/Home.vue +2 -1
|
@@ -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
|
-
'
|
|
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
|
-
'
|
|
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
|
}
|
package/src/router/index.js
CHANGED
|
@@ -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
|
|