@ebiz/designer-components 0.1.46 → 0.1.48

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.48",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,99 +1,99 @@
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" :lg="finalLg" :xl="finalXl" :xxl="finalXxl">
3
+ <div class="ebiz-detail-item" :class="{
4
+ 'vertical-layout': finalLayout === 'vertical',
5
+ 'horizontal-layout': finalLayout === 'horizontal',
5
6
  }">
6
- <!-- 字段标签 -->
7
- <div class="field-label" :style="{
8
- color: finalLabelColor,
9
- 'min-width': finalLayout === 'horizontal' ? finalLabelWidth + 'px' : 'auto'
7
+ <!-- 标签部分 -->
8
+ <div class="detail-label" :style="{
9
+ width: finalLayout === 'horizontal' ? `${finalLabelWidth}px` : 'auto',
10
+ color: finalLabelColor
10
11
  }">
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">
18
- <!-- 文本类型 -->
19
- <span v-if="type === 'text'" class="text-value">
20
- {{ displayValue }}
21
- </span>
22
-
23
- <!-- 用户类型 -->
24
- <div v-else-if="type === 'user'" class="user-value">
25
- <div class="user-list">
26
- <div v-for="user in userList" :key="user.id" class="user-item" @click="handleUserClick(user)">
27
- <div class="user-avatar">
28
- <img v-if="user.avatar" :src="user.avatar" />
29
- <div v-else class="user-avatar-default">{{ user.name?.[0] || 'U' }}</div>
30
- </div>
31
- <div class="user-info">
32
- <div class="user-name">{{ user.name }}</div>
33
- <div class="user-dept">{{ user.department }}</div>
12
+ {{ label }}
13
+ </div>
14
+
15
+ <!-- 值部分 -->
16
+ <div class="detail-value">
17
+ <slot name="default" :data="value">
18
+ <!-- 文本类型 -->
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) }}
27
+ </span>
28
+
29
+ <!-- 用户类型 -->
30
+ <div v-else-if="type === 'user'" class="user-value">
31
+ <div class="user-list">
32
+ <div v-for="user in userList" :key="user.id" class="user-item" @click="handleUserClick(user)">
33
+ <div class="user-avatar">
34
+ <img v-if="user.avatar" :src="user.avatar" />
35
+ <div v-else class="user-avatar-default">{{ user.name?.[0] || 'U' }}</div>
36
+ </div>
37
+ <div class="user-info">
38
+ <div class="user-name">{{ user.name }}</div>
39
+ <div class="user-dept">{{ user.department }}</div>
40
+ </div>
34
41
  </div>
35
42
  </div>
36
43
  </div>
37
- </div>
38
-
39
- <!-- 文件类型 -->
40
- <div v-else-if="type === 'file'" class="file-value">
41
- <EbizFileList :files="fileList" :mode="fileMode" :showDownload="showDownload"
42
- @download="handleDownloadFile" />
43
- </div>
44
-
45
- <!-- 日期类型 -->
46
- <span v-else-if="type === 'date'" class="date-value">
47
- {{ formatDate(displayValue) }}
48
- </span>
49
-
50
- <!-- 状态类型 -->
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
44
 
57
- <!-- 标签类型 -->
58
- <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 }}
45
+ <!-- 文件类型 -->
46
+ <div v-else-if="type === 'file'" class="file-value">
47
+ <EbizFileList :files="fileList" :mode="fileMode" :showDownload="showDownload"
48
+ @download="handleDownloadFile" />
49
+ </div>
50
+
51
+ <!-- 日期类型 -->
52
+ <span v-else-if="type === 'date'">{{ formatDate(value) }}</span>
53
+
54
+ <!-- 时间类型 -->
55
+ <span v-else-if="type === 'time'">{{ formatTime(value) }}</span>
56
+
57
+ <!-- 日期时间类型 -->
58
+ <span v-else-if="type === 'datetime'">{{ formatDateTime(value) }}</span>
59
+
60
+ <!-- 状态类型 -->
61
+ <t-tag v-else-if="type === 'status'" :theme="getStatusTheme(value)">
62
+ {{ getStatusText(value) }}
61
63
  </t-tag>
62
- </div>
63
64
 
64
- <!-- 链接类型 -->
65
- <div v-else-if="type === 'link'" class="link-value" @click="handleLinkClick(linkValue)">
66
- {{ linkValue?.text || linkValue?.url }}
67
- </div>
65
+ <!-- 标签类型 -->
66
+ <div v-else-if="type === 'tags'" class="tags-value">
67
+ <template v-if="Array.isArray(value) && value.length > 0">
68
+ <t-tag v-for="tag in value" :key="tag.id || tag" size="small" class="tag-item">
69
+ {{ typeof tag === 'object' ? tag.name : tag }}
70
+ </t-tag>
71
+ </template>
72
+ <span v-else>{{ value || '-' }}</span>
73
+ </div>
68
74
 
69
- <!-- 富文本类型 -->
70
- <div v-else-if="type === 'html'" class="html-value" v-html="displayValue"></div>
75
+ <!-- 链接类型 -->
76
+ <a v-else-if="type === 'link'" :href="getLinkUrl(value)" class="link-value" @click="handleLinkClick">
77
+ {{ getLinkText(value) }}
78
+ </a>
71
79
 
72
- <!-- 数字类型 -->
73
- <span v-else-if="type === 'number'" class="number-value">
74
- {{ formatNumber(displayValue) }}
75
- </span>
80
+ <!-- HTML类型 -->
81
+ <div v-else-if="type === 'html'" class="html-value" v-html="value"></div>
76
82
 
77
- <!-- 货币类型 -->
78
- <span v-else-if="type === 'currency'" class="currency-value">
79
- {{ formatCurrency(displayValue) }}
80
- </span>
83
+ <!-- 默认文本 -->
84
+ <span v-else>{{ value || '-' }}</span>
81
85
 
82
- <!-- 默认文本 -->
83
- <span v-else class="text-value">{{ displayValue }}</span>
84
- </slot>
86
+ <!-- 描述信息 -->
87
+ <div v-if="description" class="description">{{ description }}</div>
88
+ </slot>
89
+ </div>
85
90
  </div>
86
-
87
- <!-- 字段描述 -->
88
- <div v-if="description" class="field-description">
89
- {{ description }}
90
- </div>
91
-
92
- </div>
91
+ </t-col>
93
92
  </template>
94
93
 
95
94
  <script setup>
96
95
  import { computed, defineProps, defineEmits, inject } from 'vue'
96
+ import { Tag as TTag, Button as TButton, Icon as TIcon, Avatar as TAvatar, Col as TCol } from 'tdesign-vue-next'
97
97
  import EbizFileList from './EbizFileList.vue'
98
98
 
99
99
  const props = defineProps({
@@ -106,7 +106,7 @@ const props = defineProps({
106
106
  type: String,
107
107
  default: 'text',
108
108
  validator: (value) => [
109
- 'text', 'user', 'file', 'date', 'status',
109
+ 'text', 'user', 'file', 'date', 'time', 'datetime', 'status',
110
110
  'tags', 'link', 'html', 'number', 'currency'
111
111
  ].includes(value)
112
112
  },
@@ -114,15 +114,42 @@ const props = defineProps({
114
114
  type: [String, Number, Array, Object],
115
115
  default: ''
116
116
  },
117
- required: {
118
- type: Boolean,
119
- default: false
120
- },
121
117
  description: {
122
118
  type: String,
123
119
  default: ''
124
120
  },
125
121
 
122
+ // 栅格布局配置
123
+ span: {
124
+ type: Number,
125
+ default: 12,
126
+ validator: (value) => value >= 1 && value <= 24
127
+ },
128
+ xs: {
129
+ type: Number,
130
+ default: undefined
131
+ },
132
+ sm: {
133
+ type: Number,
134
+ default: undefined
135
+ },
136
+ md: {
137
+ type: Number,
138
+ default: undefined
139
+ },
140
+ lg: {
141
+ type: Number,
142
+ default: undefined
143
+ },
144
+ xl: {
145
+ type: Number,
146
+ default: undefined
147
+ },
148
+ xxl: {
149
+ type: Number,
150
+ default: undefined
151
+ },
152
+
126
153
  // 布局配置
127
154
  layout: {
128
155
  type: String,
@@ -168,6 +195,28 @@ const finalLabelWidth = computed(() => props.labelWidth || detailViewConfig?.lab
168
195
  const finalLabelColor = computed(() => props.labelColor || detailViewConfig?.labelColor || '#999999')
169
196
  const finalGap = computed(() => props.gap !== undefined ? props.gap : (detailViewConfig?.gap ?? 16))
170
197
 
198
+ // 栅格配置
199
+ const finalSpan = computed(() => props.span)
200
+ const finalXs = computed(() => props.xs)
201
+ const finalSm = computed(() => props.sm)
202
+ const finalMd = computed(() => props.md)
203
+ const finalLg = computed(() => props.lg)
204
+ const finalXl = computed(() => props.xl)
205
+ const finalXxl = computed(() => props.xxl)
206
+
207
+ // 暴露栅格配置给父组件
208
+ defineExpose({
209
+ gridConfig: computed(() => ({
210
+ span: finalSpan.value,
211
+ xs: finalXs.value,
212
+ sm: finalSm.value,
213
+ md: finalMd.value,
214
+ lg: finalLg.value,
215
+ xl: finalXl.value,
216
+ xxl: finalXxl.value
217
+ }))
218
+ })
219
+
171
220
  // 显示值
172
221
  const displayValue = computed(() => {
173
222
  return props.value !== undefined && props.value !== null ? props.value : ''
@@ -214,6 +263,31 @@ const formatDate = (date) => {
214
263
  })
215
264
  }
216
265
 
266
+ // 格式化时间
267
+ const formatTime = (time) => {
268
+ if (!time) return ''
269
+ const d = new Date(time)
270
+ return d.toLocaleTimeString('zh-CN', {
271
+ hour: '2-digit',
272
+ minute: '2-digit',
273
+ second: '2-digit'
274
+ })
275
+ }
276
+
277
+ // 格式化日期时间
278
+ const formatDateTime = (datetime) => {
279
+ if (!datetime) return ''
280
+ const d = new Date(datetime)
281
+ return d.toLocaleString('zh-CN', {
282
+ year: 'numeric',
283
+ month: '2-digit',
284
+ day: '2-digit',
285
+ hour: '2-digit',
286
+ minute: '2-digit',
287
+ second: '2-digit'
288
+ })
289
+ }
290
+
217
291
  // 格式化数字
218
292
  const formatNumber = (num) => {
219
293
  if (num === null || num === undefined || num === '') return ''
@@ -237,17 +311,65 @@ const getStatusTheme = (status) => {
237
311
  return statusMap[status] || 'default'
238
312
  }
239
313
 
314
+ // 获取状态文本
315
+ const getStatusText = (status) => {
316
+ if (typeof status === 'object' && status !== null) {
317
+ return status.text || status.status || status
318
+ }
319
+ return status
320
+ }
321
+
322
+ // 获取用户头像
323
+ const getUserAvatar = (user) => {
324
+ if (typeof user === 'object' && user !== null && user.avatar) {
325
+ return user.avatar
326
+ }
327
+ return null
328
+ }
329
+
330
+ // 获取用户初始
331
+ const getUserInitial = (user) => {
332
+ if (typeof user === 'object' && user !== null && user.name) {
333
+ return user.name.charAt(0)
334
+ }
335
+ return 'U'
336
+ }
337
+
338
+ // 获取用户名称
339
+ const getUserName = (user) => {
340
+ if (typeof user === 'object' && user !== null && user.name) {
341
+ return user.name
342
+ }
343
+ return user
344
+ }
345
+
346
+ // 获取链接 URL
347
+ const getLinkUrl = (link) => {
348
+ if (typeof link === 'object' && link !== null && link.url) {
349
+ return link.url
350
+ }
351
+ return link
352
+ }
353
+
354
+ // 获取链接文本
355
+ const getLinkText = (link) => {
356
+ if (typeof link === 'object' && link !== null && link.text) {
357
+ return link.text
358
+ }
359
+ return link
360
+ }
361
+
240
362
  // 事件处理
241
363
  const handleDownloadFile = (file) => {
242
364
  emits('download-file', file)
243
365
  }
244
366
 
245
- const handleUserClick = (user) => {
246
- emits('user-click', user)
367
+ const handleUserClick = () => {
368
+ emits('user-click', userList.value[0]) // Assuming userList is an array of users
247
369
  }
248
370
 
249
- const handleLinkClick = (link) => {
250
- emits('link-click', link)
371
+ const handleLinkClick = () => {
372
+ emits('link-click', linkValue.value)
251
373
  }
252
374
  </script>
253
375
 
@@ -263,11 +385,15 @@ const handleLinkClick = (link) => {
263
385
  border-bottom: none;
264
386
  }
265
387
 
266
- .ebiz-detail-item.vertical {
388
+ .ebiz-detail-item.vertical-layout {
267
389
  flex-direction: column;
268
390
  }
269
391
 
270
- .field-label {
392
+ .ebiz-detail-item.horizontal-layout {
393
+ flex-direction: row;
394
+ }
395
+
396
+ .detail-label {
271
397
  font-size: 14px;
272
398
  line-height: 1.5;
273
399
  font-weight: 500;
@@ -294,7 +420,7 @@ const handleLinkClick = (link) => {
294
420
  min-width: 0;
295
421
  }
296
422
 
297
- .field-description {
423
+ .description {
298
424
  font-size: 12px;
299
425
  color: #999999;
300
426
  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
  }
@@ -9,6 +9,13 @@ const props = defineProps({
9
9
  });
10
10
 
11
11
  const isShow = computed(() => {
12
+ if( !props.permissionKey ) {
13
+ return true;
14
+ }
15
+ const developMode = localStorage.getItem('ebiz-develop-mode')
16
+ if (developMode === 'designer') {
17
+ return true;
18
+ }
12
19
 
13
20
  try {
14
21
  const permissionKeysStr = localStorage.getItem('permissionKeys') || '[]';
@@ -29,6 +36,6 @@ watch(() => props.key, checkPermission, { immediate: true });
29
36
 
30
37
  <template>
31
38
  <div v-if="isShow">
32
- <slot></slot>
39
+ <slot name="default"></slot>
33
40
  </div>
34
41
  </template>