@ebiz/designer-components 0.1.46 → 0.1.47

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ebiz/designer-components",
3
- "version": "0.1.46",
3
+ "version": "0.1.47",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,25 +1,31 @@
1
1
  <template>
2
- <div class="ebiz-detail-item" :class="{ 'vertical': finalLayout === 'vertical' }" :style="{
3
- 'min-height': minHeight + 'px',
4
- 'margin-bottom': finalGap + 'px'
2
+ <t-col :span="finalSpan" :xs="finalXs" :sm="finalSm" :md="finalMd"
3
+ :lg="finalLg" :xl="finalXl" :xxl="finalXxl">
4
+ <div class="ebiz-detail-item" :class="{
5
+ 'vertical-layout': finalLayout === 'vertical',
6
+ 'horizontal-layout': finalLayout === 'horizontal',
5
7
  }">
6
- <!-- 字段标签 -->
7
- <div class="field-label" :style="{
8
- color: finalLabelColor,
9
- 'min-width': finalLayout === 'horizontal' ? finalLabelWidth + 'px' : 'auto'
8
+ <!-- 标签部分 -->
9
+ <div class="detail-label" :style="{
10
+ width: finalLayout === 'horizontal' ? `${finalLabelWidth}px` : 'auto',
11
+ color: finalLabelColor
10
12
  }">
11
- {{ label }}
12
- <span v-if="required" class="required-mark">*</span>
13
- </div>
14
-
15
- <!-- 字段值 -->
16
- <div class="field-value">
17
- <slot name="default" :data="displayValue">
13
+ {{ label }}
14
+ </div>
15
+
16
+ <!-- 值部分 -->
17
+ <div class="detail-value">
18
18
  <!-- 文本类型 -->
19
- <span v-if="type === 'text'" class="text-value">
20
- {{ displayValue }}
19
+ <span v-if="type === 'text'">{{ value || '-' }}</span>
20
+
21
+ <!-- 数字类型 -->
22
+ <span v-else-if="type === 'number'">{{ formatNumber(value) }}</span>
23
+
24
+ <!-- 货币类型 -->
25
+ <span v-else-if="type === 'currency'" class="currency-value">
26
+ {{ formatCurrency(value) }}
21
27
  </span>
22
-
28
+
23
29
  <!-- 用户类型 -->
24
30
  <div v-else-if="type === 'user'" class="user-value">
25
31
  <div class="user-list">
@@ -35,65 +41,52 @@
35
41
  </div>
36
42
  </div>
37
43
  </div>
38
-
44
+
39
45
  <!-- 文件类型 -->
40
46
  <div v-else-if="type === 'file'" class="file-value">
41
47
  <EbizFileList :files="fileList" :mode="fileMode" :showDownload="showDownload"
42
48
  @download="handleDownloadFile" />
43
49
  </div>
44
-
50
+
45
51
  <!-- 日期类型 -->
46
- <span v-else-if="type === 'date'" class="date-value">
47
- {{ formatDate(displayValue) }}
48
- </span>
49
-
52
+ <span v-else-if="type === 'date'">{{ formatDate(value) }}</span>
53
+
50
54
  <!-- 状态类型 -->
51
- <div v-else-if="type === 'status'" class="status-value">
52
- <t-tag :theme="getStatusTheme(statusValue?.status)" size="small">
53
- {{ statusValue?.text }}
54
- </t-tag>
55
- </div>
56
-
55
+ <t-tag v-else-if="type === 'status'" :theme="getStatusTheme(value)">
56
+ {{ getStatusText(value) }}
57
+ </t-tag>
58
+
57
59
  <!-- 标签类型 -->
58
60
  <div v-else-if="type === 'tags'" class="tags-value">
59
- <t-tag v-for="tag in tagList" :key="tag" size="small" variant="outline" class="tag-item">
60
- {{ tag }}
61
- </t-tag>
61
+ <template v-if="Array.isArray(value) && value.length > 0">
62
+ <t-tag v-for="tag in value" :key="tag.id || tag" size="small" class="tag-item">
63
+ {{ typeof tag === 'object' ? tag.name : tag }}
64
+ </t-tag>
65
+ </template>
66
+ <span v-else>{{ value || '-' }}</span>
62
67
  </div>
63
-
68
+
64
69
  <!-- 链接类型 -->
65
- <div v-else-if="type === 'link'" class="link-value" @click="handleLinkClick(linkValue)">
66
- {{ linkValue?.text || linkValue?.url }}
67
- </div>
68
-
69
- <!-- 富文本类型 -->
70
- <div v-else-if="type === 'html'" class="html-value" v-html="displayValue"></div>
71
-
72
- <!-- 数字类型 -->
73
- <span v-else-if="type === 'number'" class="number-value">
74
- {{ formatNumber(displayValue) }}
75
- </span>
76
-
77
- <!-- 货币类型 -->
78
- <span v-else-if="type === 'currency'" class="currency-value">
79
- {{ formatCurrency(displayValue) }}
80
- </span>
81
-
70
+ <a v-else-if="type === 'link'" :href="getLinkUrl(value)" class="link-value" @click="handleLinkClick">
71
+ {{ getLinkText(value) }}
72
+ </a>
73
+
74
+ <!-- HTML类型 -->
75
+ <div v-else-if="type === 'html'" class="html-value" v-html="value"></div>
76
+
82
77
  <!-- 默认文本 -->
83
- <span v-else class="text-value">{{ displayValue }}</span>
84
- </slot>
85
- </div>
86
-
87
- <!-- 字段描述 -->
88
- <div v-if="description" class="field-description">
89
- {{ description }}
78
+ <span v-else>{{ value || '-' }}</span>
79
+
80
+ <!-- 描述信息 -->
81
+ <div v-if="description" class="description">{{ description }}</div>
82
+ </div>
90
83
  </div>
91
-
92
- </div>
84
+ </t-col>
93
85
  </template>
94
86
 
95
87
  <script setup>
96
88
  import { computed, defineProps, defineEmits, inject } from 'vue'
89
+ import { Tag as TTag, Button as TButton, Icon as TIcon, Avatar as TAvatar, Col as TCol } from 'tdesign-vue-next'
97
90
  import EbizFileList from './EbizFileList.vue'
98
91
 
99
92
  const props = defineProps({
@@ -114,15 +107,42 @@ const props = defineProps({
114
107
  type: [String, Number, Array, Object],
115
108
  default: ''
116
109
  },
117
- required: {
118
- type: Boolean,
119
- default: false
120
- },
121
110
  description: {
122
111
  type: String,
123
112
  default: ''
124
113
  },
125
114
 
115
+ // 栅格布局配置
116
+ span: {
117
+ type: Number,
118
+ default: 12,
119
+ validator: (value) => value >= 1 && value <= 24
120
+ },
121
+ xs: {
122
+ type: Number,
123
+ default: undefined
124
+ },
125
+ sm: {
126
+ type: Number,
127
+ default: undefined
128
+ },
129
+ md: {
130
+ type: Number,
131
+ default: undefined
132
+ },
133
+ lg: {
134
+ type: Number,
135
+ default: undefined
136
+ },
137
+ xl: {
138
+ type: Number,
139
+ default: undefined
140
+ },
141
+ xxl: {
142
+ type: Number,
143
+ default: undefined
144
+ },
145
+
126
146
  // 布局配置
127
147
  layout: {
128
148
  type: String,
@@ -168,6 +188,28 @@ const finalLabelWidth = computed(() => props.labelWidth || detailViewConfig?.lab
168
188
  const finalLabelColor = computed(() => props.labelColor || detailViewConfig?.labelColor || '#999999')
169
189
  const finalGap = computed(() => props.gap !== undefined ? props.gap : (detailViewConfig?.gap ?? 16))
170
190
 
191
+ // 栅格配置
192
+ const finalSpan = computed(() => props.span)
193
+ const finalXs = computed(() => props.xs)
194
+ const finalSm = computed(() => props.sm)
195
+ const finalMd = computed(() => props.md)
196
+ const finalLg = computed(() => props.lg)
197
+ const finalXl = computed(() => props.xl)
198
+ const finalXxl = computed(() => props.xxl)
199
+
200
+ // 暴露栅格配置给父组件
201
+ defineExpose({
202
+ gridConfig: computed(() => ({
203
+ span: finalSpan.value,
204
+ xs: finalXs.value,
205
+ sm: finalSm.value,
206
+ md: finalMd.value,
207
+ lg: finalLg.value,
208
+ xl: finalXl.value,
209
+ xxl: finalXxl.value
210
+ }))
211
+ })
212
+
171
213
  // 显示值
172
214
  const displayValue = computed(() => {
173
215
  return props.value !== undefined && props.value !== null ? props.value : ''
@@ -237,17 +279,65 @@ const getStatusTheme = (status) => {
237
279
  return statusMap[status] || 'default'
238
280
  }
239
281
 
282
+ // 获取状态文本
283
+ const getStatusText = (status) => {
284
+ if (typeof status === 'object' && status !== null) {
285
+ return status.text || status.status || status
286
+ }
287
+ return status
288
+ }
289
+
290
+ // 获取用户头像
291
+ const getUserAvatar = (user) => {
292
+ if (typeof user === 'object' && user !== null && user.avatar) {
293
+ return user.avatar
294
+ }
295
+ return null
296
+ }
297
+
298
+ // 获取用户初始
299
+ const getUserInitial = (user) => {
300
+ if (typeof user === 'object' && user !== null && user.name) {
301
+ return user.name.charAt(0)
302
+ }
303
+ return 'U'
304
+ }
305
+
306
+ // 获取用户名称
307
+ const getUserName = (user) => {
308
+ if (typeof user === 'object' && user !== null && user.name) {
309
+ return user.name
310
+ }
311
+ return user
312
+ }
313
+
314
+ // 获取链接 URL
315
+ const getLinkUrl = (link) => {
316
+ if (typeof link === 'object' && link !== null && link.url) {
317
+ return link.url
318
+ }
319
+ return link
320
+ }
321
+
322
+ // 获取链接文本
323
+ const getLinkText = (link) => {
324
+ if (typeof link === 'object' && link !== null && link.text) {
325
+ return link.text
326
+ }
327
+ return link
328
+ }
329
+
240
330
  // 事件处理
241
331
  const handleDownloadFile = (file) => {
242
332
  emits('download-file', file)
243
333
  }
244
334
 
245
- const handleUserClick = (user) => {
246
- emits('user-click', user)
335
+ const handleUserClick = () => {
336
+ emits('user-click', userList.value[0]) // Assuming userList is an array of users
247
337
  }
248
338
 
249
- const handleLinkClick = (link) => {
250
- emits('link-click', link)
339
+ const handleLinkClick = () => {
340
+ emits('link-click', linkValue.value)
251
341
  }
252
342
  </script>
253
343
 
@@ -263,11 +353,15 @@ const handleLinkClick = (link) => {
263
353
  border-bottom: none;
264
354
  }
265
355
 
266
- .ebiz-detail-item.vertical {
356
+ .ebiz-detail-item.vertical-layout {
267
357
  flex-direction: column;
268
358
  }
269
359
 
270
- .field-label {
360
+ .ebiz-detail-item.horizontal-layout {
361
+ flex-direction: row;
362
+ }
363
+
364
+ .detail-label {
271
365
  font-size: 14px;
272
366
  line-height: 1.5;
273
367
  font-weight: 500;
@@ -294,7 +388,7 @@ const handleLinkClick = (link) => {
294
388
  min-width: 0;
295
389
  }
296
390
 
297
- .field-description {
391
+ .description {
298
392
  font-size: 12px;
299
393
  color: #999999;
300
394
  margin-top: 4px;
@@ -2,18 +2,29 @@
2
2
  <div class="ebiz-detail-view" v-loading="loading">
3
3
  <!-- 正常内容 -->
4
4
  <div class="detail-content">
5
- <div class="detail-fields" :class="{ 'vertical-layout': layout === 'vertical' }" :style="{
6
- 'grid-template-columns': `repeat(${columns}, 1fr)`,
7
- gap: `${gap}px`
8
- }">
9
- <slot name="default" :data="finalData">
5
+ <t-row :gutter="gap" class="detail-fields">
6
+ <slot name="default" :data="finalData" :wrapItem="wrapItem">
10
7
  <!-- 默认内容:根据fields配置或自动生成 -->
11
8
  <template v-if="fields.length > 0">
12
9
  <template v-for="field in fields" :key="field.key">
13
- <slot :name="'detail-item-' + field.key" :data="finalData">
14
- <EbizDetailItem :label="field.label" :value="getFieldValue(field.key)" :type="field.type || 'text'"
15
- :required="field.required" :description="field.description" :fileMode="field.fileMode"
16
- :showDownload="field.showDownload" @download-file="handleDownloadFile" @user-click="handleUserClick"
10
+ <slot :name="'detail-item-' + field.key" :data="finalData" :field="field" :wrapItem="wrapItem">
11
+ <EbizDetailItem
12
+ :label="field.label"
13
+ :value="getFieldValue(field.key)"
14
+ :type="field.type || 'text'"
15
+ :required="field.required"
16
+ :description="field.description"
17
+ :fileMode="field.fileMode"
18
+ :showDownload="field.showDownload"
19
+ :span="getFieldSpan(field)"
20
+ :xs="getFieldResponsiveSpan(field, 'xs')"
21
+ :sm="getFieldResponsiveSpan(field, 'sm')"
22
+ :md="getFieldResponsiveSpan(field, 'md')"
23
+ :lg="getFieldResponsiveSpan(field, 'lg')"
24
+ :xl="getFieldResponsiveSpan(field, 'xl')"
25
+ :xxl="getFieldResponsiveSpan(field, 'xxl')"
26
+ @download-file="handleDownloadFile"
27
+ @user-click="handleUserClick"
17
28
  @link-click="handleLinkClick" />
18
29
  </slot>
19
30
  </template>
@@ -21,16 +32,22 @@
21
32
 
22
33
  <!-- 自动生成字段 -->
23
34
  <template v-else>
24
- <EbizDetailItem v-for="(value, key) in finalData" :key="key" :label="key" :value="value" />
35
+ <EbizDetailItem
36
+ v-for="(value, key) in finalData"
37
+ :key="key"
38
+ :label="key"
39
+ :value="value"
40
+ :span="getDefaultSpan()" />
25
41
  </template>
26
42
  </slot>
27
- </div>
43
+ </t-row>
28
44
  </div>
29
45
  </div>
30
46
  </template>
31
47
 
32
48
  <script setup>
33
49
  import { computed, defineProps, defineEmits, ref, watch, onMounted, provide } from 'vue'
50
+ import { Row as TRow } from 'tdesign-vue-next'
34
51
  import EbizDetailItem from './EbizDetailItem.vue'
35
52
  import { dataService } from '../index'
36
53
 
@@ -89,6 +106,19 @@ const props = defineProps({
89
106
  default: '#666666'
90
107
  },
91
108
 
109
+ // 响应式配置
110
+ responsive: {
111
+ type: Object,
112
+ default: () => ({
113
+ xs: 1, // <576px 单列
114
+ sm: 1, // ≥576px 单列
115
+ md: 2, // ≥768px 双列
116
+ lg: 2, // ≥992px 双列
117
+ xl: 3, // ≥1200px 三列
118
+ xxl: 4 // ≥1400px 四列
119
+ })
120
+ },
121
+
92
122
  // 自动加载
93
123
  autoLoad: {
94
124
  type: Boolean,
@@ -103,7 +133,6 @@ const loading = ref(false)
103
133
  const error = ref('')
104
134
  const apiData = ref({})
105
135
 
106
-
107
136
  // 计算最终数据源
108
137
  const finalData = computed(() => {
109
138
  return Object.keys(props.data).length > 0 ? props.data : apiData.value
@@ -118,6 +147,47 @@ const getFieldValue = (key) => {
118
147
  return value !== undefined ? value : ''
119
148
  }
120
149
 
150
+ // 计算字段在24栅格中的span值
151
+ const getFieldSpan = (field) => {
152
+ if (props.layout === 'vertical') {
153
+ return 24 // 垂直布局时占满整行
154
+ }
155
+
156
+ const span = field.span || 1
157
+ const maxSpan = Math.min(span, props.columns)
158
+ return Math.floor(24 / props.columns) * maxSpan
159
+ }
160
+
161
+ // 计算字段在不同断点下的span值
162
+ const getFieldResponsiveSpan = (field, breakpoint) => {
163
+ if (props.layout === 'vertical') {
164
+ return 24
165
+ }
166
+
167
+ // 获取该断点下的列数
168
+ const columnsAtBreakpoint = props.responsive[breakpoint] || props.columns
169
+ const span = field.span || 1
170
+ const maxSpan = Math.min(span, columnsAtBreakpoint)
171
+
172
+ return Math.floor(24 / columnsAtBreakpoint) * maxSpan
173
+ }
174
+
175
+ // 获取默认span值(用于自动生成字段)
176
+ const getDefaultSpan = () => {
177
+ if (props.layout === 'vertical') {
178
+ return 24
179
+ }
180
+ return Math.floor(24 / props.columns)
181
+ }
182
+
183
+ // 包装函数,用于在slot中直接返回EbizDetailItem(因为t-col已在组件内部)
184
+ const wrapItem = (detailItemVnode, spanConfig = {}) => {
185
+ // 现在EbizDetailItem内部已包含t-col,直接返回vnode即可
186
+ return detailItemVnode
187
+ }
188
+
189
+
190
+
121
191
  // 加载数据
122
192
  const loadData = async () => {
123
193
  if (!props.apiConfig && !props.fetchUrl) return
@@ -144,11 +214,6 @@ const loadData = async () => {
144
214
  }
145
215
  }
146
216
 
147
- // 重试加载
148
- const handleRetry = () => {
149
- loadData()
150
- }
151
-
152
217
  // 事件处理
153
218
  const handleDownloadFile = (file) => {
154
219
  emits('download-file', file)
@@ -191,7 +256,7 @@ provide('detailViewConfig', {
191
256
  layout: props.layout,
192
257
  labelWidth: props.labelWidth,
193
258
  labelColor: props.labelColor,
194
- gap: 0 // 子组件不需要自己的gap,由父组件的网格布局控制
259
+ gap: 0 // 由t-row的gutter统一管理间距
195
260
  })
196
261
 
197
262
  // 组件挂载时自动加载
@@ -268,29 +333,12 @@ defineExpose({
268
333
  color: #333333;
269
334
  }
270
335
 
271
- .detail-fields {
272
- display: grid;
273
- gap: 16px;
274
- }
275
-
276
- .detail-fields.vertical-layout {
277
- grid-template-columns: 1fr;
278
- }
279
-
280
- .group-title {
281
- grid-column: 1 / -1;
282
- }
283
-
284
- /* 响应式布局 */
336
+ /* 移动端优化已经通过TDesign的响应式断点自动处理 */
285
337
  @media (max-width: 768px) {
286
338
  .ebiz-detail-view {
287
339
  padding: 16px;
288
340
  }
289
341
 
290
- .detail-fields {
291
- grid-template-columns: 1fr !important;
292
- }
293
-
294
342
  .group-title-text {
295
343
  font-size: 16px;
296
344
  }