baseline: 保留 07 风格重写前的 Vue 前端原件
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
// 文章互动区(点赞/打赏/分享)+ 评论区
|
||||
import { ref } from 'vue'
|
||||
|
||||
import Avatar from '@/components/common/Avatar.vue'
|
||||
import Time from '@/components/common/Time.vue'
|
||||
import type { Comment, Post } from '@/mock/data'
|
||||
import { comments } from '@/mock/data'
|
||||
|
||||
const props = defineProps<{ post: Post }>()
|
||||
|
||||
// 点赞
|
||||
const liked = ref(false)
|
||||
const likeCount = ref(props.post.likes)
|
||||
function toggleLike() {
|
||||
liked.value = !liked.value
|
||||
likeCount.value += liked.value ? 1 : -1
|
||||
}
|
||||
|
||||
// 打赏
|
||||
const tipped = ref(false)
|
||||
|
||||
// 分享(复制链接)
|
||||
const shared = ref(false)
|
||||
async function share() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(window.location.href)
|
||||
} catch {
|
||||
/* ignore clipboard errors */
|
||||
}
|
||||
shared.value = true
|
||||
window.setTimeout(() => (shared.value = false), 2000)
|
||||
}
|
||||
|
||||
// 评论
|
||||
const list = ref<Comment[]>(comments)
|
||||
const draft = ref('')
|
||||
function submitComment() {
|
||||
const content = draft.value.trim()
|
||||
if (!content) return
|
||||
list.value.unshift({
|
||||
id: `c-${list.value.length + 1}`,
|
||||
author: {
|
||||
handle: 'you',
|
||||
name: 'You',
|
||||
avatar: '',
|
||||
},
|
||||
content,
|
||||
date: new Date().toISOString(),
|
||||
likes: 0,
|
||||
})
|
||||
draft.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 互动按钮 -->
|
||||
<div
|
||||
class="xlog-reactions flex fill-gray-400 text-gray-500 sm:items-center space-x-6 sm:space-x-10 mt-14 mb-12"
|
||||
>
|
||||
<button class="button is-like" :class="{ 'text-[#f91880]': liked }" @click="toggleLike">
|
||||
<i class="i-mingcute-thumb-up-2-fill mr-2" />
|
||||
<span>{{ likeCount }}</span>
|
||||
</button>
|
||||
<button class="button is-tip" @click="tipped = !tipped">
|
||||
<i class="i-mingcute-pig-money-line mr-2" />
|
||||
<span>{{ tipped ? 'Tipped ✓' : 'Tip' }}</span>
|
||||
</button>
|
||||
<button class="button is-share" @click="share">
|
||||
<i class="i-mingcute-share-forward-line mr-2" />
|
||||
<span>{{ shared ? 'Copied!' : 'Share' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 评论区 -->
|
||||
<div class="xlog-comment mb-10" id="comments">
|
||||
<div class="xlog-comment-count border-b pb-2 mb-6 font-bold">
|
||||
Comments ({{ list.length }})
|
||||
</div>
|
||||
|
||||
<!-- 评论输入 -->
|
||||
<div class="xlog-comment-input flex mb-6">
|
||||
<span class="mr-3">
|
||||
<Avatar name="You" :size="45" />
|
||||
</span>
|
||||
<div class="flex-1">
|
||||
<textarea
|
||||
v-model="draft"
|
||||
class="input is-block min-h-[74px] py-3 resize-y"
|
||||
placeholder="Write a comment..."
|
||||
@keydown.meta.enter="submitComment"
|
||||
@keydown.ctrl.enter="submitComment"
|
||||
/>
|
||||
<div class="flex justify-end mt-2">
|
||||
<button
|
||||
class="button is-primary is-sm"
|
||||
:class="{ 'is-loading': false }"
|
||||
:disabled="!draft.trim()"
|
||||
@click="submitComment"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评论列表 -->
|
||||
<div class="xlog-comment-list">
|
||||
<div
|
||||
v-for="comment in list"
|
||||
:key="comment.id"
|
||||
class="xlog-comment-item mt-6 flex space-x-3"
|
||||
>
|
||||
<Avatar :src="comment.author.avatar" :name="comment.author.name" :size="40" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<span class="font-bold">{{ comment.author.name }}</span>
|
||||
<span class="text-zinc-400 text-xs">
|
||||
<Time :date="comment.date" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-zinc-600 mt-1 leading-relaxed break-words">
|
||||
{{ comment.content }}
|
||||
</div>
|
||||
<button
|
||||
class="text-zinc-400 text-xs mt-1 hover:text-accent inline-flex items-center"
|
||||
>
|
||||
<i class="i-mingcute-thumb-up-2-line mr-1" />
|
||||
{{ comment.likes }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { Post } from '@/mock/data'
|
||||
|
||||
import Time from '@/components/common/Time.vue'
|
||||
|
||||
defineProps<{ post: Post }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="xlog-post-meta">
|
||||
<div class="text-zinc-400 mt-5 space-x-5 flex items-center justify-center">
|
||||
<!-- 发布日期 -->
|
||||
<Time class="xlog-post-date whitespace-nowrap" :date="post.date" />
|
||||
<!-- 标签 -->
|
||||
<span class="xlog-post-tags space-x-1 truncate min-w-0">
|
||||
<a
|
||||
v-for="tag in post.tags"
|
||||
:key="tag"
|
||||
class="hover:text-accent"
|
||||
:href="`/tag/${tag}`"
|
||||
>
|
||||
#{{ tag }}
|
||||
</a>
|
||||
</span>
|
||||
<!-- 阅读量 -->
|
||||
<span class="xlog-post-views inline-flex items-center">
|
||||
<i class="i-mingcute-eye-line mr-[2px]" />
|
||||
<span>{{ post.views }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { Post } from '@/mock/data'
|
||||
|
||||
defineProps<{ post: Post }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2
|
||||
class="xlog-post-title mb-8 flex items-center justify-center text-center relative text-4xl font-extrabold"
|
||||
>
|
||||
<span>{{ post.title }}</span>
|
||||
</h2>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
// 站点页 Footer:版权 + 语言/暗色切换
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import DarkModeSwitch from '@/components/common/DarkModeSwitch.vue'
|
||||
import LanguageSwitch from '@/components/common/LanguageSwitch.vue'
|
||||
import Logo from '@/components/common/Logo.vue'
|
||||
import { sites } from '@/mock/data'
|
||||
|
||||
const route = useRoute()
|
||||
const site = computed(
|
||||
() => sites.find((s) => s.handle === (route.params.site as string)) ?? sites[0],
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="text-zinc-500 border-t">
|
||||
<div class="max-w-screen-lg mx-auto px-5 py-10">
|
||||
<div
|
||||
class="text-xs sm:flex justify-between sm:space-x-5 sm:space-y-0 space-y-5 sm:items-center"
|
||||
>
|
||||
<div class="font-medium text-base">
|
||||
©
|
||||
<RouterLink :to="`/site/${site.handle}`" class="hover:text-accent">
|
||||
{{ site.name }}
|
||||
</RouterLink>
|
||||
· powered by
|
||||
<RouterLink to="/" class="inline-flex items-center align-middle hover:text-accent">
|
||||
<span class="inline-block size-5 mr-1">
|
||||
<Logo :size="20" />
|
||||
</span>
|
||||
xLog
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="flex gap-x-2 items-center justify-center">
|
||||
<LanguageSwitch />
|
||||
<DarkModeSwitch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
// 站点信息头:Banner + 头像 + 站点名/简介 + 关注按钮 + 导航
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { sites } from '@/mock/data'
|
||||
|
||||
import SiteTabs from './SiteTabs.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const site = computed(
|
||||
() => sites.find((s) => s.handle === (route.params.site as string)) ?? sites[0],
|
||||
)
|
||||
|
||||
const following = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="xlog-header border-b border-zinc-100 relative">
|
||||
<!-- 可选 Banner 图 -->
|
||||
<div v-if="site.banner" class="xlog-banner absolute inset-0 overflow-hidden">
|
||||
<img class="object-cover w-full h-full" :src="site.banner" alt="banner" />
|
||||
</div>
|
||||
|
||||
<div class="px-5 max-w-screen-lg mx-auto h-full relative flex items-center flex-col z-10">
|
||||
<div class="flex py-12 w-full">
|
||||
<div class="xlog-site-info flex space-x-6 sm:space-x-8 w-full">
|
||||
<!-- 头像 -->
|
||||
<img
|
||||
class="xlog-site-icon max-w-[100px] max-h-[100px] sm:max-w-none sm:max-h-none rounded-full bg-zinc-100"
|
||||
:src="site.avatar"
|
||||
width="150"
|
||||
height="150"
|
||||
:alt="`${site.name} avatar`"
|
||||
/>
|
||||
|
||||
<!-- 站点信息 -->
|
||||
<div class="flex-1 min-w-0 relative space-y-2 sm:space-y-3 min-h-[108px]">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1
|
||||
class="xlog-site-name text-3xl sm:text-4xl font-bold text-zinc-900 leading-snug break-words min-w-0"
|
||||
>
|
||||
{{ site.name }}
|
||||
</h1>
|
||||
<div class="ml-0 sm:ml-8 space-x-3 sm:space-x-4 flex items-center">
|
||||
<button
|
||||
class="button is-primary is-sm"
|
||||
@click="following = !following"
|
||||
>
|
||||
{{ following ? 'Following' : 'Follow' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 简介 -->
|
||||
<div
|
||||
class="xlog-site-description text-gray-500 leading-snug text-sm sm:text-base line-clamp-4 whitespace-pre-wrap"
|
||||
>
|
||||
{{ site.description }}
|
||||
</div>
|
||||
<!-- 关注数 -->
|
||||
<span class="text-sm text-zinc-400">{{ site.followers }} followers</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<div class="text-gray-500 flex items-center justify-between w-full mt-auto">
|
||||
<SiteTabs />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
// 站点头导航:Home / Archives
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import Tabs from '@/components/ui/Tabs.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const items = computed(() => {
|
||||
const site = route.params.site as string
|
||||
return [
|
||||
{ key: 'home', label: 'Home', to: `/site/${site}` },
|
||||
{ key: 'archives', label: 'Archives', to: `/site/${site}/archives` },
|
||||
]
|
||||
})
|
||||
|
||||
const activeKey = computed(() =>
|
||||
route.path.includes('/archives') ? 'archives' : 'home',
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tabs :items="items" :active-key="activeKey" class="border-none mb-0" />
|
||||
</template>
|
||||
Reference in New Issue
Block a user