Compare commits

..

2 Commits

Author SHA1 Message Date
xiaomai
3254926c43 chore(deps): update project dependencies
Bumps several dependencies to their latest patch versions, including Nuxt, Tailwind CSS, and TypeScript. This keeps the project up-to-date with recent bug fixes and
improvements.
2025-10-08 09:14:55 +08:00
xiaomai
6f181d3f22 feat(admin): add initial admin dashboard structure
This commit introduces the foundational structure for the new admin dashboard.

- Utilizes @nuxt/ui to build the dashboard layout, including a collapsible sidebar and navigation.
- Adds initial pages for the dashboard, news, events, and hall of fame management.
- Implements a composable `useDashboardSidebarLinks` for managing sidebar navigation.
- Refactors the default layout by integrating the header and footer directly.
- Swaps the primary and secondary theme colors across the application.
2025-10-08 09:05:14 +08:00
21 changed files with 1429 additions and 1116 deletions

View File

@@ -1,6 +0,0 @@
@import "tailwindcss";
@theme {
--color-primary: #fcef91;
--color-secondary: #fb9e3a;
}

View File

@@ -1,2 +1,9 @@
@import "./app.css";
@import "./markdown.css";
@import "./markdown.css";
@import "tailwindcss";
@import "@nuxt/ui";
@theme {
--color-primary: #fb9e3a;
--color-secondary: #fcef91;
}

View File

@@ -1,39 +0,0 @@
<template>
<!-- 页脚 -->
<footer class="bg-gray-900 text-gray-300 py-6">
<div class="max-w-6xl mx-auto px-4 flex flex-col md:flex-row justify-between items-center">
<!-- 左侧版权信息 -->
<div class="text-center md:text-left">
<p>© 2025 永平中学校友会. 保留所有权利.</p>
</div>
<div class="">
<p class="mt-1">
Powered by:
<a href="https://tootaio.com" target="_blank" class="font-semibold hover:underline" style="color: #e24545;">
Tootaio Studio
</a>
<span class="mt-1 text-sm text-gray-400">2018 级毕业学长麦祖奕</span>
</p>
</div>
<!-- 右侧社交链接 -->
<div class="flex space-x-4 mt-3 md:mt-0">
<a href="#">
<Icon name="mdi-facebook" />
</a>
<a href="#">
<Icon name="mdi-instagram" />
</a>
<a href="#">
<Icon name="mdi-gmail" />
</a>
</div>
</div>
</footer>
</template>
<script lang="ts" setup>
</script>
<style></style>

View File

@@ -1,25 +0,0 @@
<template>
<!-- 导航栏 -->
<header class="bg-white shadow-md sticky top-0 z-50">
<div class="max-w-6xl mx-auto px-4 py-3 flex justify-between items-center">
<div>
<img class="inline w-16" src="/Logo.svg" alt="YPHS Alumni">
<h1 class="inline text-xl font-bold text-gray-900">
<a href="/" class="ml-4 hover:text-secondary">永平中学校友会</a>
</h1>
</div>
<nav class="space-x-6 hidden md:flex items-center">
<a href="#news" class="hover:text-secondary">新闻</a>
<a href="#events" class="hover:text-secondary">活动</a>
<a href="#donate" class="hover:text-secondary">捐赠未开放</a>
<a href="#about" class="hover:text-secondary">关于</a>
<a href="/join-us"
class="inline-flex items-center gap-2 bg-secondary text-white px-4 py-2 rounded-xl shadow hover:opacity-90">
加入
<Icon name="mdi:account-plus" class="w-5 h-5" />
</a>
</nav>
</div>
</header>
</template>

View File

@@ -0,0 +1,52 @@
<template>
<UModal v-model:open="open" title="撰写新闻" description="在数据库中添加一篇新闻">
<UButton label="撰写新闻" icon="mdi:newspaper-plus" />
<template #body>
<UForm :schema="newsSchema" :state="newsState" @submit="onSubmit">
<UFormField label="新闻标题" name="title">
<UInput v-model="newsState.title" placeholder="请输入新闻标题" class="w-full" />
</UFormField>
<UFormField label="新闻内容" name="contents">
<UTextarea v-model="newsState.content" class="w-full" />
</UFormField>
这里应该是要有一个 Markdown 编辑界面
<div class="flex justify-end gap-2">
<UButton label="Cancel" color="neutral" variant="subtle" @click="open = false" />
<UButton label="Create" color="primary" variant="solid" type="submit" />
</div>
</UForm>
</template>
</UModal>
</template>
<script lang="ts" setup>
import type { FormSubmitEvent } from '@nuxt/ui';
import * as z from 'zod'
const open = ref(false);
const newsSchema = z.object({
title: z.string().min(2, '标题太短了容易产生歧义,请写长一些').max(15, '标题太长了影响阅读体验,请简短一些'),
content: z.string()
})
type NewsSchema = z.output<typeof newsSchema>
const newsState = reactive<Partial<NewsSchema>>({
title: undefined,
content: undefined
})
const toast = useToast();
const onSubmit = async (event: FormSubmitEvent<NewsSchema>) => {
toast.add({
title: "成功",
description: `${event.data.title} 新闻稿已经撰写完毕`,
color: 'success'
})
open.value = false
}
</script>
<style></style>

View File

@@ -1,10 +1,10 @@
<template>
<div>
<!-- 捐赠模块 -->
<section id="donate" class="py-16 text-center bg-[var(--color-primary)]">
<section id="donate" class="py-16 text-center bg-secondary">
<h3 class="text-2xl font-bold text-gray-900 mb-4">支持与捐赠功能未开放</h3>
<p class="max-w-2xl mx-auto text-gray-700 mb-6">您的捐赠将用于奖学金校园建设及校友活动发展感谢您对母校的支持</p>
<a href="#" class="bg-[var(--color-secondary)] text-white px-8 py-3 rounded-xl shadow hover:opacity-90">立即捐赠</a>
<a href="#" class="bg-primary text-white px-8 py-3 rounded-xl shadow hover:opacity-90">立即捐赠</a>
</section>
</div>
</template>

View File

@@ -11,7 +11,7 @@
<h4 class="font-semibold text-lg mb-2">{{ event.title }}</h4>
<p class="text-sm text-gray-600 mb-1">日期{{ useChineseDateFormat(event.date) }}</p>
<p class="text-sm text-gray-600 mb-4">地点{{ event.location }}</p>
<a :href="event.path" class="bg-secondary text-white px-5 py-2 rounded-lg hover:opacity-90">阅读详情</a>
<a :href="event.path" class="bg-primary text-white px-5 py-2 rounded-lg hover:opacity-90">阅读详情</a>
</div>
</div>
</div>

View File

@@ -5,7 +5,7 @@
<h3 class="text-2xl font-bold text-center text-gray-900 mb-6">名人堂</h3>
<div class="grid md:grid-cols-4 gap-6">
<div v-for="person in persons" :key="person.id" class="flex flex-col items-center cursor-pointer transition hover:scale-105 hover:drop-shadow-2xl hover:-translate-y-1" @click="jumpToPersonIntro(person.path)">
<img :src="person.photo" :alt="person.name" class="w-40 rounded-full border-secondary border-4" />
<img :src="person.photo" :alt="person.name" class="w-40 rounded-full border-primary border-4" />
<h4 class="text-lg font-bold">{{ person.name }}</h4>
<p class="text-sm text-gray-500">{{ person.title }}</p>
</div>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<!-- Hero Banner -->
<section class="relative bg-primary py-32 md:py-48 lg:py-64 text-center bg-cover bg-center"
<section class="relative bg-secondary py-32 md:py-48 lg:py-64 text-center bg-cover bg-center"
style="background-image: url('/hero-image.jpg');">
<!-- 遮罩 -->
<div class="absolute inset-0 bg-black/40"></div>
@@ -15,7 +15,7 @@
马来西亚柔佛永平中学校友会官方网站
</p>
<div class="mt-6 space-x-4">
<a href="/join-us" class="bg-secondary text-white px-6 py-3 rounded-xl shadow hover:opacity-90">
<a href="/join-us" class="bg-primary text-white px-6 py-3 rounded-xl shadow hover:opacity-90">
立即加入我们
</a>
<!-- <a href="#donate"

View File

@@ -5,11 +5,11 @@
<h2 class="text-2xl font-bold text-gray-900 mb-6">最新新闻与公告</h2>
<div class="grid md:grid-cols-3 gap-6">
<article v-for="n in news" :key="n.id" @click="jumpToNewsDetail(n.stem)"
class="bg-primary/10 rounded-xl shadow cursor-pointer transition transform hover:-translate-y-1 hover:scale-105 hover:shadow-xl duration-300 ease-in-out">
class="bg-secondary/10 rounded-xl shadow cursor-pointer transition transform hover:-translate-y-1 hover:scale-105 hover:shadow-xl duration-300 ease-in-out">
<img class="rounded-xl" :src="n.cover" :alt="n.title">
<div class="p-5">
<h3 class="font-semibold mb-2">{{ n.title }}</h3>
<div class="px-1 w-max bg-primary/25 border-primary border-2 rounded-xl text-secondary text-sm mb-2">{{
<div class="px-1 w-max bg-secondary/25 border-secondary border-2 rounded-xl text-primary text-sm mb-2">{{
useChineseDateFormat(n.date) }}</div>
<p class="text-sm text-gray-600">{{ n.description }}</p>
</div>

View File

@@ -0,0 +1,56 @@
import type { NavigationMenuItem } from "@nuxt/ui";
export const useDashboardSidebarLinks = () => {
const sidebarLinks = [
[
{
label: "回到主站",
icon: "mdi:home",
type: "link",
to: "/",
target: "_blank"
},
{
label: "仪表盘",
icon: "mdi:view-dashboard",
to: "/admin/dashboard",
},
{
label: "内容管理",
icon: "mdi:bookshelf",
defaultOpen: true,
type: "trigger",
children: [
{
label: "新闻",
icon: "mdi:newspaper",
to: "/admin/contents/news",
},
{
label: "活动",
icon: "mdi:event",
to: "/admin/contents/events",
},
{
label: "名人堂",
icon: "mdi:trophy-award",
to: "/admin/contents/hall-of-fames",
},
],
},
// {
// label: "Settings",
// to: "/settings",
// icon: "mdi:cog",
// defaultOpen: true,
// type: "trigger",
// children: [
// { label: "General", icon: "mdi:tune", to: "/settings", exact: true },
// { label: "Advanced", icon: "mdi:flask", to: "/settings/advanced" },
// ],
// },
],
] satisfies NavigationMenuItem[][];
return { sidebarLinks };
};

View File

@@ -0,0 +1,44 @@
<template>
<UDashboardGroup>
<UDashboardSidebar id="default" :open="sidebarOpen" collapsible resizable>
<template #header="{ collapsed }">
<div class="font-bold flex items-center">
<img src="/Logo.svg" alt="YPHS Alumni" class="h-8">
<span class="ml-2" v-if="!collapsed">永平中学校友会官网</span>
</div>
</template>
<template #default="{ collapsed }">
<UDashboardSearchButton :collapsed="collapsed" icon="mdi:magnify" class="bg-transparent ring-default" />
<UNavigationMenu :collapsed="collapsed" :items="sidebarLinks" orientation="vertical" tooltip popover />
</template>
<template #footer="{ collapsed }">
</template>
</UDashboardSidebar>
<UDashboardSearch :groups="groups" />
<slot />
</UDashboardGroup>
</template>
<script lang="ts" setup>
const sidebarOpen = ref(false);
const groups = ref([]);
const { sidebarLinks } = useDashboardSidebarLinks();
const route = useRoute();
// 🪄 自动根据路由 name 或 meta 显示标题
const pageTitle = computed(() => {
// 如果路由定义了 meta.title 优先用它,否则用 name
return route.meta.title || route.name || '首页'
})
</script>
<style></style>

View File

@@ -0,0 +1,13 @@
<template>
<div>
<slot />
</div>
</template>
<script lang="ts" setup>
</script>
<style>
</style>

View File

@@ -1,8 +1,63 @@
<template>
<div>
<AppHeader />
<!-- 导航栏 -->
<header class="bg-white shadow-md sticky top-0 z-50">
<div class="max-w-6xl mx-auto px-4 py-3 flex justify-between items-center">
<div>
<img class="inline w-16" src="/Logo.svg" alt="YPHS Alumni">
<h1 class="inline text-xl font-bold text-gray-900">
<a href="/" class="ml-4 hover:text-primary">永平中学校友会</a>
</h1>
</div>
<nav class="space-x-6 hidden md:flex items-center">
<a href="#news" class="hover:text-primary">新闻</a>
<a href="#events" class="hover:text-primary">活动</a>
<a href="#donate" class="hover:text-primary">捐赠未开放</a>
<a href="#about" class="hover:text-primary">关于</a>
<a href="/join-us"
class="inline-flex items-center gap-2 bg-primary text-white px-4 py-2 rounded-xl shadow hover:opacity-90">
加入
<Icon name="mdi:account-plus" class="w-5 h-5" />
</a>
</nav>
</div>
</header>
<slot />
<AppFooter />
<!-- 页脚 -->
<footer class="bg-gray-900 text-gray-300 py-6">
<div class="max-w-6xl mx-auto px-4 flex flex-col md:flex-row justify-between items-center">
<!-- 左侧版权信息 -->
<div class="text-center md:text-left">
<p>© 2025 永平中学校友会. 保留所有权利.</p>
</div>
<div class="">
<p class="mt-1">
Powered by:
<a href="https://tootaio.com" target="_blank" class="font-semibold hover:underline" style="color: #e24545;">
Tootaio Studio
</a>
<span class="mt-1 text-sm text-gray-400">2018 级毕业学长麦祖奕</span>
</p>
</div>
<!-- 右侧社交链接 -->
<div class="flex space-x-4 mt-3 md:mt-0">
<a href="#">
<Icon name="mdi-facebook" />
</a>
<a href="#">
<Icon name="mdi-instagram" />
</a>
<a href="#">
<Icon name="mdi-gmail" />
</a>
</div>
</div>
</footer>
</div>
</template>
@@ -10,6 +65,4 @@
</script>
<style>
</style>
<style></style>

View File

@@ -0,0 +1,27 @@
<template>
<UDashboardPanel>
<template #header>
<UDashboardNavbar :title="pageTitle" toggle>
<template #leading>
<UDashboardSidebarCollapse />
</template>
</UDashboardNavbar>
</template>
<template #body>
This is events page
</template>
</UDashboardPanel>
</template>
<script lang="ts" setup>
const pageTitle = "活动列表"
definePageMeta({
layout: "admin-dashboard",
title: pageTitle
})
useHead({
title: pageTitle
})
</script>
<style></style>

View File

@@ -0,0 +1,27 @@
<template>
<UDashboardPanel>
<template #header>
<UDashboardNavbar :title="pageTitle" toggle>
<template #leading>
<UDashboardSidebarCollapse />
</template>
</UDashboardNavbar>
</template>
<template #body>
This is hall of fame page
</template>
</UDashboardPanel>
</template>
<script lang="ts" setup>
const pageTitle = "名人堂列表"
definePageMeta({
layout: "admin-dashboard",
title: pageTitle
})
useHead({
title: pageTitle
})
</script>
<style></style>

View File

@@ -0,0 +1,34 @@
<template>
<UDashboardPanel>
<template #header>
<UDashboardNavbar :title="pageTitle" toggle>
<template #leading>
<UDashboardSidebarCollapse />
</template>
<template #trailing>
<UBadge size="md" label="4" variant="outline" color="neutral" />
</template>
<template #right>
<AdminContentsNewsAddModal />
</template>
</UDashboardNavbar>
</template>
<template #body>
This is news page
</template>
</UDashboardPanel>
</template>
<script lang="ts" setup>
const pageTitle = "新闻列表"
definePageMeta({
layout: "admin-dashboard",
title: pageTitle
})
useHead({
title: pageTitle
})
</script>
<style></style>

View File

@@ -0,0 +1,28 @@
<template>
<UDashboardPanel>
<template #header>
<UDashboardNavbar :title="pageTitle" toggle>
<template #leading>
<UDashboardSidebarCollapse />
</template>
</UDashboardNavbar>
</template>
<template #body>
This is home page
</template>
</UDashboardPanel>
</template>
<script lang="ts" setup>
const pageTitle = "仪表盘"
definePageMeta({
layout: "admin-dashboard",
title: pageTitle,
})
useHead({
title: pageTitle
})
</script>
<style></style>

15
app/pages/admin/index.vue Normal file
View File

@@ -0,0 +1,15 @@
<template>
<div>
Login Page
</div>
</template>
<script lang="ts" setup>
definePageMeta({
layout: "admin-login"
})
</script>
<style>
</style>

View File

@@ -12,19 +12,19 @@
"dependencies": {
"@nuxt/content": "3.7.1",
"@nuxt/image": "1.11.0",
"@nuxt/ui": "4.0.0",
"@nuxt/ui": "4.0.1",
"@nuxtjs/robots": "5.5.5",
"@nuxtjs/seo": "3.2.2",
"@nuxtjs/sitemap": "7.4.7",
"@tailwindcss/vite": "^4.1.13",
"@tailwindcss/vite": "^4.1.14",
"better-sqlite3": "^12.4.1",
"element-plus": "^2.11.4",
"html2pdf.js": "^0.12.1",
"maska": "^3.2.0",
"nuxt": "^4.1.2",
"nuxt": "^4.1.3",
"reka-ui": "^2.5.1",
"tailwindcss": "^4.1.13",
"typescript": "^5.9.2",
"tailwindcss": "^4.1.14",
"typescript": "^5.9.3",
"vue": "^3.5.22",
"vue-router": "^4.5.1",
"vue-sonner": "^2.0.9"

2079
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff