@lucifer.chao.du/home-schema-components 0.1.0

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.
@@ -0,0 +1,120 @@
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 slides = 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 trackStyle = computed(() => ({
32
+ '--activity-duration': `${props.mode.duration || 500}ms`,
33
+ transform: `translateX(-${activeIndex.value * 100}%)`,
34
+ transitionDuration: `${props.mode.duration || 500}ms`,
35
+ }));
36
+
37
+ function clearActivitiesTimer() {
38
+ if (!activitiesTimer) {
39
+ return;
40
+ }
41
+ clearInterval(activitiesTimer);
42
+ activitiesTimer = null;
43
+ }
44
+
45
+ function startActivitiesTimer() {
46
+ clearActivitiesTimer();
47
+ if (!props.mode.autoplay || slides.value.length <= 1) {
48
+ return;
49
+ }
50
+
51
+ activitiesTimer = setInterval(() => {
52
+ activeIndex.value = (activeIndex.value + 1) % slides.value.length;
53
+ }, props.mode.interval || 5000);
54
+ }
55
+
56
+ watch(
57
+ () => [props.mode.autoplay, props.mode.interval, slides.value.length],
58
+ () => {
59
+ if (activeIndex.value >= slides.value.length) {
60
+ activeIndex.value = 0;
61
+ }
62
+ startActivitiesTimer();
63
+ },
64
+ { immediate: true },
65
+ );
66
+
67
+ onBeforeUnmount(() => {
68
+ clearActivitiesTimer();
69
+ });
70
+ </script>
71
+
72
+ <template>
73
+ <div class="grid gap-2">
74
+ <div class="relative overflow-hidden rounded-[20px]">
75
+ <div
76
+ :style="trackStyle"
77
+ :class="{
78
+ 'home-schema-preview__activities-track--autoplay': props.mode.autoplay,
79
+ }"
80
+ class="home-schema-preview__activities-track flex"
81
+ >
82
+ <div
83
+ v-for="(item, index) in slides"
84
+ :key="`activities-${index}`"
85
+ class="home-schema-preview__activity-item flex h-24 w-full shrink-0 items-end bg-gradient-to-br from-primary to-primary/65 px-4 py-3 text-primary-foreground"
86
+ >
87
+ <div class="grid gap-1">
88
+ <div class="text-sm font-semibold leading-5">横幅 {{ index + 1 }}</div>
89
+ <div
90
+ class="text-[11px] leading-4 text-primary-foreground/75 whitespace-normal break-all"
91
+ >
92
+ {{ item.image || '未配置图片地址' }}
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ <div v-if="slides.length > 1" class="flex justify-center gap-1 pt-1">
99
+ <span
100
+ v-for="(_, index) in slides"
101
+ :key="`activities-dot-${index}`"
102
+ :class="{
103
+ 'bg-primary/80': index === activeIndex,
104
+ 'bg-border': index !== activeIndex,
105
+ }"
106
+ class="h-1.5 w-1.5 rounded-full"
107
+ ></span>
108
+ </div>
109
+ </div>
110
+ </template>
111
+
112
+ <style scoped>
113
+ .home-schema-preview__activities-track {
114
+ transition: transform var(--activity-duration, 500ms) ease;
115
+ }
116
+
117
+ .home-schema-preview__activity-item {
118
+ border-radius: 20px;
119
+ }
120
+ </style>
@@ -0,0 +1,74 @@
1
+ <script lang="ts" setup>
2
+ import type { CommunitiesMode, CommunityItem } from '../schema/index.js';
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ image?: string;
7
+ mode?: Partial<CommunitiesMode>;
8
+ options?: CommunityItem[];
9
+ }>(),
10
+ {
11
+ image: '',
12
+ mode: () => ({
13
+ autoplay: true,
14
+ displayMultipleItems: 2,
15
+ duration: 500,
16
+ interval: 5000,
17
+ showIcon: true,
18
+ }),
19
+ options: () => [],
20
+ },
21
+ );
22
+ </script>
23
+
24
+ <template>
25
+ <div class="grid gap-2">
26
+ <div
27
+ v-for="(item, index) in props.options.slice(0, props.mode.displayMultipleItems || 2)"
28
+ :key="`communities-${index}`"
29
+ class="home-schema-preview__community-item text-text-secondary rounded-2xl border border-border bg-muted/30 px-3 py-3 text-xs leading-5"
30
+ :style="{
31
+ '--community-duration': `${props.mode.duration || 500}ms`,
32
+ '--community-interval': `${props.mode.interval || 5000}ms`,
33
+ }"
34
+ :class="{
35
+ 'home-schema-preview__community-item--autoplay': props.mode.autoplay,
36
+ }"
37
+ >
38
+ <span
39
+ v-if="props.mode.showIcon"
40
+ class="home-schema-preview__community-icon mr-1.5 inline-flex h-4 w-4 items-center justify-center rounded-full text-[10px]"
41
+ >
42
+
43
+ </span>
44
+ {{ item.title || `播报内容 ${index + 1}` }}
45
+ </div>
46
+ </div>
47
+ </template>
48
+
49
+ <style scoped>
50
+ .home-schema-preview__community-item {
51
+ transition: transform var(--community-duration, 500ms) ease;
52
+ }
53
+
54
+ .home-schema-preview__community-item--autoplay {
55
+ animation: home-schema-preview-community-loop var(--community-interval, 5000ms)
56
+ ease-in-out infinite;
57
+ }
58
+
59
+ .home-schema-preview__community-icon {
60
+ color: hsl(var(--primary));
61
+ background: hsl(var(--primary) / 14%);
62
+ }
63
+
64
+ @keyframes home-schema-preview-community-loop {
65
+ 0%,
66
+ 100% {
67
+ transform: translateX(0);
68
+ }
69
+
70
+ 50% {
71
+ transform: translateX(2px);
72
+ }
73
+ }
74
+ </style>
@@ -0,0 +1,103 @@
1
+ <script lang="ts" setup>
2
+ import type { HomeSchemaComponent } from '../schema/index.js';
3
+
4
+ import { computed } from 'vue';
5
+
6
+ import {
7
+ normalizeComponentProps,
8
+ } from '../schema/index.js';
9
+ import ActivitiesPreview from './ActivitiesPreview.vue';
10
+ import CommunitiesPreview from './CommunitiesPreview.vue';
11
+ import ServicesPreview from './ServicesPreview.vue';
12
+ import TagsPreview from './TagsPreview.vue';
13
+ import ToolsPreview from './ToolsPreview.vue';
14
+
15
+ type ComponentValue = {
16
+ componentProps?: Record<string, any>;
17
+ };
18
+
19
+ const props = withDefaults(
20
+ defineProps<{
21
+ componentType: HomeSchemaComponent | string;
22
+ disabled?: boolean;
23
+ locationText?: string;
24
+ pageValue?: Record<string, any>;
25
+ selected?: boolean;
26
+ showServiceHead?: boolean;
27
+ value?: ComponentValue;
28
+ }>(),
29
+ {
30
+ disabled: false,
31
+ locationText: '',
32
+ pageValue: () => ({}),
33
+ selected: false,
34
+ showServiceHead: false,
35
+ value: () => ({}),
36
+ },
37
+ );
38
+
39
+ const normalizedTools = computed(() =>
40
+ props.componentType === 'tools'
41
+ ? normalizeComponentProps('tools', props.value?.componentProps)
42
+ : null,
43
+ );
44
+
45
+ const normalizedCommunities = computed(() =>
46
+ props.componentType === 'communities'
47
+ ? normalizeComponentProps('communities', props.value?.componentProps)
48
+ : null,
49
+ );
50
+
51
+ const normalizedTags = computed(() =>
52
+ props.componentType === 'tags'
53
+ ? normalizeComponentProps('tags', props.value?.componentProps)
54
+ : null,
55
+ );
56
+
57
+ const normalizedActivities = computed(() =>
58
+ props.componentType === 'activities'
59
+ ? normalizeComponentProps('activities', props.value?.componentProps)
60
+ : null,
61
+ );
62
+
63
+ const normalizedServices = computed(() =>
64
+ props.componentType === 'services'
65
+ ? normalizeComponentProps('services', props.value?.componentProps)
66
+ : null,
67
+ );
68
+ </script>
69
+
70
+ <template>
71
+ <div class="grid gap-3">
72
+ <ToolsPreview
73
+ v-if="props.componentType === 'tools' && normalizedTools"
74
+ :mode="normalizedTools.mode"
75
+ :options="normalizedTools.options"
76
+ />
77
+ <CommunitiesPreview
78
+ v-else-if="props.componentType === 'communities' && normalizedCommunities"
79
+ :image="normalizedCommunities.image"
80
+ :mode="normalizedCommunities.mode"
81
+ :options="normalizedCommunities.options"
82
+ />
83
+ <TagsPreview
84
+ v-else-if="props.componentType === 'tags' && normalizedTags"
85
+ :mode="normalizedTags.mode"
86
+ :options="normalizedTags.options"
87
+ />
88
+ <ActivitiesPreview
89
+ v-else-if="props.componentType === 'activities' && normalizedActivities"
90
+ :mode="normalizedActivities.mode"
91
+ :options="normalizedActivities.options"
92
+ />
93
+ <ServicesPreview
94
+ v-else-if="props.componentType === 'services' && normalizedServices"
95
+ :location-text="props.locationText"
96
+ :options="normalizedServices.options"
97
+ :show-head="props.showServiceHead"
98
+ />
99
+ <div v-else class="text-text-secondary text-xs leading-5">
100
+ 当前组件暂无预览模板。
101
+ </div>
102
+ </div>
103
+ </template>
@@ -0,0 +1,74 @@
1
+ <script lang="ts" setup>
2
+ import type { ServiceItem } from '../schema/index.js';
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ locationText?: string;
7
+ options?: ServiceItem[];
8
+ showHead?: boolean;
9
+ }>(),
10
+ {
11
+ locationText: '',
12
+ options: () => [],
13
+ showHead: false,
14
+ },
15
+ );
16
+
17
+ function getDiscountText(discount: Record<string, any>) {
18
+ return (
19
+ String(discount.project || '').trim() ||
20
+ String(discount.type || '').trim() ||
21
+ '优惠信息'
22
+ );
23
+ }
24
+ </script>
25
+
26
+ <template>
27
+ <div class="grid gap-2">
28
+ <div
29
+ v-if="props.showHead"
30
+ class="flex items-end justify-between gap-3 border-b border-border pb-2"
31
+ >
32
+ <div class="flex items-center gap-2">
33
+ <span class="h-5 w-1 rounded-full bg-primary"></span>
34
+ <span class="text-text text-base font-semibold">
35
+ {{ props.locationText }}生活圈
36
+ </span>
37
+ </div>
38
+ <span class="text-text-secondary text-xs">申请加入</span>
39
+ </div>
40
+
41
+ <div
42
+ v-for="(item, index) in props.options"
43
+ :key="`services-${index}`"
44
+ class="grid gap-3 rounded-[20px] border border-border bg-muted/30 p-3"
45
+ >
46
+ <div class="flex gap-3">
47
+ <div
48
+ class="h-14 w-14 shrink-0 rounded-2xl bg-gradient-to-br from-primary/20 to-primary/10"
49
+ ></div>
50
+ <div class="min-w-0 flex-1">
51
+ <div class="text-text truncate text-sm font-semibold leading-5">
52
+ {{ item.title || `服务 ${index + 1}` }}
53
+ </div>
54
+ <div class="text-text-secondary mt-1 text-[11px] leading-4">
55
+ {{ item.category || '服务分类' }} ·
56
+ {{ item.distance || '距离待配置' }}
57
+ </div>
58
+ </div>
59
+ </div>
60
+ <div
61
+ v-if="Array.isArray(item.discount) && item.discount.length > 0"
62
+ class="flex flex-wrap gap-2"
63
+ >
64
+ <span
65
+ v-for="(discount, discountIndex) in item.discount"
66
+ :key="`services-${index}-${discountIndex}`"
67
+ class="text-text-secondary rounded-full border border-border bg-background px-2 py-1 text-[11px] leading-4"
68
+ >
69
+ {{ getDiscountText(discount) }}
70
+ </span>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </template>