作品展示页:前端展示 + 后台增删改查
- 新增 projects 表与迁移(唯一 slug 索引) - 后端:公开 GET /api/projects,后台 GET/POST/PUT/DELETE /api/admin/projects - 前端:/projects 双虚线边框卡片页(参考 diygod.cc/projects),后台管理页 - 左栏/手机顶栏/后台侧栏均加「作品」入口 - 补充项目 CRUD 单元测试与后台鉴权拦截测试
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user