baseline: 保留 07 风格重写前的 Vue 前端原件
This commit is contained in:
commit
edee708802
63 files changed
+4404
No files matched your search
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
// 页面管理页
|
||||
import PagesManager from '@/components/dashboard/PagesManager.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PagesManager title="Pages" item-label="Page" />
|
||||
</template>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
// 文章管理页
|
||||
import PagesManager from '@/components/dashboard/PagesManager.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PagesManager title="Posts" item-label="Post" />
|
||||
</template>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user