作品展示页:前端展示 + 后台增删改查

- 新增 projects 表与迁移(唯一 slug 索引)
- 后端:公开 GET /api/projects,后台 GET/POST/PUT/DELETE /api/admin/projects
- 前端:/projects 双虚线边框卡片页(参考 diygod.cc/projects),后台管理页
- 左栏/手机顶栏/后台侧栏均加「作品」入口
- 补充项目 CRUD 单元测试与后台鉴权拦截测试
This commit is contained in:
Sakurasan
2026-09-22 01:47:59 +08:00
parent 2b53eb976b
commit 3238175ef0
14 changed files with 1029 additions and 1 deletions
+98
View File
@@ -0,0 +1,98 @@
<script setup>
import { onMounted, ref } from 'vue'
import LeftNav from '../components/LeftNav.vue'
import RightRail from '../components/RightRail.vue'
import ProjectCard from '../components/ProjectCard.vue'
import { publicApi } from '../api'
import { applyDocTitle } from '../site'
const projects = ref([])
const loading = ref(true)
const error = ref('')
onMounted(async () => {
try {
const data = await publicApi.projects()
projects.value = data.projects || []
} catch (e) {
error.value = e.message || '加载失败'
} finally {
loading.value = false
}
})
applyDocTitle('作品')
</script>
<template>
<div class="shell">
<div class="layout">
<LeftNav />
<main class="main">
<header class="head">
<h1 class="page-title">作品</h1>
<p class="sub">做过的一些东西。</p>
</header>
<div v-if="loading" class="loading">载入中…</div>
<div v-else-if="error" class="empty">{{ error }}</div>
<div v-else-if="!projects.length" class="empty">还没有作品。</div>
<div v-else class="grid">
<ProjectCard v-for="p in projects" :key="p.id" :project="p" />
</div>
</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;
}
.head {
position: sticky;
top: 0;
z-index: 10;
background-color: var(--paper);
backdrop-filter: blur(6px);
border-bottom: 1px solid var(--line);
padding: 14px 16px;
}
.page-title {
font-size: 20px;
}
.sub {
margin: 2px 0 0;
font-size: 13px;
color: var(--muted);
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
padding: 22px 16px;
}
@media (max-width: 620px) {
.grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 780px) {
.main {
border: 0;
}
}
</style>