@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucifer.chao.du/home-schema-components",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Shared home schema components for member and web preview consumers.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -0,0 +1,70 @@
1
+ .activity {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 8px;
5
+ border-radius: 10px;
6
+ }
7
+
8
+ .swiper {
9
+ overflow: hidden;
10
+ border-radius: 10px;
11
+ }
12
+
13
+ .swiper-track {
14
+ display: flex;
15
+ transition-property: transform;
16
+ transition-timing-function: ease;
17
+ }
18
+
19
+ .swiper-item {
20
+ flex: 0 0 100%;
21
+ width: 100%;
22
+ }
23
+
24
+ .image {
25
+ display: block;
26
+ width: 100%;
27
+ height: 250px;
28
+ border-radius: 10px;
29
+ object-fit: cover;
30
+ }
31
+
32
+ .image--placeholder {
33
+ display: flex;
34
+ flex-direction: column;
35
+ justify-content: flex-end;
36
+ padding: 16px;
37
+ background: linear-gradient(135deg, #0f766e 0%, #14b8a6 100%);
38
+ color: #ffffff;
39
+ }
40
+
41
+ .image-placeholder__title {
42
+ font-size: 16px;
43
+ font-weight: 700;
44
+ line-height: 1.5;
45
+ }
46
+
47
+ .image-placeholder__text {
48
+ margin-top: 4px;
49
+ font-size: 12px;
50
+ line-height: 1.4;
51
+ opacity: 0.8;
52
+ word-break: break-all;
53
+ }
54
+
55
+ .swiper-dots {
56
+ display: flex;
57
+ justify-content: center;
58
+ gap: 6px;
59
+ }
60
+
61
+ .swiper-dot {
62
+ width: 6px;
63
+ height: 6px;
64
+ border-radius: 999px;
65
+ background-color: #cbd5e1;
66
+ }
67
+
68
+ .swiper-dot--active {
69
+ background-color: var(--custom-color-base, #0f766e);
70
+ }
@@ -0,0 +1,106 @@
1
+ <script lang="ts" setup>
2
+ import type { ActivitiesMode, ActivityItem } from '../schema/index.js';
3
+
4
+ import { computed, onBeforeUnmount, ref, watch } from 'vue';
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ mode?: Partial<ActivitiesMode>;
9
+ options?: ActivityItem[];
10
+ }>(),
11
+ {
12
+ mode: () => ({
13
+ autoplay: true,
14
+ duration: 500,
15
+ interval: 5000,
16
+ }),
17
+ options: () => [],
18
+ },
19
+ );
20
+
21
+ const displayList = computed<ActivityItem[]>(() => {
22
+ if (props.options.length > 0) {
23
+ return props.options;
24
+ }
25
+ return [{ image: '', path: undefined as never }];
26
+ });
27
+
28
+ const activeIndex = ref(0);
29
+ let activitiesTimer: null | ReturnType<typeof setInterval> = null;
30
+
31
+ const swiperTrackStyle = computed(() => ({
32
+ transform: `translateX(-${activeIndex.value * 100}%)`,
33
+ transitionDuration: `${props.mode.duration || 500}ms`,
34
+ }));
35
+
36
+ function clearActivitiesTimer() {
37
+ if (!activitiesTimer) {
38
+ return;
39
+ }
40
+ clearInterval(activitiesTimer);
41
+ activitiesTimer = null;
42
+ }
43
+
44
+ function startActivitiesTimer() {
45
+ clearActivitiesTimer();
46
+ if (!props.mode.autoplay || displayList.value.length <= 1) {
47
+ return;
48
+ }
49
+
50
+ activitiesTimer = setInterval(() => {
51
+ activeIndex.value = (activeIndex.value + 1) % displayList.value.length;
52
+ }, props.mode.interval || 5000);
53
+ }
54
+
55
+ watch(
56
+ () => [props.mode.autoplay, props.mode.interval, displayList.value.length],
57
+ () => {
58
+ if (activeIndex.value >= displayList.value.length) {
59
+ activeIndex.value = 0;
60
+ }
61
+ startActivitiesTimer();
62
+ },
63
+ { immediate: true },
64
+ );
65
+
66
+ onBeforeUnmount(() => {
67
+ clearActivitiesTimer();
68
+ });
69
+ </script>
70
+
71
+ <template>
72
+ <div class="activity">
73
+ <div class="swiper">
74
+ <div class="swiper-track" :style="swiperTrackStyle">
75
+ <div
76
+ v-for="(item, index) in displayList"
77
+ :key="`activities-${index}`"
78
+ class="swiper-item"
79
+ >
80
+ <img
81
+ v-if="item.image"
82
+ class="image"
83
+ :src="item.image"
84
+ :alt="`活动横幅 ${index + 1}`"
85
+ />
86
+ <div v-else class="image image--placeholder">
87
+ <span class="image-placeholder__title">活动横幅 {{ index + 1 }}</span>
88
+ <span class="image-placeholder__text">未配置图片地址</span>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ <div v-if="displayList.length > 1" class="swiper-dots">
94
+ <span
95
+ v-for="(_, index) in displayList"
96
+ :key="`activities-dot-${index}`"
97
+ class="swiper-dot"
98
+ :class="{ 'swiper-dot--active': index === activeIndex }"
99
+ ></span>
100
+ </div>
101
+ </div>
102
+ </template>
103
+
104
+ <style scoped lang="scss">
105
+ @import './activities.scss';
106
+ </style>
@@ -0,0 +1,82 @@
1
+ .community {
2
+ padding: 11px;
3
+ min-height: 66px;
4
+ background-color: var(--uni-bg-color, #ffffff);
5
+ border-radius: 5px;
6
+ display: flex;
7
+ flex-direction: row;
8
+ box-shadow: 0 0 6px rgb(15 23 42 / 8%);
9
+ }
10
+
11
+ .community-title {
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ margin-right: 15px;
16
+ }
17
+
18
+ .community-title .image {
19
+ width: 45px;
20
+ height: 40px;
21
+ border-radius: 6px;
22
+ object-fit: cover;
23
+ }
24
+
25
+ .community-title .image--placeholder {
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ background: rgb(15 118 110 / 10%);
30
+ color: var(--custom-color-base, #0f766e);
31
+ font-size: 12px;
32
+ font-weight: 600;
33
+ }
34
+
35
+ .community-content {
36
+ flex: 1;
37
+ display: flex;
38
+ }
39
+
40
+ .community-swiper {
41
+ display: flex;
42
+ flex: 1;
43
+ flex-direction: column;
44
+ justify-content: center;
45
+ gap: 8px;
46
+ }
47
+
48
+ .community-swiper-item {
49
+ min-width: 0;
50
+ }
51
+
52
+ .community-swiper-item .item-content {
53
+ width: 100%;
54
+ min-height: 20px;
55
+ display: flex;
56
+ flex-direction: row;
57
+ align-items: center;
58
+ }
59
+
60
+ .community-swiper-item .circle {
61
+ flex: 0 0 auto;
62
+ margin-right: 7px;
63
+ width: 4px;
64
+ height: 4px;
65
+ background-color: var(--uni-text-color-gray, #64748b);
66
+ border-radius: 999px;
67
+ transform: translateY(2px);
68
+ }
69
+
70
+ .community-swiper-item .text {
71
+ flex: 1;
72
+ font-size: 15px;
73
+ line-height: 1.4;
74
+ color: var(--uni-text-color, #0f172a);
75
+ }
76
+
77
+ .community-swiper-item .text.ellipsis {
78
+ display: inline-block;
79
+ overflow: hidden;
80
+ white-space: nowrap;
81
+ text-overflow: ellipsis;
82
+ }
@@ -0,0 +1,125 @@
1
+ <script lang="ts" setup>
2
+ import type { CommunitiesMode, CommunityItem } from '../schema/index.js';
3
+
4
+ import { computed, onBeforeUnmount, ref, watch } from 'vue';
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ image?: string;
9
+ mode?: Partial<CommunitiesMode>;
10
+ options?: CommunityItem[];
11
+ }>(),
12
+ {
13
+ image: '',
14
+ mode: () => ({
15
+ autoplay: true,
16
+ displayMultipleItems: 2,
17
+ duration: 500,
18
+ interval: 5000,
19
+ showIcon: true,
20
+ }),
21
+ options: () => [],
22
+ },
23
+ );
24
+
25
+ const resolvedImage = computed(() => props.image || '');
26
+ const displayMultipleItems = computed(() =>
27
+ Math.max(props.mode.displayMultipleItems || 2, 1),
28
+ );
29
+ const sourceList = computed<CommunityItem[]>(() => {
30
+ if (props.options.length > 0) {
31
+ return props.options;
32
+ }
33
+ return Array.from({ length: displayMultipleItems.value }, () => ({
34
+ path: undefined as never,
35
+ title: '',
36
+ }));
37
+ });
38
+
39
+ const activeIndex = ref(0);
40
+ let communitiesTimer: null | ReturnType<typeof setInterval> = null;
41
+
42
+ const displayList = computed<CommunityItem[]>(() => {
43
+ if (sourceList.value.length <= displayMultipleItems.value) {
44
+ return sourceList.value;
45
+ }
46
+
47
+ return Array.from({ length: displayMultipleItems.value }, (_, index) => {
48
+ const resolvedIndex = (activeIndex.value + index) % sourceList.value.length;
49
+ return sourceList.value[resolvedIndex]!;
50
+ });
51
+ });
52
+
53
+ function clearCommunitiesTimer() {
54
+ if (!communitiesTimer) {
55
+ return;
56
+ }
57
+ clearInterval(communitiesTimer);
58
+ communitiesTimer = null;
59
+ }
60
+
61
+ function startCommunitiesTimer() {
62
+ clearCommunitiesTimer();
63
+ if (!props.mode.autoplay || sourceList.value.length <= displayMultipleItems.value) {
64
+ return;
65
+ }
66
+
67
+ communitiesTimer = setInterval(() => {
68
+ activeIndex.value = (activeIndex.value + 1) % sourceList.value.length;
69
+ }, props.mode.interval || 5000);
70
+ }
71
+
72
+ watch(
73
+ () => [
74
+ props.mode.autoplay,
75
+ props.mode.interval,
76
+ sourceList.value.length,
77
+ displayMultipleItems.value,
78
+ ],
79
+ () => {
80
+ if (activeIndex.value >= sourceList.value.length) {
81
+ activeIndex.value = 0;
82
+ }
83
+ startCommunitiesTimer();
84
+ },
85
+ { immediate: true },
86
+ );
87
+
88
+ onBeforeUnmount(() => {
89
+ clearCommunitiesTimer();
90
+ });
91
+ </script>
92
+
93
+ <template>
94
+ <div class="community" v-if="displayList.length > 0">
95
+ <div class="community-title">
96
+ <img
97
+ v-if="resolvedImage"
98
+ class="image"
99
+ :src="resolvedImage"
100
+ alt="社区播报标题"
101
+ />
102
+ <div v-else class="image image--placeholder">播报</div>
103
+ </div>
104
+ <div class="community-content">
105
+ <div class="community-swiper">
106
+ <div
107
+ v-for="(item, index) in displayList"
108
+ :key="`communities-${index}`"
109
+ class="community-swiper-item"
110
+ >
111
+ <div class="item-content">
112
+ <span v-if="props.mode.showIcon" class="circle"></span>
113
+ <span class="text" :class="{ ellipsis: displayMultipleItems > 1 }">
114
+ {{ item.title || `播报内容 ${index + 1}` }}
115
+ </span>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ </template>
122
+
123
+ <style scoped lang="scss">
124
+ @import './communities.scss';
125
+ </style>
@@ -1,6 +1,5 @@
1
- export { default as ActivitiesPreview } from './ActivitiesPreview.vue';
2
- export { default as CommunitiesPreview } from './CommunitiesPreview.vue';
3
- export { default as HomeSchemaPreview } from './HomeSchemaPreview.vue';
4
- export { default as ServicesPreview } from './ServicesPreview.vue';
5
- export { default as TagsPreview } from './TagsPreview.vue';
6
- export { default as ToolsPreview } from './ToolsPreview.vue';
1
+ export { default as ActivitiesPreview } from './activities.vue';
2
+ export { default as CommunitiesPreview } from './communities.vue';
3
+ export { default as ServicesPreview } from './services.vue';
4
+ export { default as TagsPreview } from './tags.vue';
5
+ export { default as ToolsPreview } from './tools.vue';
@@ -0,0 +1,237 @@
1
+ .service-list {
2
+ display: flex;
3
+ flex-direction: column;
4
+ }
5
+
6
+ .service-list__head {
7
+ display: flex;
8
+ flex-direction: row;
9
+ align-items: flex-end;
10
+ margin-bottom: 16px;
11
+ padding-bottom: 8px;
12
+ }
13
+
14
+ .service-list__head-title {
15
+ flex: 1;
16
+ display: flex;
17
+ flex-direction: row;
18
+ align-items: center;
19
+ }
20
+
21
+ .service-list__head-line {
22
+ margin-right: 8px;
23
+ width: 4px;
24
+ height: 26px;
25
+ border-radius: 999px;
26
+ background-color: var(--custom-color-base, #0f766e);
27
+ }
28
+
29
+ .service-list__head-text {
30
+ font-size: 20px;
31
+ font-weight: 700;
32
+ color: var(--uni-text-color, #0f172a);
33
+ }
34
+
35
+ .service-list__head-action {
36
+ padding-bottom: 4px;
37
+ }
38
+
39
+ .service-list__head-action-text {
40
+ font-size: 13px;
41
+ color: var(--uni-text-color-gray, #64748b);
42
+ }
43
+
44
+ .service-list__body {
45
+ display: flex;
46
+ flex-direction: column;
47
+ }
48
+
49
+ .service-list__item {
50
+ margin-bottom: 13px;
51
+ padding: 15px 12px;
52
+ background-color: var(--uni-bg-color, #ffffff);
53
+ box-shadow: 0 0 5px rgb(15 23 42 / 10%);
54
+ border-radius: 10px;
55
+ display: flex;
56
+ flex-direction: row;
57
+ }
58
+
59
+ .service-list__media {
60
+ flex: 0 0 auto;
61
+ margin-right: 8px;
62
+ }
63
+
64
+ .service-list__media .image {
65
+ display: block;
66
+ width: 88px;
67
+ height: 88px;
68
+ border-radius: 10px;
69
+ object-fit: cover;
70
+ }
71
+
72
+ .service-list__media .image--placeholder {
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ background: linear-gradient(135deg, rgb(15 118 110 / 16%), rgb(20 184 166 / 26%));
77
+ color: var(--custom-color-base, #0f766e);
78
+ font-size: 14px;
79
+ font-weight: 600;
80
+ }
81
+
82
+ .service-list__content {
83
+ flex: 1;
84
+ min-width: 0;
85
+ display: flex;
86
+ flex-direction: column;
87
+ }
88
+
89
+ .service-list__row {
90
+ margin-bottom: 9px;
91
+ display: flex;
92
+ flex-direction: row;
93
+ align-items: center;
94
+ }
95
+
96
+ .service-list__row:last-child {
97
+ margin-bottom: 0;
98
+ }
99
+
100
+ .service-list__row-item {
101
+ display: flex;
102
+ flex-direction: row;
103
+ align-items: center;
104
+ }
105
+
106
+ .service-list__row-item--grow {
107
+ flex: 1;
108
+ min-width: 0;
109
+ }
110
+
111
+ .service-list__row-item--rates {
112
+ margin-right: 12px;
113
+ }
114
+
115
+ .service-list__title-text {
116
+ display: inline-block;
117
+ max-width: 100%;
118
+ overflow: hidden;
119
+ text-overflow: ellipsis;
120
+ white-space: nowrap;
121
+ font-size: 16px;
122
+ font-weight: 700;
123
+ color: var(--uni-text-color, #0f172a);
124
+ }
125
+
126
+ .service-list__type-badge {
127
+ margin-left: 13px;
128
+ padding: 3px 4px;
129
+ border-radius: 4px;
130
+ }
131
+
132
+ .service-list__type-text {
133
+ font-size: 10px;
134
+ line-height: 1;
135
+ }
136
+
137
+ .service-list__meta-text,
138
+ .service-list__distance-text {
139
+ font-size: 14px;
140
+ color: var(--uni-text-color-gray, #64748b);
141
+ }
142
+
143
+ .service-list__separator {
144
+ margin: 0 3px;
145
+ width: 2px;
146
+ height: 2px;
147
+ border-radius: 999px;
148
+ background-color: var(--uni-text-color-gray, #64748b);
149
+ }
150
+
151
+ .service-list__rate {
152
+ display: inline-flex;
153
+ align-items: center;
154
+ font-size: 13px;
155
+ color: #f59e0b;
156
+ }
157
+
158
+ .service-list__distance-icon {
159
+ margin-right: 4px;
160
+ font-size: 12px;
161
+ line-height: 1;
162
+ }
163
+
164
+ .service-list__discount {
165
+ display: flex;
166
+ flex-direction: column;
167
+ gap: 6px;
168
+ }
169
+
170
+ .service-list__discount-row {
171
+ display: flex;
172
+ flex-direction: row;
173
+ align-items: center;
174
+ flex-wrap: wrap;
175
+ gap: 4px 0;
176
+ }
177
+
178
+ .service-list__discount-type {
179
+ padding: 2px 4px;
180
+ background-color: rgb(239 68 68 / 10%);
181
+ border: 1px solid rgb(239 68 68 / 35%);
182
+ border-radius: 5px;
183
+ }
184
+
185
+ .service-list__discount-type-text,
186
+ .service-list__discount-text-content {
187
+ font-size: 11px;
188
+ color: #dc2626;
189
+ }
190
+
191
+ .service-list__discount-name {
192
+ margin-right: 3px;
193
+ }
194
+
195
+ .service-list__discount-name-text,
196
+ .service-list__discount-project-text {
197
+ font-size: 14px;
198
+ color: var(--uni-text-color, #0f172a);
199
+ }
200
+
201
+ .service-list__discount-price {
202
+ margin-right: 8px;
203
+ display: flex;
204
+ flex-direction: row;
205
+ align-items: center;
206
+ }
207
+
208
+ .service-list__discount-final-price {
209
+ margin-right: 8px;
210
+ display: flex;
211
+ flex-direction: row;
212
+ align-items: flex-end;
213
+ }
214
+
215
+ .service-list__discount-final-price-text {
216
+ font-size: 15px;
217
+ font-weight: 700;
218
+ color: #dc2626;
219
+ }
220
+
221
+ .service-list__discount-cny {
222
+ margin-bottom: 1px;
223
+ font-size: 11px;
224
+ font-weight: 700;
225
+ color: #dc2626;
226
+ }
227
+
228
+ .service-list__discount-text {
229
+ padding: 2px 5px;
230
+ background-color: rgb(239 68 68 / 10%);
231
+ border-radius: 5px;
232
+ }
233
+
234
+ .service-list__discount-row--bonus .service-list__discount-name-text,
235
+ .service-list__discount-row--bonus .service-list__discount-project-text {
236
+ color: #dc2626;
237
+ }