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

This commit is contained in:
Sakurasan committed 2026-09-20 20:48:00 +08:00
commit edee708802
63 files changed
+4404

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
<script setup lang="ts">
// 关于页
import Logo from '@/components/common/Logo.vue'
import PromotionLinks from '@/components/home/PromotionLinks.vue'
</script>
<template>
<div class="max-w-screen-lg mx-auto py-8">
<div class="text-center py-10 space-y-6">
<div class="inline-block size-16 mx-auto">
<Logo :size="64" />
</div>
<h1 class="text-4xl font-extrabold">About xLog</h1>
<p class="text-zinc-500 max-w-xl mx-auto leading-loose">
xLog is a decentralized blog platform built on Crossbell. It gives every
creator a permanent home on the open web — own your content, connect with
readers, and tip creators across the network.
</p>
<div class="max-w-sm mx-auto">
<PromotionLinks />
</div>
</div>
</div>
</template>
+78
View File
@@ -0,0 +1,78 @@
<script setup lang="ts">
// 仪表盘主页:统计卡片 + 最近文章
import { computed } from 'vue'
import DashboardMain from '@/components/dashboard/DashboardMain.vue'
import { allPosts } from '@/mock/data'
const stats = computed(() => [
{
label: 'Posts',
value: String(allPosts.length),
icon: 'i-mingcute-news-line',
},
{
label: 'Total Views',
value: allPosts.reduce((sum, p) => sum + p.views, 0).toLocaleString(),
icon: 'i-mingcute-eye-line',
},
{
label: 'Followers',
value: '1,284',
icon: 'i-mingcute-user-follow-line',
},
{
label: 'CSB Balance',
value: '1,024',
icon: 'i-mingcute-pig-money-line',
},
])
const recent = computed(() =>
[...allPosts]
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
.slice(0, 5),
)
</script>
<template>
<DashboardMain title="Dashboard">
<!-- 统计卡片 -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div
v-for="stat in stats"
:key="stat.label"
class="border rounded-xl p-4 flex items-center space-x-3"
>
<span class="size-10 rounded-lg bg-accent/10 text-accent flex items-center justify-center shrink-0">
<i :class="stat.icon" class="text-xl" />
</span>
<div class="min-w-0">
<div class="text-2xl font-bold">{{ stat.value }}</div>
<div class="text-zinc-500 text-sm truncate">{{ stat.label }}</div>
</div>
</div>
</div>
<!-- 最近文章 -->
<div class="mt-8">
<h3 class="font-bold text-lg mb-3">Recent Posts</h3>
<div class="border rounded-xl divide-y">
<RouterLink
v-for="post in recent"
:key="post.id"
:to="`/post/${post.siteId}/${post.slug}`"
class="flex items-center px-4 py-3 hover:bg-zinc-50 transition-colors"
>
<img
:src="post.cover"
:alt="post.title"
class="w-12 h-8 rounded object-cover mr-3 shrink-0"
/>
<span class="flex-1 min-w-0 truncate font-medium">{{ post.title }}</span>
<span class="text-zinc-400 text-sm ml-3 shrink-0">{{ post.views }} views</span>
</RouterLink>
</div>
</div>
</DashboardMain>
</template>
+45
View File
@@ -0,0 +1,45 @@
<script setup lang="ts">
// 简易 Markdown 编辑器(占位实现,仅用于文章管理页跳转)
import { computed, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import DashboardMain from '@/components/dashboard/DashboardMain.vue'
import { allPosts } from '@/mock/data'
const route = useRoute()
const post = computed(() => {
const id = route.query.id as string | undefined
return allPosts.find((p) => p.id === id) ?? allPosts[0]
})
const title = ref('')
const content = ref('')
watch(
post,
(p) => {
if (p) {
title.value = p.title
content.value = p.content
}
},
{ immediate: true },
)
</script>
<template>
<DashboardMain title="Editor">
<div class="space-y-4 max-w-screen-lg">
<input v-model="title" class="input is-block text-lg font-bold" placeholder="Title" />
<textarea
v-model="content"
class="input is-block min-h-[400px] py-3 font-mono text-sm resize-y"
placeholder="Write in Markdown..."
/>
<div class="flex justify-end space-x-3">
<button class="button is-secondary">Save Draft</button>
<button class="button is-primary">Publish</button>
</div>
</div>
</DashboardMain>
</template>
+30
View File
@@ -0,0 +1,30 @@
<script setup lang="ts">
// 主页:活动标签 + 文章 Grid + 右侧边栏
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import HomeActivitiesTabs from '@/components/home/HomeActivitiesTabs.vue'
import HomeFeed from '@/components/home/HomeFeed.vue'
import HomeSidebar from '@/components/home/HomeSidebar.vue'
const route = useRoute()
const feedType = computed(() => {
const path = route.path.replace(/^\//, '')
if (['shorts', 'latest', 'hottest', 'following'].includes(path)) {
return path as 'shorts' | 'latest' | 'hottest' | 'following'
}
return 'featured'
})
</script>
<template>
<!-- 主栏 -->
<div class="flex-1 min-w-[300px]">
<HomeActivitiesTabs />
<HomeFeed :type="feedType" />
</div>
<!-- 右侧边栏(桌面端显示) -->
<HomeSidebar />
</template>
+15
View File
@@ -0,0 +1,15 @@
<script setup lang="ts">
// 404 页
import Logo from '@/components/common/Logo.vue'
</script>
<template>
<div class="min-h-screen bg-white flex flex-col items-center justify-center py-20 space-y-6">
<div class="inline-block size-16">
<Logo :size="64" />
</div>
<h1 class="text-6xl font-extrabold">404</h1>
<p class="text-zinc-500">This page could not be found.</p>
<RouterLink to="/" class="button is-primary">Back to Home</RouterLink>
</div>
</template>
+8
View File
@@ -0,0 +1,8 @@
<script setup lang="ts">
// 页面管理页
import PagesManager from '@/components/dashboard/PagesManager.vue'
</script>
<template>
<PagesManager title="Pages" item-label="Page" />
</template>
+46
View File
@@ -0,0 +1,46 @@
<script setup lang="ts">
// 文章详情页:标题 + 元信息 + AI 摘要 + Markdown 正文 + 互动区 + 评论
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import EmptyState from '@/components/common/EmptyState.vue'
import MarkdownContent from '@/components/common/MarkdownContent.vue'
import PostFooter from '@/components/site/PostFooter.vue'
import PostMeta from '@/components/site/PostMeta.vue'
import PostTitle from '@/components/site/PostTitle.vue'
import { getPost } from '@/mock/data'
const route = useRoute()
const post = computed(() =>
getPost(route.params.site as string, route.params.slug as string),
)
</script>
<template>
<div class="max-w-screen-md mx-auto">
<article v-if="post">
<!-- 文章标题 -->
<PostTitle :post="post" />
<!-- 文章元信息 -->
<PostMeta :post="post" />
<!-- AI 摘要 -->
<div class="xlog-post-summary border rounded-xl mt-5 p-4 space-y-2">
<div class="font-bold text-zinc-700 flex items-center">
<i class="i-mingcute-sparkles-line mr-2 text-lg" />
AI-generated summary
</div>
<div class="text-zinc-500 leading-loose text-sm">{{ post.excerpt }}</div>
</div>
<!-- Markdown 正文 -->
<MarkdownContent :content="post.content" class="mt-10" />
<!-- 互动区 + 评论区 -->
<PostFooter :post="post" />
</article>
<EmptyState v-else icon="i-mingcute-file-line" text="Post not found" />
</div>
</template>
+8
View File
@@ -0,0 +1,8 @@
<script setup lang="ts">
// 文章管理页
import PagesManager from '@/components/dashboard/PagesManager.vue'
</script>
<template>
<PagesManager title="Posts" item-label="Post" />
</template>
+37
View File
@@ -0,0 +1,37 @@
<script setup lang="ts">
// 搜索页:基于 URL query 过滤 mock 文章
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import EmptyState from '@/components/common/EmptyState.vue'
import PostCard from '@/components/common/PostCard.vue'
import { allPosts } from '@/mock/data'
const route = useRoute()
const keyword = computed(() => String(route.query.q ?? '').trim().toLowerCase())
const results = computed(() => {
if (!keyword.value) return []
return allPosts.filter((post) =>
[post.title, post.excerpt, ...post.tags, post.author.name]
.join(' ')
.toLowerCase()
.includes(keyword.value),
)
})
</script>
<template>
<div class="flex-1 min-w-[300px]">
<div class="flex mb-8 border-b">
<h2 class="inline-flex items-center h-10 whitespace-nowrap cursor-pointer text-accent font-medium border-b-2 border-accent">
Results for "{{ keyword }}"
</h2>
</div>
<div v-if="results.length" class="grid gap-3 sm:gap-6 grid-cols-1 sm:grid-cols-3 my-8">
<PostCard v-for="post in results" :key="post.id" :post="post" />
</div>
<EmptyState v-else :text="keyword ? 'No results found' : 'Type something to search'" />
</div>
</template>
+77
View File
@@ -0,0 +1,77 @@
<script setup lang="ts">
// 设置页:General / Social / Navigation / Domains / Custom CSS
import { computed, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import EmptyState from '@/components/common/EmptyState.vue'
import Tabs from '@/components/ui/Tabs.vue'
const route = useRoute()
const tab = computed(() => (route.params.tab as string) || 'general')
const tabs = [
{ key: 'general', label: 'General', to: '/dashboard/settings/general' },
{ key: 'social', label: 'Social Platforms', to: '/dashboard/settings/social' },
{ key: 'navigation', label: 'Navigation', to: '/dashboard/settings/navigation' },
{ key: 'domains', label: 'Domains', to: '/dashboard/settings/domains' },
{ key: 'css', label: 'Custom CSS', to: '/dashboard/settings/css' },
]
const form = ref({ name: 'MyBlog', description: 'A blog about code and life.' })
const saved = ref(false)
watch(form, () => (saved.value = false), { deep: true })
function save() {
saved.value = true
window.setTimeout(() => (saved.value = false), 2000)
}
</script>
<template>
<div class="min-w-[270px] relative p-5 md:px-10 md:py-8 min-h-full flex flex-col">
<header class="mb-8">
<h2 class="text-2xl font-bold">Settings</h2>
</header>
<div>
<!-- 设置子标签 -->
<Tabs :items="tabs" :active-key="tab" />
<!-- 设置内容 -->
<div class="max-w-screen-md">
<!-- General -->
<div v-if="tab === 'general'" class="space-y-6">
<div>
<label class="block mb-2 font-bold text-gray-700">Site Name</label>
<input v-model="form.name" class="input is-block" />
</div>
<div>
<label class="block mb-2 font-bold text-gray-700">Description</label>
<textarea
v-model="form.description"
class="input is-block min-h-[74px] py-3 resize-y"
/>
</div>
<div>
<label class="block mb-2 font-bold text-gray-700">Avatar</label>
<button class="button is-secondary">
<i class="i-mingcute-camera-line mr-2" />
Upload Avatar
</button>
</div>
<button class="button is-primary" @click="save">
{{ saved ? 'Saved ✓' : 'Save' }}
</button>
</div>
<!-- 其他设置(占位) -->
<EmptyState
v-else
:icon="'i-mingcute-settings-3-line'"
:text="`${tab} settings are coming soon`"
/>
</div>
</div>
</div>
</template>
+48
View File
@@ -0,0 +1,48 @@
<script setup lang="ts">
// 站点归档页:按月份分组的文章列表
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import EmptyState from '@/components/common/EmptyState.vue'
import Time from '@/components/common/Time.vue'
import { allPosts } from '@/mock/data'
const route = useRoute()
const grouped = computed(() => {
const posts = allPosts
.filter((p) => p.siteId === route.params.site)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
const map = new Map<string, typeof posts>()
for (const post of posts) {
const key = post.date.slice(0, 7) // YYYY-MM
map.set(key, [...(map.get(key) ?? []), post])
}
return [...map.entries()]
})
</script>
<template>
<div class="max-w-screen-md mx-auto">
<div v-if="grouped.length">
<div v-for="[month, posts] in grouped" :key="month" class="mb-8">
<h3 class="text-xl font-bold text-zinc-900 mb-3">{{ month }}</h3>
<ul class="border-t divide-y">
<li v-for="post in posts" :key="post.id">
<RouterLink
:to="`/post/${post.siteId}/${post.slug}`"
class="flex items-center justify-between py-3 hover:text-accent transition-colors"
>
<span class="truncate">{{ post.title }}</span>
<span class="text-zinc-400 text-sm ml-3 shrink-0">
<Time :date="post.date" />
</span>
</RouterLink>
</li>
</ul>
</div>
</div>
<EmptyState v-else text="No posts yet" />
</div>
</template>
+30
View File
@@ -0,0 +1,30 @@
<script setup lang="ts">
// 站点主页:该创作者的公开文章列表
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import EmptyState from '@/components/common/EmptyState.vue'
import PostCard from '@/components/common/PostCard.vue'
import { allPosts, sites } from '@/mock/data'
const route = useRoute()
const site = computed(
() => sites.find((s) => s.handle === (route.params.site as string)) ?? null,
)
const posts = computed(() => allPosts.filter((p) => p.siteId === route.params.site))
</script>
<template>
<div v-if="site">
<div
v-if="posts.length"
class="grid gap-3 sm:gap-6 grid-cols-1 sm:grid-cols-2 my-8"
>
<PostCard v-for="post in posts" :key="post.id" :post="post" />
</div>
<EmptyState v-else text="No posts yet" />
</div>
<EmptyState v-else icon="i-mingcute-file-line" text="Site not found" />
</template>