@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.
- package/README.md +9 -0
- package/package.json +29 -0
- package/src/schema/index.d.ts +183 -0
- package/src/schema/index.js +413 -0
- package/src/web-preview/ActivitiesPreview.vue +120 -0
- package/src/web-preview/CommunitiesPreview.vue +74 -0
- package/src/web-preview/HomeSchemaPreview.vue +103 -0
- package/src/web-preview/ServicesPreview.vue +74 -0
- package/src/web-preview/TagsPreview.vue +507 -0
- package/src/web-preview/ToolsPreview.vue +49 -0
- package/src/web-preview/index.ts +6 -0
|
@@ -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>
|