@lucifer.chao.du/home-schema-components 0.1.0 → 0.1.1
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 +1 -1
- package/src/web-preview/activities.scss +70 -0
- package/src/web-preview/activities.vue +106 -0
- package/src/web-preview/communities.scss +82 -0
- package/src/web-preview/communities.vue +125 -0
- package/src/web-preview/index.ts +5 -6
- package/src/web-preview/services.scss +237 -0
- package/src/web-preview/services.vue +242 -0
- package/src/web-preview/tags.scss +70 -0
- package/src/web-preview/{TagsPreview.vue → tags.vue} +24 -101
- package/src/web-preview/tools.scss +76 -0
- package/src/web-preview/tools.vue +126 -0
- package/src/web-preview/ActivitiesPreview.vue +0 -120
- package/src/web-preview/CommunitiesPreview.vue +0 -74
- package/src/web-preview/HomeSchemaPreview.vue +0 -103
- package/src/web-preview/ServicesPreview.vue +0 -74
- package/src/web-preview/ToolsPreview.vue +0 -49
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { DiscountItem, ServiceItem } from '../schema/index.js';
|
|
3
|
+
|
|
4
|
+
import { computed } from 'vue';
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(
|
|
7
|
+
defineProps<{
|
|
8
|
+
locationText?: string;
|
|
9
|
+
options?: ServiceItem[];
|
|
10
|
+
showHead?: boolean;
|
|
11
|
+
}>(),
|
|
12
|
+
{
|
|
13
|
+
locationText: '',
|
|
14
|
+
options: () => [],
|
|
15
|
+
showHead: false,
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const displayList = computed<ServiceItem[]>(() => {
|
|
20
|
+
if (props.options.length > 0) {
|
|
21
|
+
return props.options;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
category: '',
|
|
27
|
+
count: '',
|
|
28
|
+
discount: [],
|
|
29
|
+
distance: '',
|
|
30
|
+
image: '',
|
|
31
|
+
path: undefined as never,
|
|
32
|
+
range: '',
|
|
33
|
+
rate: '',
|
|
34
|
+
title: '',
|
|
35
|
+
type: 'home',
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function resolveServiceTypeOption(type: string) {
|
|
41
|
+
switch (type) {
|
|
42
|
+
case 'home':
|
|
43
|
+
return {
|
|
44
|
+
backgroundColor: 'var(--custom-color-base, #0f766e)',
|
|
45
|
+
color: '#ffffff',
|
|
46
|
+
text: '上门',
|
|
47
|
+
};
|
|
48
|
+
case 'site':
|
|
49
|
+
return {
|
|
50
|
+
backgroundColor: '#f97316',
|
|
51
|
+
color: '#ffffff',
|
|
52
|
+
text: '到店',
|
|
53
|
+
};
|
|
54
|
+
case 'online':
|
|
55
|
+
return {
|
|
56
|
+
backgroundColor: '#eab308',
|
|
57
|
+
color: '#111827',
|
|
58
|
+
text: '在线',
|
|
59
|
+
};
|
|
60
|
+
case 'self':
|
|
61
|
+
return {
|
|
62
|
+
backgroundColor: '#eab308',
|
|
63
|
+
color: '#111827',
|
|
64
|
+
text: '自提',
|
|
65
|
+
};
|
|
66
|
+
default:
|
|
67
|
+
return {
|
|
68
|
+
backgroundColor: '#cbd5e1',
|
|
69
|
+
color: '#334155',
|
|
70
|
+
text: '服务',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveDiscountTypeOption(type: string) {
|
|
76
|
+
switch (type) {
|
|
77
|
+
case 'voucher':
|
|
78
|
+
return { name: '优惠券', shortName: '券' };
|
|
79
|
+
case 'bonus':
|
|
80
|
+
return { name: '神券', shortName: '券' };
|
|
81
|
+
case 'group':
|
|
82
|
+
return { name: '团购', shortName: '团' };
|
|
83
|
+
case 'discount':
|
|
84
|
+
default:
|
|
85
|
+
return { name: '优惠', shortName: '惠' };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveDiscountTypeShortName(type: string) {
|
|
90
|
+
return resolveDiscountTypeOption(type).shortName;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolveDiscountTypeName(type: string) {
|
|
94
|
+
return resolveDiscountTypeOption(type).name;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveRateText(rate: string) {
|
|
98
|
+
return rate ? `★ ${rate}` : '★ 暂无评分';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function shouldShowDiscountPrice(discount: DiscountItem) {
|
|
102
|
+
return Number(discount.price || 0) > 0 || Number(discount.discounted || 0) > 0;
|
|
103
|
+
}
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<template>
|
|
107
|
+
<div class="service-list" v-if="displayList.length > 0">
|
|
108
|
+
<div class="service-list__head" v-if="showHead">
|
|
109
|
+
<div class="service-list__head-title">
|
|
110
|
+
<div class="service-list__head-line"></div>
|
|
111
|
+
<span class="service-list__head-text">{{ locationText }}生活圈</span>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="service-list__head-action">
|
|
114
|
+
<span class="service-list__head-action-text">申请加入</span>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="service-list__body">
|
|
118
|
+
<div
|
|
119
|
+
v-for="(item, index) in displayList"
|
|
120
|
+
:key="`services-${index}`"
|
|
121
|
+
class="service-list__item"
|
|
122
|
+
>
|
|
123
|
+
<div class="service-list__media">
|
|
124
|
+
<img
|
|
125
|
+
v-if="item.image"
|
|
126
|
+
class="image"
|
|
127
|
+
:src="item.image"
|
|
128
|
+
:alt="item.title || `服务 ${index + 1}`"
|
|
129
|
+
/>
|
|
130
|
+
<div v-else class="image image--placeholder">服务</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="service-list__content">
|
|
133
|
+
<div class="service-list__row">
|
|
134
|
+
<div class="service-list__row-item service-list__row-item--grow">
|
|
135
|
+
<span class="service-list__title-text">
|
|
136
|
+
{{ item.title || `服务 ${index + 1}` }}
|
|
137
|
+
</span>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="service-list__row-item">
|
|
140
|
+
<div
|
|
141
|
+
class="service-list__type-badge"
|
|
142
|
+
:style="{
|
|
143
|
+
backgroundColor: resolveServiceTypeOption(item.type).backgroundColor,
|
|
144
|
+
}"
|
|
145
|
+
>
|
|
146
|
+
<span
|
|
147
|
+
class="service-list__type-text"
|
|
148
|
+
:style="{ color: resolveServiceTypeOption(item.type).color }"
|
|
149
|
+
>
|
|
150
|
+
{{ resolveServiceTypeOption(item.type).text }}
|
|
151
|
+
</span>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="service-list__row">
|
|
156
|
+
<div class="service-list__row-item service-list__row-item--rates">
|
|
157
|
+
<span class="service-list__rate">{{ resolveRateText(item.rate) }}</span>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="service-list__row-item service-list__row-item--count">
|
|
160
|
+
<span class="service-list__meta-text">
|
|
161
|
+
{{ item.count ? `${item.count}条` : '暂无评价' }}
|
|
162
|
+
</span>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
<div class="service-list__row">
|
|
166
|
+
<div class="service-list__row-item service-list__row-item--grow">
|
|
167
|
+
<span class="service-list__meta-text">
|
|
168
|
+
{{ item.category || '服务分类' }}
|
|
169
|
+
</span>
|
|
170
|
+
<template v-if="item.range">
|
|
171
|
+
<span class="service-list__separator"></span>
|
|
172
|
+
<span class="service-list__meta-text">{{ item.range }}</span>
|
|
173
|
+
</template>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="service-list__row-item">
|
|
176
|
+
<span class="service-list__distance-icon">📍</span>
|
|
177
|
+
<span class="service-list__distance-text">
|
|
178
|
+
{{ item.distance || '距离待配置' }}
|
|
179
|
+
</span>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div
|
|
183
|
+
class="service-list__discount"
|
|
184
|
+
v-if="Array.isArray(item.discount) && item.discount.length > 0"
|
|
185
|
+
>
|
|
186
|
+
<div
|
|
187
|
+
v-for="(discount, discountIndex) in item.discount"
|
|
188
|
+
:key="`services-${index}-${discountIndex}`"
|
|
189
|
+
class="service-list__discount-row"
|
|
190
|
+
:class="`service-list__discount-row--${discount.type}`"
|
|
191
|
+
>
|
|
192
|
+
<div
|
|
193
|
+
class="service-list__discount-type"
|
|
194
|
+
:style="{ marginRight: discount.price ? '5px' : '8px' }"
|
|
195
|
+
>
|
|
196
|
+
<span class="service-list__discount-type-text">
|
|
197
|
+
{{ resolveDiscountTypeShortName(discount.type) }}
|
|
198
|
+
</span>
|
|
199
|
+
</div>
|
|
200
|
+
<div class="service-list__discount-name" v-if="discount.type === 'bonus'">
|
|
201
|
+
<span class="service-list__discount-name-text">
|
|
202
|
+
{{ resolveDiscountTypeName(discount.type) }}
|
|
203
|
+
</span>
|
|
204
|
+
</div>
|
|
205
|
+
<div
|
|
206
|
+
class="service-list__discount-price"
|
|
207
|
+
v-if="shouldShowDiscountPrice(discount)"
|
|
208
|
+
>
|
|
209
|
+
<div
|
|
210
|
+
class="service-list__discount-final-price"
|
|
211
|
+
v-if="Number(discount.price || 0) > 0"
|
|
212
|
+
>
|
|
213
|
+
<span class="service-list__discount-cny">¥</span>
|
|
214
|
+
<span class="service-list__discount-final-price-text">
|
|
215
|
+
{{ discount.price }}
|
|
216
|
+
</span>
|
|
217
|
+
</div>
|
|
218
|
+
<div
|
|
219
|
+
class="service-list__discount-text"
|
|
220
|
+
v-if="Number(discount.discounted || 0) > 0"
|
|
221
|
+
>
|
|
222
|
+
<span class="service-list__discount-text-content">
|
|
223
|
+
已减{{ discount.discounted }}
|
|
224
|
+
</span>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="service-list__discount-project">
|
|
228
|
+
<span class="service-list__discount-project-text">
|
|
229
|
+
{{ discount.project || resolveDiscountTypeName(discount.type) }}
|
|
230
|
+
</span>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</template>
|
|
239
|
+
|
|
240
|
+
<style scoped lang="scss">
|
|
241
|
+
@import './services.scss';
|
|
242
|
+
</style>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
.tag-list {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: row;
|
|
4
|
+
justify-content: space-between;
|
|
5
|
+
flex-wrap: wrap;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.placeholder {
|
|
9
|
+
flex: 1 0 auto;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.placeholder-tag {
|
|
13
|
+
border: 0;
|
|
14
|
+
cursor: pointer;
|
|
15
|
+
padding: 6px 8px;
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: row;
|
|
18
|
+
align-items: center;
|
|
19
|
+
background-color: var(--uni-bg-color, #ffffff);
|
|
20
|
+
border-radius: 5px;
|
|
21
|
+
box-shadow: 0 0 5px rgb(15 23 42 / 10%);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.placeholder-tag .text {
|
|
25
|
+
font-size: 13px;
|
|
26
|
+
color: var(--uni-text-color-gray, #64748b);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.placeholder-tag .icon {
|
|
30
|
+
margin-left: 4px;
|
|
31
|
+
color: var(--custom-color-base, #0f766e);
|
|
32
|
+
font-size: 14px;
|
|
33
|
+
line-height: 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.tag-item {
|
|
37
|
+
margin-bottom: 15px;
|
|
38
|
+
padding: 6px 8px;
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: row;
|
|
41
|
+
align-items: center;
|
|
42
|
+
background-color: var(--uni-bg-color, #ffffff);
|
|
43
|
+
border-radius: 5px;
|
|
44
|
+
box-shadow: 0 0 5px rgb(15 23 42 / 10%);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.tag-item.no-margin-bottom {
|
|
48
|
+
margin-bottom: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.tag-item .image {
|
|
52
|
+
display: inline-flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
justify-content: center;
|
|
55
|
+
margin-right: 6px;
|
|
56
|
+
width: 20px;
|
|
57
|
+
height: 15px;
|
|
58
|
+
border-radius: 999px;
|
|
59
|
+
background: rgb(15 118 110 / 12%);
|
|
60
|
+
color: var(--custom-color-base, #0f766e);
|
|
61
|
+
font-size: 10px;
|
|
62
|
+
font-weight: 700;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.tag-item .text {
|
|
66
|
+
font-size: 13px;
|
|
67
|
+
line-height: 1.4;
|
|
68
|
+
color: var(--uni-text-color, #0f172a);
|
|
69
|
+
word-break: break-all;
|
|
70
|
+
}
|
|
@@ -14,16 +14,16 @@ import {
|
|
|
14
14
|
|
|
15
15
|
import { getTextInitial } from '../schema/index.js';
|
|
16
16
|
|
|
17
|
-
type DisplayLastRowData = {
|
|
18
|
-
displayLastRowItemGap: number;
|
|
19
|
-
placeholderWidth: number;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
17
|
type DisplayTagData = {
|
|
23
18
|
displayTagCount: number;
|
|
24
19
|
showPlaceholderTag: boolean;
|
|
25
20
|
};
|
|
26
21
|
|
|
22
|
+
type DisplayLastRowData = {
|
|
23
|
+
displayLastRowItemGap: number;
|
|
24
|
+
placeholderWidth: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
27
|
type TagLayoutSnapshot = {
|
|
28
28
|
placeholderTagWidth: number;
|
|
29
29
|
signature: string;
|
|
@@ -105,13 +105,18 @@ const displayTagData = computed<DisplayTagData>(() => {
|
|
|
105
105
|
|
|
106
106
|
const showPlaceholderTag = computed(() => displayTagData.value.showPlaceholderTag);
|
|
107
107
|
const displayTagCount = computed(() => displayTagData.value.displayTagCount);
|
|
108
|
+
const displayLastRowStartIndex = computed(() =>
|
|
109
|
+
Math.max(displayTagCount.value - displayLastRowCount.value, 0),
|
|
110
|
+
);
|
|
108
111
|
|
|
109
112
|
const displayLastRowCount = computed(() => {
|
|
110
113
|
let remainingTagCount = displayTagCount.value;
|
|
114
|
+
|
|
111
115
|
for (const rowCount of tagListRow.value) {
|
|
112
116
|
if (remainingTagCount <= 0) {
|
|
113
117
|
break;
|
|
114
118
|
}
|
|
119
|
+
|
|
115
120
|
if (remainingTagCount > rowCount) {
|
|
116
121
|
remainingTagCount -= rowCount;
|
|
117
122
|
} else {
|
|
@@ -121,10 +126,6 @@ const displayLastRowCount = computed(() => {
|
|
|
121
126
|
return 0;
|
|
122
127
|
});
|
|
123
128
|
|
|
124
|
-
const displayLastRowStartIndex = computed(() =>
|
|
125
|
-
Math.max(displayTagCount.value - displayLastRowCount.value, 0),
|
|
126
|
-
);
|
|
127
|
-
|
|
128
129
|
const displayLastRowData = computed<DisplayLastRowData>(() => {
|
|
129
130
|
if (displayLastRowCount.value === 0 || displayLastRowCount.value === 3) {
|
|
130
131
|
return {
|
|
@@ -379,17 +380,13 @@ onBeforeUnmount(() => {
|
|
|
379
380
|
</script>
|
|
380
381
|
|
|
381
382
|
<template>
|
|
382
|
-
<div ref="containerRef" class="
|
|
383
|
+
<div ref="containerRef" class="tag-list">
|
|
383
384
|
<template v-for="(tag, index) in tagList" :key="`tags-${index}`">
|
|
384
|
-
<
|
|
385
|
+
<div
|
|
385
386
|
v-show="showAllTagsForMeasurement || index < displayTagCount"
|
|
386
387
|
:ref="(element) => setTagItemRef(element, index)"
|
|
387
|
-
|
|
388
|
-
class="
|
|
389
|
-
:class="{
|
|
390
|
-
'home-schema-tags__item--no-margin-bottom':
|
|
391
|
-
!isMeasurePending && isDisplayLastRowTag(index),
|
|
392
|
-
}"
|
|
388
|
+
class="tag-item"
|
|
389
|
+
:class="{ 'no-margin-bottom': !isMeasurePending && isDisplayLastRowTag(index) }"
|
|
393
390
|
:style="{
|
|
394
391
|
marginRight:
|
|
395
392
|
isMeasurePending || index < displayLastRowStartIndex
|
|
@@ -397,111 +394,37 @@ onBeforeUnmount(() => {
|
|
|
397
394
|
: `${displayLastRowItemGap}px`,
|
|
398
395
|
}"
|
|
399
396
|
>
|
|
400
|
-
<span
|
|
401
|
-
v-if="props.mode.showIcon"
|
|
402
|
-
class="home-schema-tags__icon"
|
|
403
|
-
>
|
|
397
|
+
<span v-if="props.mode.showIcon" class="image">
|
|
404
398
|
{{ getTextInitial(tag.title) }}
|
|
405
399
|
</span>
|
|
406
|
-
<span class="
|
|
407
|
-
|
|
408
|
-
</span>
|
|
409
|
-
</button>
|
|
400
|
+
<span class="text">{{ tag.title || `标签 ${index + 1}` }}</span>
|
|
401
|
+
</div>
|
|
410
402
|
</template>
|
|
411
|
-
|
|
412
403
|
<template v-if="props.options.length > 0">
|
|
413
404
|
<span
|
|
414
405
|
v-show="!isMeasurePending && showPlaceholder"
|
|
415
|
-
class="
|
|
406
|
+
class="placeholder"
|
|
416
407
|
:style="{ flexBasis: `${placeholderWidthValue}px` }"
|
|
417
408
|
></span>
|
|
418
409
|
<button
|
|
419
410
|
v-show="isMeasurePending || showPlaceholderTag"
|
|
420
411
|
ref="placeholderTagRef"
|
|
421
412
|
type="button"
|
|
422
|
-
class="
|
|
413
|
+
class="placeholder-tag"
|
|
423
414
|
:style="{
|
|
424
|
-
marginTop: displayLastRowCount === 3 ? '
|
|
415
|
+
marginTop: displayLastRowCount === 3 ? '15px' : '0px',
|
|
425
416
|
pointerEvents: isMeasurePending ? 'none' : 'auto',
|
|
426
417
|
visibility: isMeasurePending ? 'hidden' : 'visible',
|
|
427
418
|
}"
|
|
428
419
|
@click="toggleExpanded"
|
|
429
420
|
>
|
|
430
|
-
<span>{{ tagListExpanded ? '收起' : '展开' }}</span>
|
|
431
|
-
<span class="
|
|
432
|
-
{{ tagListExpanded ? '↑' : '↓' }}
|
|
433
|
-
</span>
|
|
421
|
+
<span class="text">{{ tagListExpanded ? '收起' : '展开' }}</span>
|
|
422
|
+
<span class="icon">{{ tagListExpanded ? '↑' : '↓' }}</span>
|
|
434
423
|
</button>
|
|
435
424
|
</template>
|
|
436
425
|
</div>
|
|
437
426
|
</template>
|
|
438
427
|
|
|
439
|
-
<style scoped>
|
|
440
|
-
.
|
|
441
|
-
display: flex;
|
|
442
|
-
flex-wrap: wrap;
|
|
443
|
-
justify-content: space-between;
|
|
444
|
-
gap: 0 0;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
.home-schema-tags__item,
|
|
448
|
-
.home-schema-tags__placeholder-tag {
|
|
449
|
-
border: 0;
|
|
450
|
-
cursor: default;
|
|
451
|
-
display: inline-flex;
|
|
452
|
-
align-items: center;
|
|
453
|
-
border-radius: 10px;
|
|
454
|
-
background: rgb(255 255 255 / 85%);
|
|
455
|
-
box-shadow: 0 0 10px rgb(15 23 42 / 10%);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
.home-schema-tags__item {
|
|
459
|
-
margin-bottom: 12px;
|
|
460
|
-
padding: 8px 10px;
|
|
461
|
-
background: rgb(248 250 252 / 90%);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
.home-schema-tags__item--no-margin-bottom {
|
|
465
|
-
margin-bottom: 0;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
.home-schema-tags__icon {
|
|
469
|
-
display: inline-flex;
|
|
470
|
-
align-items: center;
|
|
471
|
-
justify-content: center;
|
|
472
|
-
width: 18px;
|
|
473
|
-
height: 18px;
|
|
474
|
-
margin-right: 8px;
|
|
475
|
-
border-radius: 999px;
|
|
476
|
-
background: hsl(var(--primary) / 12%);
|
|
477
|
-
color: hsl(var(--primary));
|
|
478
|
-
font-size: 10px;
|
|
479
|
-
font-weight: 600;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
.home-schema-tags__text {
|
|
483
|
-
min-width: 0;
|
|
484
|
-
color: hsl(var(--foreground));
|
|
485
|
-
font-size: 12px;
|
|
486
|
-
line-height: 1.4;
|
|
487
|
-
white-space: normal;
|
|
488
|
-
word-break: break-all;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
.home-schema-tags__placeholder {
|
|
492
|
-
flex: 1 0 auto;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
.home-schema-tags__placeholder-tag {
|
|
496
|
-
padding: 8px 10px;
|
|
497
|
-
color: hsl(var(--muted-foreground));
|
|
498
|
-
font-size: 12px;
|
|
499
|
-
line-height: 1.4;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
.home-schema-tags__placeholder-icon {
|
|
503
|
-
margin-left: 6px;
|
|
504
|
-
color: hsl(var(--primary));
|
|
505
|
-
font-size: 12px;
|
|
506
|
-
}
|
|
428
|
+
<style scoped lang="scss">
|
|
429
|
+
@import './tags.scss';
|
|
507
430
|
</style>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
.swiper {
|
|
2
|
+
overflow-x: auto;
|
|
3
|
+
padding-bottom: 12px;
|
|
4
|
+
box-sizing: content-box;
|
|
5
|
+
scrollbar-width: thin;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.swiper-track {
|
|
9
|
+
display: flex;
|
|
10
|
+
gap: 12px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.swiper-item {
|
|
14
|
+
flex: 0 0 100%;
|
|
15
|
+
min-width: 100%;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.tool-list {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: row;
|
|
21
|
+
flex-wrap: wrap;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.tool-item {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
align-items: center;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.tool-item .image {
|
|
31
|
+
width: 34px;
|
|
32
|
+
height: 34px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.tool-item .image__content {
|
|
36
|
+
display: flex;
|
|
37
|
+
width: 100%;
|
|
38
|
+
height: 100%;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
border-radius: 999px;
|
|
42
|
+
object-fit: cover;
|
|
43
|
+
background: rgb(15 118 110 / 10%);
|
|
44
|
+
color: var(--custom-color-base, #0f766e);
|
|
45
|
+
font-size: 11px;
|
|
46
|
+
font-weight: 700;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.tool-item .image__content--placeholder {
|
|
50
|
+
text-transform: uppercase;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.tool-item .text {
|
|
54
|
+
font-size: 13px;
|
|
55
|
+
line-height: 1.4;
|
|
56
|
+
color: var(--uni-text-color, #0f172a);
|
|
57
|
+
text-align: center;
|
|
58
|
+
word-break: break-all;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.mt-8rpx {
|
|
62
|
+
margin-top: 4px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.swiper-dots {
|
|
66
|
+
display: flex;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
gap: 6px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.swiper-dot {
|
|
72
|
+
width: 6px;
|
|
73
|
+
height: 6px;
|
|
74
|
+
border-radius: 999px;
|
|
75
|
+
background-color: #cbd5e1;
|
|
76
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { ToolItem, ToolsMode } from '../schema/index.js';
|
|
3
|
+
|
|
4
|
+
import { computed } from 'vue';
|
|
5
|
+
|
|
6
|
+
import { getTextInitial } from '../schema/index.js';
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(
|
|
9
|
+
defineProps<{
|
|
10
|
+
mode?: Partial<ToolsMode>;
|
|
11
|
+
options?: ToolItem[];
|
|
12
|
+
}>(),
|
|
13
|
+
{
|
|
14
|
+
mode: () => ({
|
|
15
|
+
displayMultipleItems: 10,
|
|
16
|
+
displayRowCount: 5,
|
|
17
|
+
}),
|
|
18
|
+
options: () => [],
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const displayMultipleItems = computed(() =>
|
|
23
|
+
Math.max(props.mode.displayMultipleItems ?? 10, 1),
|
|
24
|
+
);
|
|
25
|
+
const displayRowCount = computed(() => Math.max(props.mode.displayRowCount || 5, 1));
|
|
26
|
+
|
|
27
|
+
const displayList = computed<ToolItem[]>(() => {
|
|
28
|
+
if (props.options.length === 0) {
|
|
29
|
+
return Array.from({ length: displayMultipleItems.value }, () => ({
|
|
30
|
+
icon: '',
|
|
31
|
+
path: undefined as never,
|
|
32
|
+
title: '',
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
return props.options;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const swiperList = computed<ToolItem[][]>(() => {
|
|
39
|
+
const pages: ToolItem[][] = [];
|
|
40
|
+
for (let index = 0; index < displayList.value.length; index += displayMultipleItems.value) {
|
|
41
|
+
pages.push(displayList.value.slice(index, index + displayMultipleItems.value));
|
|
42
|
+
}
|
|
43
|
+
return pages;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function resolveItemStyle(itemCount: number, index: number) {
|
|
47
|
+
const rowCount = displayRowCount.value;
|
|
48
|
+
const lastRowCount = itemCount % rowCount || rowCount;
|
|
49
|
+
const shouldShowBottomMargin = index < itemCount - lastRowCount;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
marginBottom: shouldShowBottomMargin ? '16px' : '0px',
|
|
53
|
+
width: `${100 / rowCount}%`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<template>
|
|
59
|
+
<div>
|
|
60
|
+
<template v-if="swiperList.length > 1">
|
|
61
|
+
<div class="swiper">
|
|
62
|
+
<div class="swiper-track">
|
|
63
|
+
<div
|
|
64
|
+
v-for="(page, pageIndex) in swiperList"
|
|
65
|
+
:key="`tools-page-${pageIndex}`"
|
|
66
|
+
class="swiper-item tool-list"
|
|
67
|
+
>
|
|
68
|
+
<div
|
|
69
|
+
v-for="(tool, index) in page"
|
|
70
|
+
:key="`tools-${pageIndex}-${index}`"
|
|
71
|
+
class="tool-item"
|
|
72
|
+
:style="resolveItemStyle(page.length, index)"
|
|
73
|
+
>
|
|
74
|
+
<div class="image">
|
|
75
|
+
<img
|
|
76
|
+
v-if="tool.icon"
|
|
77
|
+
class="image__content"
|
|
78
|
+
:src="tool.icon"
|
|
79
|
+
:alt="tool.title || `工具 ${index + 1}`"
|
|
80
|
+
/>
|
|
81
|
+
<span v-else class="image__content image__content--placeholder">
|
|
82
|
+
{{ getTextInitial(tool.title) }}
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
<span class="text mt-8rpx">{{ tool.title || `工具 ${index + 1}` }}</span>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="swiper-dots">
|
|
91
|
+
<span
|
|
92
|
+
v-for="(_, index) in swiperList"
|
|
93
|
+
:key="`tools-dot-${index}`"
|
|
94
|
+
class="swiper-dot"
|
|
95
|
+
></span>
|
|
96
|
+
</div>
|
|
97
|
+
</template>
|
|
98
|
+
<template v-else>
|
|
99
|
+
<div class="tool-list">
|
|
100
|
+
<div
|
|
101
|
+
v-for="(tool, index) in displayList"
|
|
102
|
+
:key="`tools-${index}`"
|
|
103
|
+
class="tool-item"
|
|
104
|
+
:style="resolveItemStyle(displayList.length, index)"
|
|
105
|
+
>
|
|
106
|
+
<div class="image">
|
|
107
|
+
<img
|
|
108
|
+
v-if="tool.icon"
|
|
109
|
+
class="image__content"
|
|
110
|
+
:src="tool.icon"
|
|
111
|
+
:alt="tool.title || `工具 ${index + 1}`"
|
|
112
|
+
/>
|
|
113
|
+
<span v-else class="image__content image__content--placeholder">
|
|
114
|
+
{{ getTextInitial(tool.title) }}
|
|
115
|
+
</span>
|
|
116
|
+
</div>
|
|
117
|
+
<span class="text mt-8rpx">{{ tool.title || `工具 ${index + 1}` }}</span>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</template>
|
|
121
|
+
</div>
|
|
122
|
+
</template>
|
|
123
|
+
|
|
124
|
+
<style scoped lang="scss">
|
|
125
|
+
@import './tools.scss';
|
|
126
|
+
</style>
|