baseline: 保留 07 风格重写前的 Vue 前端原件

This commit is contained in:
Sakurasan
2026-09-20 20:48:00 +08:00
commit edee708802
63 changed files with 4404 additions and 0 deletions
+137
View File
@@ -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>
+32
View File
@@ -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">
&copy;
<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>
+25
View File
@@ -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>