MVP: 按 07 风格重写前端 + Go 后端落地(长文/短文、编辑器、后台管理)
This commit is contained in:
1 parent
edee708802
commit
4d8f2de3a4
96 files changed
+6005
-2602
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user