@vue-lynx-example/hackernews 0.2.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/src/App.vue ADDED
@@ -0,0 +1,97 @@
1
+ <script setup lang="ts">
2
+ import { RouterView } from 'vue-router';
3
+ import { validFeeds } from './api';
4
+ import NavLink from './components/NavLink.vue';
5
+ import logoUrl from './assets/lynx-logo.png';
6
+
7
+ import './App.css';
8
+
9
+ const feedKeys = Object.keys(validFeeds);
10
+ </script>
11
+
12
+ <template>
13
+ <view class="w-full h-full bg-hn-bg flex flex-col">
14
+ <!-- Header: 55px, matching reference -->
15
+ <view
16
+ class="bg-hn-green flex flex-col"
17
+ :style="{ height: '55px' }"
18
+ >
19
+ <scroll-view
20
+ scroll-x
21
+ class="flex-1"
22
+ :style="{ height: '100%' }"
23
+ :content-container-style="{ display: 'flex', flexDirection: 'row', alignItems: 'center' }"
24
+ >
25
+ <!-- Logo: Lynx logo with thin white border (YC-style) -->
26
+ <view
27
+ :style="{ display: 'flex', alignItems: 'center', justifyContent: 'center', marginLeft: '15px', marginRight: '12px' }"
28
+ >
29
+ <view
30
+ class="flex-shrink-0"
31
+ :style="{ width: '26px', height: '26px', borderWidth: '1px', borderColor: '#fff', borderRadius: '0px', display: 'flex', alignItems: 'center', justifyContent: 'center' }"
32
+ >
33
+ <image
34
+ :src="logoUrl"
35
+ :style="{ width: '24px', height: '24px' }"
36
+ resize="cover"
37
+ />
38
+ </view>
39
+ </view>
40
+
41
+ <!-- Feed nav links -->
42
+ <NavLink
43
+ v-for="key in feedKeys"
44
+ :key="key"
45
+ :to="`/${key}`"
46
+ :label="validFeeds[key].title"
47
+ />
48
+
49
+ <!-- Built with VueLynx -->
50
+ <view
51
+ :style="{
52
+ display: 'flex',
53
+ alignItems: 'center',
54
+ justifyContent: 'center',
55
+ marginLeft: 'auto',
56
+ marginRight: '15px',
57
+ }"
58
+ >
59
+ <text
60
+ :style="{
61
+ color: '#fff',
62
+ fontSize: '14px',
63
+ fontWeight: '300',
64
+ letterSpacing: '0.075em',
65
+ whiteSpace: 'nowrap',
66
+ flexShrink: 0,
67
+ }"
68
+ >
69
+ Built with VueLynx
70
+ </text>
71
+ </view>
72
+ </scroll-view>
73
+ </view>
74
+
75
+ <!-- Route content: fade on route type change, NOT on pagination -->
76
+ <RouterView v-slot="{ Component, route }">
77
+ <Transition name="fade" mode="out-in" :duration="200">
78
+ <component
79
+ :is="Component"
80
+ :key="(route.params.feed as string) || route.name"
81
+ />
82
+ </Transition>
83
+ </RouterView>
84
+ </view>
85
+ </template>
86
+
87
+ <style>
88
+ /* Fade: for switching between route types (feed→item, feed→user, news→ask) */
89
+ .fade-enter-active,
90
+ .fade-leave-active {
91
+ transition: opacity 200ms ease;
92
+ }
93
+ .fade-enter-from,
94
+ .fade-leave-to {
95
+ opacity: 0;
96
+ }
97
+ </style>
package/src/api.ts ADDED
@@ -0,0 +1,82 @@
1
+ const _fetch: typeof fetch = globalThis.fetch ?? fetch;
2
+
3
+ const BASE_URL = 'https://api.hackerwebapp.com';
4
+
5
+ export interface FeedItem {
6
+ id: number;
7
+ title: string;
8
+ url: string;
9
+ user: string;
10
+ points: number;
11
+ time_ago: string;
12
+ comments_count: number;
13
+ type: string;
14
+ }
15
+
16
+ export interface ItemDetail {
17
+ id: number;
18
+ title: string;
19
+ url: string;
20
+ user: string;
21
+ points: number;
22
+ time_ago: string;
23
+ comments_count: number;
24
+ comments: CommentData[];
25
+ content: string;
26
+ type: string;
27
+ }
28
+
29
+ export interface CommentData {
30
+ id: number;
31
+ user: string;
32
+ time_ago: string;
33
+ content: string;
34
+ comments: CommentData[];
35
+ }
36
+
37
+ export interface UserData {
38
+ id: string;
39
+ created: string;
40
+ karma: number;
41
+ about: string;
42
+ }
43
+
44
+ export const validFeeds: Record<string, { title: string; pages: number }> = {
45
+ news: { title: 'News', pages: 10 },
46
+ newest: { title: 'Newest', pages: 12 },
47
+ ask: { title: 'Ask', pages: 2 },
48
+ show: { title: 'Show', pages: 2 },
49
+ jobs: { title: 'Jobs', pages: 1 },
50
+ };
51
+
52
+ export async function fetchFeed(
53
+ feed: string,
54
+ page: number,
55
+ ): Promise<FeedItem[]> {
56
+ const res = await _fetch(`${BASE_URL}/${feed}?page=${page}`);
57
+ if (!res.ok) throw new Error(`Failed to fetch ${feed} page ${page}`);
58
+ return res.json();
59
+ }
60
+
61
+ export async function fetchItem(id: number): Promise<ItemDetail> {
62
+ const res = await _fetch(`${BASE_URL}/item/${id}`);
63
+ if (!res.ok) throw new Error(`Failed to fetch item ${id}`);
64
+ return res.json();
65
+ }
66
+
67
+ export async function fetchUser(id: string): Promise<UserData> {
68
+ // The hackerwebapp API doesn't have a working user endpoint,
69
+ // so we use the official HN Firebase API and normalize the response.
70
+ const res = await _fetch(
71
+ `https://hacker-news.firebaseio.com/v0/user/${id}.json`,
72
+ );
73
+ if (!res.ok) throw new Error(`Failed to fetch user ${id}`);
74
+ const data = await res.json();
75
+ if (!data) throw new Error(`User ${id} not found`);
76
+ return {
77
+ id: data.id,
78
+ created: new Date(data.created * 1000).toLocaleDateString(),
79
+ karma: data.karma,
80
+ about: data.about || '',
81
+ };
82
+ }
Binary file
@@ -0,0 +1,78 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue-lynx';
3
+ import { useRouter } from 'vue-router';
4
+ import type { CommentData } from '../api';
5
+ import { pluralize, stripHtml } from '../utils';
6
+
7
+ defineProps<{
8
+ comment: CommentData;
9
+ depth?: number;
10
+ }>();
11
+
12
+ const router = useRouter();
13
+ const open = ref(true);
14
+
15
+ function goToUser(userId: string) {
16
+ router.push(`/user/${userId}`);
17
+ }
18
+ </script>
19
+
20
+ <template>
21
+ <view
22
+ v-if="comment && comment.user"
23
+ class="flex flex-col border-t border-hn-border"
24
+ :style="{ paddingLeft: `${(depth ?? 0) * 24}px` }"
25
+ >
26
+ <!-- Comment header -->
27
+ <view class="flex flex-row" :style="{ gap: '6px', paddingTop: '1em' }">
28
+ <text
29
+ :style="{ color: '#222', fontSize: '0.9em', textDecorationLine: 'underline' }"
30
+ @tap="goToUser(comment.user)"
31
+ >
32
+ {{ comment.user }}
33
+ </text>
34
+ <text :style="{ color: '#595959', fontSize: '0.9em' }">
35
+ {{ comment.time_ago }}
36
+ </text>
37
+ </view>
38
+
39
+ <!-- Comment body -->
40
+ <view :style="{ paddingTop: '0.5em', paddingBottom: '0.5em' }">
41
+ <text :style="{ color: '#2e495e', fontSize: '0.9em', lineHeight: '1.5em' }">
42
+ {{ stripHtml(comment.content) }}
43
+ </text>
44
+ </view>
45
+
46
+ <!-- Toggle children -->
47
+ <view v-if="comment.comments && comment.comments.length">
48
+ <view
49
+ :style="{
50
+ backgroundColor: open ? 'transparent' : '#fffbf2',
51
+ borderRadius: '4px',
52
+ padding: open ? '0' : '0.3em 0.5em',
53
+ alignSelf: 'flex-start',
54
+ marginBottom: open ? '0' : '0.5em',
55
+ }"
56
+ @tap="open = !open"
57
+ >
58
+ <text :style="{ color: '#222', fontSize: '0.9em' }">
59
+ {{
60
+ open
61
+ ? '[-]'
62
+ : '[+] ' + pluralize(comment.comments.length, 'reply', 'replies') + ' collapsed'
63
+ }}
64
+ </text>
65
+ </view>
66
+ </view>
67
+
68
+ <!-- Nested comments -->
69
+ <template v-if="open && comment.comments">
70
+ <Comment
71
+ v-for="child in comment.comments"
72
+ :key="child.id"
73
+ :comment="child"
74
+ :depth="(depth ?? 0) + 1"
75
+ />
76
+ </template>
77
+ </view>
78
+ </template>
@@ -0,0 +1,35 @@
1
+ <script setup lang="ts">
2
+ import { RouterLink } from 'vue-router';
3
+
4
+ defineProps<{
5
+ to: string;
6
+ label: string;
7
+ }>();
8
+ </script>
9
+
10
+ <template>
11
+ <RouterLink :to="to" custom v-slot="{ navigate, isActive }">
12
+ <view
13
+ :style="{
14
+ display: 'flex',
15
+ alignItems: 'center',
16
+ justifyContent: 'center',
17
+ }"
18
+ >
19
+ <text
20
+ @tap="navigate"
21
+ :style="{
22
+ color: '#fff',
23
+ fontSize: '15px',
24
+ fontWeight: isActive ? '600' : '300',
25
+ letterSpacing: '0.075em',
26
+ lineHeight: '24px',
27
+ marginRight: '1em',
28
+ whiteSpace: 'nowrap',
29
+ }"
30
+ >
31
+ {{ label }}
32
+ </text>
33
+ </view>
34
+ </RouterLink>
35
+ </template>
@@ -0,0 +1,80 @@
1
+ <script setup lang="ts">
2
+ import { useRouter } from 'vue-router';
3
+ import type { FeedItem } from '../api';
4
+ import { toHost, isAbsoluteUrl } from '../utils';
5
+
6
+ const props = defineProps<{
7
+ item: FeedItem;
8
+ }>();
9
+
10
+ const router = useRouter();
11
+ const host = toHost(props.item.url);
12
+
13
+ function goToItem() {
14
+ router.push(`/item/${props.item.id}`);
15
+ }
16
+
17
+ function goToUser() {
18
+ router.push(`/user/${props.item.user}`);
19
+ }
20
+ </script>
21
+
22
+ <template>
23
+ <view
24
+ class="bg-hn-card flex flex-row border-b border-hn-border"
25
+ :style="{ padding: '20px 16px 20px 0' }"
26
+ >
27
+ <!-- Score: 80px wide, centered -->
28
+ <view :style="{ width: '80px', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }">
29
+ <text :style="{ color: '#3eaf7c', fontSize: '1.1em', fontWeight: '700' }">
30
+ {{ item.points }}
31
+ </text>
32
+ </view>
33
+
34
+ <!-- Content -->
35
+ <view class="flex flex-col flex-1">
36
+ <!-- Title -->
37
+ <text
38
+ :style="{ color: '#2e495e', fontSize: '15px', lineHeight: '20px' }"
39
+ @tap="goToItem"
40
+ >
41
+ {{ item.title }}
42
+ </text>
43
+
44
+ <!-- Host -->
45
+ <text
46
+ v-if="isAbsoluteUrl(item.url)"
47
+ :style="{ color: '#595959', fontSize: '0.85em', marginTop: '2px' }"
48
+ >
49
+ ({{ host }})
50
+ </text>
51
+
52
+ <!-- Meta line -->
53
+ <view class="flex flex-row flex-wrap" :style="{ marginTop: '4px', gap: '4px' }">
54
+ <text v-if="item.type !== 'job'" :style="{ color: '#595959', fontSize: '0.85em' }">
55
+ by
56
+ </text>
57
+ <text
58
+ v-if="item.type !== 'job'"
59
+ :style="{ color: '#595959', fontSize: '0.85em', textDecorationLine: 'underline' }"
60
+ @tap="goToUser"
61
+ >
62
+ {{ item.user }}
63
+ </text>
64
+ <text :style="{ color: '#595959', fontSize: '0.85em' }">
65
+ {{ item.time_ago }}
66
+ </text>
67
+ <text v-if="item.type !== 'job'" :style="{ color: '#595959', fontSize: '0.85em' }">
68
+ |
69
+ </text>
70
+ <text
71
+ v-if="item.type !== 'job'"
72
+ :style="{ color: '#595959', fontSize: '0.85em', textDecorationLine: 'underline' }"
73
+ @tap="goToItem"
74
+ >
75
+ {{ item.comments_count }} comments
76
+ </text>
77
+ </view>
78
+ </view>
79
+ </view>
80
+ </template>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ show: boolean;
4
+ }>();
5
+ </script>
6
+
7
+ <template>
8
+ <view
9
+ v-if="show"
10
+ :style="{
11
+ width: '28px',
12
+ height: '28px',
13
+ borderRadius: '14px',
14
+ borderWidth: '3px',
15
+ borderColor: '#eee',
16
+ borderTopColor: '#3eaf7c',
17
+ animationName: 'spin',
18
+ animationDuration: '0.8s',
19
+ animationTimingFunction: 'linear',
20
+ animationIterationCount: 'infinite',
21
+ }"
22
+ />
23
+ </template>
24
+
25
+ <style>
26
+ @keyframes spin {
27
+ from { transform: rotate(0deg); }
28
+ to { transform: rotate(360deg); }
29
+ }
30
+ </style>
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { createApp } from 'vue-lynx';
2
+ import { createPinia } from 'pinia';
3
+ import { VueQueryPlugin } from '@tanstack/vue-query';
4
+
5
+ import router from './router';
6
+ import App from './App.vue';
7
+
8
+ const app = createApp(App);
9
+ app.use(createPinia());
10
+ app.use(VueQueryPlugin);
11
+ app.use(router);
12
+
13
+ router.push('/');
14
+
15
+ app.mount();
@@ -0,0 +1,161 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue-lynx';
3
+ import { useRouter } from 'vue-router';
4
+ import { useQuery, useQueryClient, keepPreviousData } from '@tanstack/vue-query';
5
+ import { fetchFeed } from '../api';
6
+ import NewsItem from '../components/NewsItem.vue';
7
+ import Spinner from '../components/Spinner.vue';
8
+
9
+ const props = defineProps<{
10
+ feed: string;
11
+ page: number;
12
+ maxPage: number;
13
+ }>();
14
+
15
+ const router = useRouter();
16
+ const queryClient = useQueryClient();
17
+
18
+ const { data: items, isLoading, isFetching, isError } = useQuery({
19
+ queryKey: computed(() => ['feed', props.feed, props.page]),
20
+ queryFn: () => fetchFeed(props.feed, props.page),
21
+ staleTime: 5 * 60 * 1000,
22
+ placeholderData: keepPreviousData,
23
+ });
24
+
25
+ // Track slide direction: "next" slides left, "prev" slides right
26
+ const slideDirection = ref<'slide-left' | 'slide-right'>('slide-left');
27
+
28
+ // Prefetch adjacent pages + track direction
29
+ watch(
30
+ () => [props.feed, props.page] as const,
31
+ ([feed, page], old) => {
32
+ // Determine slide direction from page change
33
+ if (old) {
34
+ const [oldFeed, oldPage] = old;
35
+ slideDirection.value = (feed !== oldFeed || page >= oldPage) ? 'slide-left' : 'slide-right';
36
+ }
37
+
38
+ if (page < props.maxPage) {
39
+ queryClient.prefetchQuery({
40
+ queryKey: ['feed', feed, page + 1],
41
+ queryFn: () => fetchFeed(feed, page + 1),
42
+ staleTime: 5 * 60 * 1000,
43
+ });
44
+ }
45
+ if (page > 1) {
46
+ queryClient.prefetchQuery({
47
+ queryKey: ['feed', feed, page - 1],
48
+ queryFn: () => fetchFeed(feed, page - 1),
49
+ staleTime: 5 * 60 * 1000,
50
+ });
51
+ }
52
+ },
53
+ { immediate: true },
54
+ );
55
+
56
+ const hasPrev = computed(() => props.page > 1);
57
+ const hasNext = computed(() => props.page < props.maxPage);
58
+
59
+ function goPrev() {
60
+ if (hasPrev.value) {
61
+ router.push(`/${props.feed}/${props.page - 1}`);
62
+ }
63
+ }
64
+
65
+ function goNext() {
66
+ if (hasNext.value) {
67
+ router.push(`/${props.feed}/${props.page + 1}`);
68
+ }
69
+ }
70
+ </script>
71
+
72
+ <template>
73
+ <view class="flex flex-col flex-1">
74
+ <!-- Pagination nav -->
75
+ <view
76
+ class="bg-hn-card flex flex-row items-center justify-center border-b border-hn-border"
77
+ :style="{ padding: '15px 30px', gap: '1em' }"
78
+ >
79
+ <text
80
+ :style="{
81
+ fontSize: '15px',
82
+ color: hasPrev ? '#3eaf7c' : '#595959',
83
+ opacity: hasPrev ? 1 : 0.5,
84
+ }"
85
+ @tap="goPrev"
86
+ >
87
+ &lt; prev
88
+ </text>
89
+ <text :style="{ fontSize: '15px', color: '#2e495e' }">
90
+ {{ page }}/{{ maxPage }}
91
+ </text>
92
+ <text
93
+ :style="{
94
+ fontSize: '15px',
95
+ color: hasNext ? '#3eaf7c' : '#595959',
96
+ opacity: hasNext ? 1 : 0.5,
97
+ }"
98
+ @tap="goNext"
99
+ >
100
+ more &gt;
101
+ </text>
102
+ </view>
103
+
104
+ <!-- Item list -->
105
+ <scroll-view class="flex-1" scroll-orientation="vertical">
106
+ <!-- Full loading (no cached data) -->
107
+ <view v-if="isLoading" class="items-center" :style="{ padding: '40px' }">
108
+ <Spinner :show="true" />
109
+ </view>
110
+
111
+ <view v-else-if="isError" class="items-center" :style="{ padding: '40px' }">
112
+ <text :style="{ fontSize: '15px', color: '#e53e3e' }">Failed to load stories.</text>
113
+ </view>
114
+
115
+ <template v-else>
116
+ <!-- Background refetch indicator -->
117
+ <view v-if="isFetching && !isLoading" class="items-center" :style="{ paddingTop: '12px' }">
118
+ <Spinner :show="true" />
119
+ </view>
120
+
121
+ <!-- Slide transition: direction flips based on prev/next -->
122
+ <Transition :name="slideDirection" mode="out-in" :duration="300">
123
+ <view class="bg-hn-card" :key="feed + '-' + page" :style="{ marginTop: '10px' }">
124
+ <NewsItem
125
+ v-for="item in items"
126
+ :key="item.id"
127
+ :item="item"
128
+ />
129
+ </view>
130
+ </Transition>
131
+ </template>
132
+ </scroll-view>
133
+ </view>
134
+ </template>
135
+
136
+ <style>
137
+ /* Slide left: next page (enter from right, leave to left) */
138
+ .slide-left-enter-active,
139
+ .slide-left-leave-active,
140
+ .slide-right-enter-active,
141
+ .slide-right-leave-active {
142
+ transition: opacity 300ms cubic-bezier(0.55, 0, 0.1, 1), transform 300ms cubic-bezier(0.55, 0, 0.1, 1);
143
+ }
144
+ .slide-left-enter-from {
145
+ opacity: 0;
146
+ transform: translateX(30px);
147
+ }
148
+ .slide-left-leave-to {
149
+ opacity: 0;
150
+ transform: translateX(-30px);
151
+ }
152
+ /* Slide right: prev page (enter from left, leave to right) */
153
+ .slide-right-enter-from {
154
+ opacity: 0;
155
+ transform: translateX(-30px);
156
+ }
157
+ .slide-right-leave-to {
158
+ opacity: 0;
159
+ transform: translateX(30px);
160
+ }
161
+ </style>
@@ -0,0 +1,109 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue-lynx';
3
+ import { useRoute, useRouter } from 'vue-router';
4
+ import { useQuery } from '@tanstack/vue-query';
5
+ import { fetchItem } from '../api';
6
+ import { toHost, isAbsoluteUrl } from '../utils';
7
+ import Comment from '../components/Comment.vue';
8
+ import Spinner from '../components/Spinner.vue';
9
+
10
+ const route = useRoute();
11
+ const router = useRouter();
12
+
13
+ const itemId = computed(() => Number(route.params.id));
14
+
15
+ const { data: item, isLoading, isError } = useQuery({
16
+ queryKey: computed(() => ['item', itemId.value]),
17
+ queryFn: () => fetchItem(itemId.value),
18
+ staleTime: 60 * 1000,
19
+ });
20
+
21
+ const host = computed(() => item.value ? toHost(item.value.url) : '');
22
+
23
+ function goToUser() {
24
+ if (item.value) {
25
+ router.push(`/user/${item.value.user}`);
26
+ }
27
+ }
28
+
29
+ function goBack() {
30
+ router.back();
31
+ }
32
+ </script>
33
+
34
+ <template>
35
+ <scroll-view class="flex-1" scroll-orientation="vertical">
36
+ <!-- Loading -->
37
+ <view v-if="isLoading" class="items-center" :style="{ padding: '40px' }">
38
+ <Spinner :show="true" />
39
+ </view>
40
+
41
+ <!-- Error -->
42
+ <view v-else-if="isError" class="items-center" :style="{ padding: '40px' }">
43
+ <text :style="{ fontSize: '15px', color: '#e53e3e' }">Failed to load item.</text>
44
+ </view>
45
+
46
+ <template v-else-if="item">
47
+ <!-- Item header card -->
48
+ <view class="bg-hn-card flex flex-col" :style="{ padding: '1.8em 2em 1em' }">
49
+ <!-- Back link -->
50
+ <text
51
+ :style="{ color: '#3eaf7c', fontSize: '13px', marginBottom: '12px' }"
52
+ @tap="goBack"
53
+ >
54
+ &lt; Back
55
+ </text>
56
+
57
+ <!-- Title: 1.5em like reference h1 -->
58
+ <text :style="{ color: '#2e495e', fontSize: '1.5em', fontWeight: 'bold', lineHeight: '1.3em' }">
59
+ {{ item.title }}
60
+ </text>
61
+
62
+ <!-- Host -->
63
+ <text
64
+ v-if="isAbsoluteUrl(item.url)"
65
+ :style="{ color: '#595959', fontSize: '0.85em', marginTop: '4px' }"
66
+ >
67
+ {{ host }}
68
+ </text>
69
+
70
+ <!-- Meta -->
71
+ <view class="flex flex-row flex-wrap" :style="{ marginTop: '8px', gap: '4px' }">
72
+ <text :style="{ color: '#595959', fontSize: '0.85em' }">
73
+ {{ item.points }} points | by
74
+ </text>
75
+ <text
76
+ :style="{ color: '#595959', fontSize: '0.85em', textDecorationLine: 'underline' }"
77
+ @tap="goToUser"
78
+ >
79
+ {{ item.user }}
80
+ </text>
81
+ <text :style="{ color: '#595959', fontSize: '0.85em' }">
82
+ {{ item.time_ago }}
83
+ </text>
84
+ </view>
85
+ </view>
86
+
87
+ <!-- Comments section -->
88
+ <view class="bg-hn-card flex flex-col" :style="{ marginTop: '10px', padding: '0 2em 0.5em' }">
89
+ <!-- Comments header -->
90
+ <view class="flex flex-row items-center" :style="{ padding: '1em 0', gap: '8px' }">
91
+ <text :style="{ fontSize: '1.1em', color: '#2e495e' }">
92
+ {{
93
+ item.comments && item.comments.length
94
+ ? item.comments.length + ' comments'
95
+ : 'No comments yet'
96
+ }}
97
+ </text>
98
+ </view>
99
+
100
+ <Comment
101
+ v-for="comment in item.comments"
102
+ :key="comment.id"
103
+ :comment="comment"
104
+ :depth="0"
105
+ />
106
+ </view>
107
+ </template>
108
+ </scroll-view>
109
+ </template>