MVP: 按 07 风格重写前端 + Go 后端落地(长文/短文、编辑器、后台管理)

This commit is contained in:
Sakurasan committed 2026-09-20 20:56:03 +08:00
1 parent edee708802
commit 4d8f2de3a4
96 files changed
+6005 -2602

No files matched your search

+210
View File
@@ -0,0 +1,210 @@
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import LeftNav from '../components/LeftNav.vue'
import RightRail from '../components/RightRail.vue'
import PostCard from '../components/PostCard.vue'
import { publicApi } from '../api'
import { site, applyDocTitle } from '../site'
const route = useRoute()
const router = useRouter()
const items = ref([])
const total = ref(0)
const loading = ref(true)
const error = ref('')
const kind = computed(() => route.query.kind || '')
const tag = computed(() => route.query.tag || '')
const page = computed(() => Number(route.query.page || 1))
const size = computed(() => site.posts_per_page || 10)
const tabs = [
{ label: '全部', value: '' },
{ label: '长文', value: 'long' },
{ label: '短文', value: 'short' }
]
async function load() {
loading.value = true
error.value = ''
try {
const data = await publicApi.posts({
kind: kind.value,
tag: tag.value,
page: page.value,
size: size.value
})
items.value = data.items || []
total.value = data.total || 0
} catch (e) {
error.value = e.message || '加载失败'
items.value = []
} finally {
loading.value = false
}
}
onMounted(load)
watch([kind, tag, page], load)
function setKind(value) {
router.push({ path: '/', query: { ...(value ? { kind: value } : {}), ...(tag.value ? { tag: tag.value } : {}) } })
}
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / size.value)))
const heading = computed(() => {
if (tag.value) return `#${tag.value}`
if (kind.value === 'long') return '长文'
if (kind.value === 'short') return '短文'
return '时间线'
})
applyDocTitle('')
</script>
<template>
<div class="shell">
<div class="layout">
<LeftNav />
<main class="main">
<header class="sticky-head">
<h1 class="page-title">{{ heading }}</h1>
<nav class="tabs">
<button
v-for="t in tabs"
:key="t.value"
class="tab"
:class="{ active: kind === t.value }"
@click="setKind(t.value)"
>
{{ t.label }}
</button>
<a v-if="tag" class="tab clear" href="#" @click.prevent="router.push({ path: '/' })">
清除标签 ×
</a>
</nav>
</header>
<div v-if="loading" class="loading">载入中…</div>
<div v-else-if="error" class="empty">{{ error }}(后端是否已启动?)</div>
<div v-else-if="!items.length" class="empty">
这里还空着。<RouterLink to="/admin">去后台写第一篇 →</RouterLink>
</div>
<template v-else>
<PostCard v-for="p in items" :key="p.id" :post="p" />
<nav v-if="pageCount > 1" class="pager">
<RouterLink
v-if="page > 1"
class="btn"
:to="{ path: '/', query: { ...route.query, page: page - 1 } }"
>
← 上一页
</RouterLink>
<span class="page-info">第 {{ page }} / {{ pageCount }} 页</span>
<RouterLink
v-if="page < pageCount"
class="btn"
:to="{ path: '/', query: { ...route.query, page: page + 1 } }"
>
下一页 →
</RouterLink>
</nav>
</template>
</main>
<RightRail />
</div>
</div>
</template>
<style scoped>
.main {
min-width: 0;
border-left: 1px solid var(--line);
border-right: 1px solid var(--line);
padding-bottom: 60px;
}
.sticky-head {
position: sticky;
top: 0;
z-index: 10;
background: rgba(250, 247, 241, 0.94);
backdrop-filter: blur(6px);
border-bottom: 1px solid var(--line);
padding: 14px 16px 0;
}
.page-title {
font-size: 20px;
}
.tabs {
display: flex;
gap: 4px;
margin-top: 6px;
}
.tab {
position: relative;
padding: 8px 14px 10px;
background: none;
border: 0;
cursor: pointer;
font-size: 14px;
color: var(--muted);
}
.tab:hover {
color: var(--ink);
background: var(--paper-sunken);
}
.tab.active {
color: var(--accent);
font-weight: 500;
}
.tab.active::after {
content: '';
position: absolute;
left: 14px;
right: 14px;
bottom: 0;
height: 2px;
background: var(--accent);
border-radius: 2px;
}
.tab.clear {
margin-left: auto;
font-size: 13px;
color: var(--faint);
}
.pager {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 22px 16px 0;
}
.page-info {
font-size: 13px;
color: var(--muted);
}
@media (max-width: 780px) {
.main {
border-left: 0;
border-right: 0;
}
}
</style>