Compare commits
5 Commits
b05faddfc0
...
e7f2bc2c47
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7f2bc2c47 | ||
|
|
1fedf7094c | ||
|
|
cc7b2a7398 | ||
|
|
3254926c43 | ||
|
|
6f181d3f22 |
27
.vscode/admin-templates.vue
vendored
Normal file
27
.vscode/admin-templates.vue
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar :title="pageTitle" toggle>
|
||||
<template #leading>
|
||||
<UDashboardSidebarCollapse />
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
</template>
|
||||
<template #body>
|
||||
管理员界面模板
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const pageTitle = "管理员界面模板"
|
||||
definePageMeta({
|
||||
layout: "admin-dashboard",
|
||||
title: pageTitle
|
||||
})
|
||||
useHead({
|
||||
title: pageTitle
|
||||
})
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"vue.volar",
|
||||
"nuxtr.nuxt-vscode-extentions",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
30
.vscode/settings.json
vendored
30
.vscode/settings.json
vendored
@@ -1,8 +1,24 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.page-template": "vue",
|
||||
"*.layout-template": "vue",
|
||||
"*.vue": "vue",
|
||||
"*.css": "tailwindcss"
|
||||
}
|
||||
}
|
||||
"files.associations": {
|
||||
"*.page-template": "vue",
|
||||
"*.layout-template": "vue",
|
||||
"*.vue": "vue",
|
||||
"*.css": "tailwindcss"
|
||||
},
|
||||
"editor.quickSuggestions": {
|
||||
"other": "on",
|
||||
"comments": "off",
|
||||
"strings": "on"
|
||||
},
|
||||
"tailwindCSS.classAttributes": [
|
||||
"class",
|
||||
"className",
|
||||
"ngClass",
|
||||
"class:list",
|
||||
"ui"
|
||||
],
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["ui:\\s*{([^)]*)\\s*}", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
||||
],
|
||||
"css.lint.unknownAtRules": "ignore"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<UApp>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
</UApp>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-primary: #fcef91;
|
||||
--color-secondary: #fb9e3a;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
64
app/components/PhoneInput.vue
Normal file
64
app/components/PhoneInput.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<!-- ~/components/PhoneInput.vue -->
|
||||
<script setup lang="ts">
|
||||
import { USelectMenu, UInput } from "#components";
|
||||
import { useCountries } from "~/composables/useCountries";
|
||||
|
||||
interface Props {
|
||||
modelValue?: string;
|
||||
defaultDial?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(["update:modelValue", "update:country"]);
|
||||
|
||||
const { getAll } = useCountries();
|
||||
|
||||
// 当前选中国家
|
||||
const selectedCountry = ref(
|
||||
getAll().find((c) => c.dial === props.defaultDial)?.dial || getAll()[0]?.dial
|
||||
);
|
||||
|
||||
console.table(getAll().map(c => ({ name: c.name.cn, dial: c.dial })));
|
||||
|
||||
// 用户输入的电话号码
|
||||
const phone = ref(props.modelValue || "");
|
||||
|
||||
// 计算选项
|
||||
const countryOptions = computed(() =>
|
||||
getAll()
|
||||
.filter((c) => c.dial && c.dial.trim() !== "")
|
||||
.map((c) => ({
|
||||
label: `${c.name.cn} (${c.dial || "未知"})`,
|
||||
id: c.dial || "unknown",
|
||||
}))
|
||||
);
|
||||
|
||||
// 完整号码输出
|
||||
const fullNumber = computed(() => {
|
||||
const dial = selectedCountry.value || "";
|
||||
return `${dial}${phone.value}`;
|
||||
});
|
||||
|
||||
// 双向绑定
|
||||
watch(phone, () => emit("update:modelValue", fullNumber.value));
|
||||
watch(selectedCountry, () => emit("update:country", selectedCountry.value));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 国家区号选择 -->
|
||||
<USelectMenu
|
||||
v-model="selectedCountry"
|
||||
value-key="id"
|
||||
:items="countryOptions"
|
||||
/>
|
||||
|
||||
<!-- 电话号码输入 -->
|
||||
<UInput
|
||||
v-model="phone"
|
||||
placeholder="输入电话号码"
|
||||
type="tel"
|
||||
class="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
59
app/components/admin/contents/news/AddModal.vue
Normal file
59
app/components/admin/contents/news/AddModal.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<UModal v-model:open="open" title="撰写新闻" description="在数据库中添加一篇新闻" fullscreen :ui="{
|
||||
body: 'overflow-y-auto p-6', // 防止溢出 + 内边距
|
||||
footer: 'justify-end'
|
||||
}">
|
||||
<UButton label="撰写新闻" icon="mdi:newspaper-plus" />
|
||||
|
||||
<template #body>
|
||||
<UForm ref="form" :schema="newsSchema" :state="newsState" @submit="onSubmit" class="space-y-4">
|
||||
<UFormField label="新闻标题" name="title">
|
||||
<UInput v-model="newsState.title" placeholder="请输入新闻标题" class="w-full" />
|
||||
</UFormField>
|
||||
<UFormField label="新闻内容" name="contents">
|
||||
<ClientOnly>
|
||||
<MdEditor v-model="newsState.content" />
|
||||
</ClientOnly>
|
||||
</UFormField>
|
||||
</UForm>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<UButton label="Cancel" color="neutral" variant="subtle" @click="open = false" />
|
||||
<UButton label="Create" color="primary" variant="solid" @click="form.submit()" />
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormSubmitEvent } from '@nuxt/ui';
|
||||
import { MdEditor } from 'md-editor-v3';
|
||||
import * as z from 'zod'
|
||||
|
||||
const form = ref();
|
||||
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>
|
||||
271
app/components/admin/manage/members/AddModal.vue
Normal file
271
app/components/admin/manage/members/AddModal.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<UModal
|
||||
v-model:open="open"
|
||||
title="新增会员"
|
||||
description="新增一名会员信息"
|
||||
:ui="{
|
||||
body: 'overflow-y-auto p-6', // 防止溢出 + 内边距
|
||||
footer: 'justify-end',
|
||||
}"
|
||||
>
|
||||
<UButton label="新增会员" icon="mdi:account-plus" />
|
||||
|
||||
<template #body>
|
||||
<UForm
|
||||
ref="alumniForm"
|
||||
:schema="alumniSchema"
|
||||
:state="alumniState"
|
||||
@submit="onSubmit"
|
||||
class="space-y-4"
|
||||
>
|
||||
<div class="two-col">
|
||||
<UFormField label="中文姓名" name="chineseName">
|
||||
<UInput
|
||||
v-model="alumniState.chineseName"
|
||||
placeholder="张三"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField label="英文姓名" name="englishName">
|
||||
<UInput
|
||||
v-model="alumniState.englishName"
|
||||
placeholder="John Doe"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<UFormField label="身份证号码(支持旧 IC)" name="icNumber">
|
||||
<div class="flex items-center gap-2">
|
||||
<UCheckbox
|
||||
v-model="alumniState.icWithA"
|
||||
label="带 A?"
|
||||
class="whitespace-nowrap"
|
||||
/>
|
||||
<div class="relative w-full">
|
||||
<UInput
|
||||
v-model="alumniState.icNumber"
|
||||
placeholder="1234567 或 123456-78-9101"
|
||||
class="w-full pl-6"
|
||||
v-maska="{ mask: icNumberMask }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="three-col">
|
||||
<UFormField label="加入年份" name="joinedYear">
|
||||
<UInput
|
||||
v-model="alumniState.joinedYear"
|
||||
type="number"
|
||||
placeholder="例如:2015"
|
||||
:min="1986"
|
||||
:max="new Date().getFullYear()"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField label="收据" name="receiptNumber">
|
||||
<UInput
|
||||
v-model="alumniState.receiptNumber"
|
||||
placeholder="1234 或 8888/1234"
|
||||
v-maska="{ mask: ['####', '####/####'] }"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField label="收据类型" name="receiptType">
|
||||
<USelect
|
||||
v-model="alumniState.receiptType"
|
||||
:items="receiptType"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="three-col">
|
||||
<UFormField label="毕业 / 离校" name="educationStatus">
|
||||
<USelect
|
||||
v-model="alumniState.educationStatus"
|
||||
:items="educationStatus"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField
|
||||
:label="`${alumniState.educationStatus?.endsWith('graduated') ? '毕业' : alumniState.educationStatus?.endsWith('dropout') ? '离校' : '毕业 / 离校'}年份`"
|
||||
name="leaveYear"
|
||||
>
|
||||
<UInput
|
||||
v-model="alumniState.leaveYear"
|
||||
type="number"
|
||||
placeholder="例如: 1998"
|
||||
:min="1957"
|
||||
:max="new Date().getFullYear()"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<!-- 计算毕业届别 / 输入离校年级 -->
|
||||
<div
|
||||
v-if="
|
||||
alumniState.educationStatus == 'junior_graduated' ||
|
||||
alumniState.educationStatus == 'senior_graduated'
|
||||
"
|
||||
class="flex items-center h-full"
|
||||
>
|
||||
{{
|
||||
alumniState.educationStatus == "junior_graduated" ? "初" : "高"
|
||||
}}中第 {{ graduatedLevel }} 届
|
||||
</div>
|
||||
<UFormField
|
||||
v-else-if="
|
||||
alumniState.educationStatus == 'junior_dropout' ||
|
||||
alumniState.educationStatus == 'senior_dropout'
|
||||
"
|
||||
:label="`${alumniState.educationStatus == 'junior_dropout' ? '初' : '高'}中?年级离校`"
|
||||
name="dropout_level"
|
||||
>
|
||||
<UInput type="number" :min="1" :max="3" class="w-full" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<div class="two-col">
|
||||
<UFormField label="联系方式( WhatsApp 优先)" name="phone">
|
||||
<UInput
|
||||
v-model="alumniState.phoneNumber"
|
||||
placeholder="国外电话需要加上区号"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField label="电子邮箱" name="email">
|
||||
<UInput
|
||||
type="email"
|
||||
v-model="alumniState.email"
|
||||
placeholder="请输入您的电子邮箱"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
</UForm>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<UButton
|
||||
label="Cancel"
|
||||
color="neutral"
|
||||
variant="subtle"
|
||||
@click="open = false"
|
||||
/>
|
||||
<UButton
|
||||
label="Create"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
@click="alumniForm.submit()"
|
||||
/>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { vMaska } from "maska/vue";
|
||||
import type { SelectItem, FormSubmitEvent } from "@nuxt/ui";
|
||||
import * as z from "zod";
|
||||
|
||||
// 根据是否带 A,动态调整 mask
|
||||
const icNumberMask = computed(() => {
|
||||
return alumniState.icWithA ? ["A#######"] : ["#######", "######-##-####"];
|
||||
});
|
||||
|
||||
const receiptType = ref<SelectItem[]>([
|
||||
{ label: "---请选择---", value: "unknown" },
|
||||
{ label: "表格", value: "form" },
|
||||
{ label: "收据", value: "receipt" },
|
||||
]);
|
||||
|
||||
const educationStatus = ref<SelectItem[]>([
|
||||
{ label: "---请选择---", value: "unknown" },
|
||||
{ label: "初中离校", value: "junior_dropout" },
|
||||
{ label: "初中毕业", value: "junior_graduated" },
|
||||
{ label: "高中离校", value: "senior_dropout" },
|
||||
{ label: "高中毕业", value: "senior_graduated" },
|
||||
]);
|
||||
|
||||
const graduatedLevel = computed<number | string>(() => {
|
||||
const leaveYear = Number(alumniState.leaveYear);
|
||||
switch (alumniState.educationStatus) {
|
||||
case "junior_graduated":
|
||||
return leaveYear > 1958 ? leaveYear - 1958 : "-";
|
||||
case "senior_graduated":
|
||||
return leaveYear > 1965 ? leaveYear - 1965 : "-";
|
||||
default:
|
||||
return "-";
|
||||
}
|
||||
});
|
||||
|
||||
const alumniForm = ref();
|
||||
const open = ref(false);
|
||||
|
||||
const alumniSchema = z.object({
|
||||
chineseName: z.string().min(2, "名字似乎太短了哦").max(4, "名字似乎太长了哦"),
|
||||
englishName: z.string().nullable(),
|
||||
icWithA: z.boolean(),
|
||||
icNumber: z
|
||||
.string()
|
||||
.min(7, "身份证号码最少得 7 位")
|
||||
.max(14, "身份证号码最多支持 12 位")
|
||||
.nullable(),
|
||||
joinedYear: z.number().nullable(),
|
||||
receiptNumber: z.string().nullable(),
|
||||
receiptType: z.enum(["unknown", "form", "receipt"]), // 要和上面的 receiptType 相匹配
|
||||
leaveYear: z.number().nullable(),
|
||||
educationStatus: z.enum([
|
||||
"unknown",
|
||||
"junior_dropout",
|
||||
"junior_graduated",
|
||||
"senior_dropout",
|
||||
"senior_graduated",
|
||||
]),
|
||||
dropout_level: z.string().min(1).max(1).nullable(),
|
||||
phoneNumber: z.string(),
|
||||
email: z.string().email().nullable(),
|
||||
});
|
||||
|
||||
type AlumniSchema = z.output<typeof alumniSchema>;
|
||||
|
||||
const alumniState = reactive<Partial<AlumniSchema>>({
|
||||
chineseName: undefined,
|
||||
englishName: null,
|
||||
icWithA: false,
|
||||
icNumber: null,
|
||||
joinedYear: new Date().getFullYear(),
|
||||
receiptNumber: null,
|
||||
receiptType: "unknown",
|
||||
leaveYear: null,
|
||||
educationStatus: "unknown",
|
||||
dropout_level: null,
|
||||
phoneNumber: undefined,
|
||||
email: null,
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
const onSubmit = async (event: FormSubmitEvent<AlumniSchema>) => {
|
||||
console.table(event.data);
|
||||
toast.add({
|
||||
title: "成功",
|
||||
description: `${event.data.chineseName} 新闻稿已经撰写完毕`,
|
||||
color: "success",
|
||||
});
|
||||
open.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference 'tailwindcss';
|
||||
|
||||
.two-col {
|
||||
@apply grid grid-cols-2 gap-4;
|
||||
}
|
||||
|
||||
.three-col {
|
||||
@apply grid grid-cols-3 gap-4;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
76
app/composables/useCountries.ts
Normal file
76
app/composables/useCountries.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { countries, type Country } from "~/data/countries";
|
||||
|
||||
/**
|
||||
* 🌍 useCountries composable
|
||||
* 提供国家相关的搜索、过滤、分组、查找等功能
|
||||
*/
|
||||
export const useCountries = () => {
|
||||
/**
|
||||
* 获取全部国家
|
||||
*/
|
||||
const getAll = (): Country[] => countries;
|
||||
|
||||
/**
|
||||
* 按洲分组
|
||||
*/
|
||||
const groupedByContinent = computed(() => {
|
||||
const groups: Record<string, Country[]> = {};
|
||||
for (const c of countries) {
|
||||
const key = c.continent || "Unknown";
|
||||
if (!groups[key]) groups[key] = [];
|
||||
groups[key].push(c);
|
||||
}
|
||||
return groups;
|
||||
});
|
||||
|
||||
/**
|
||||
* 按名称搜索(支持多语言字段)
|
||||
* @param query 搜索关键字
|
||||
*/
|
||||
const search = (query: string) => {
|
||||
if (!query) return countries;
|
||||
const q = query.toLowerCase();
|
||||
return countries.filter((c) =>
|
||||
Object.values(c.name).some((name) => name.toLowerCase().includes(q))
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据国家代码查找
|
||||
* @param code ISO Alpha-2 或 Alpha-3 代码
|
||||
*/
|
||||
const findByCode = (code: string) => {
|
||||
const upper = code.toUpperCase();
|
||||
return countries.find((c) => c.code === upper || c.iso3 === upper);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取特定语言的显示名称
|
||||
* @param country 国家对象
|
||||
* @param lang 语言代码(默认为 en)
|
||||
*/
|
||||
const getDisplayName = (
|
||||
country: Country,
|
||||
lang: keyof Country["name"] = "en"
|
||||
) => {
|
||||
return country.name?.[lang] || country.name.en;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据洲名筛选
|
||||
* @param continent 洲名(如 'Asia'、'Europe')
|
||||
*/
|
||||
const filterByContinent = (continent: string) => {
|
||||
const key = continent.trim().toLowerCase();
|
||||
return countries.filter((c) => c.continent.toLowerCase() === key);
|
||||
};
|
||||
|
||||
return {
|
||||
getAll,
|
||||
groupedByContinent,
|
||||
search,
|
||||
findByCode,
|
||||
filterByContinent,
|
||||
getDisplayName,
|
||||
};
|
||||
};
|
||||
71
app/composables/useDashboardSidebarLinks.ts
Normal file
71
app/composables/useDashboardSidebarLinks.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { NavigationMenuItem } from "@nuxt/ui";
|
||||
|
||||
export const useDashboardSidebarLinks = () => {
|
||||
const sidebarLinks = [
|
||||
[
|
||||
{
|
||||
label: "仪表盘",
|
||||
icon: "mdi:view-dashboard",
|
||||
to: "/admin/dashboard",
|
||||
},
|
||||
{
|
||||
label: "信息管理",
|
||||
icon: "mdi:file-document-outline",
|
||||
defaultOpen: true,
|
||||
type: "trigger",
|
||||
children: [
|
||||
{
|
||||
label: "会员籍管理",
|
||||
icon: "mdi:account",
|
||||
to: "/admin/manage/members",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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" },
|
||||
// ],
|
||||
// },
|
||||
],
|
||||
[
|
||||
{
|
||||
label: "回到主站",
|
||||
icon: "mdi:home",
|
||||
type: "link",
|
||||
to: "/",
|
||||
target: "_blank",
|
||||
},
|
||||
],
|
||||
] satisfies NavigationMenuItem[][];
|
||||
|
||||
return { sidebarLinks };
|
||||
};
|
||||
3003
app/data/countries.ts
Normal file
3003
app/data/countries.ts
Normal file
File diff suppressed because it is too large
Load Diff
271
app/error.vue
Normal file
271
app/error.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<script setup lang="ts">
|
||||
import type { NuxtError } from "#app";
|
||||
|
||||
// 定义扩展错误类型
|
||||
interface ExtendedError extends NuxtError {
|
||||
url?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{ error?: ExtendedError }>();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const status = computed(() => props.error?.statusCode ?? 500);
|
||||
const statusMessage = computed(() => props.error?.statusMessage ?? "");
|
||||
|
||||
const title = computed(() => {
|
||||
switch (status.value) {
|
||||
case 404:
|
||||
return "页面未找到";
|
||||
case 403:
|
||||
return "禁止访问";
|
||||
case 500:
|
||||
return "服务器内部错误";
|
||||
default:
|
||||
return `${status.value} 错误`;
|
||||
}
|
||||
});
|
||||
|
||||
useHead({ title: title.value });
|
||||
|
||||
const friendlyMessage = computed(() => {
|
||||
if (status.value === 404)
|
||||
return "抱歉,我们找不到您要访问的页面。它可能已被移动、删除或从未存在过。";
|
||||
if (status.value === 403)
|
||||
return "抱歉,您没有权限访问此页面。请检查您的账户权限或联系管理员。";
|
||||
if (status.value === 500)
|
||||
return "服务器出了点问题,我们正在努力修复。请稍后再试。";
|
||||
return props.error?.message ?? "发生了未知错误,我们的技术团队已经收到通知。";
|
||||
});
|
||||
|
||||
const goHome = () => router.push("/");
|
||||
const goBack = () => {
|
||||
if (import.meta.client && window.history.length > 1) router.back();
|
||||
else router.push("/");
|
||||
};
|
||||
const reloadPage = () => {
|
||||
if (import.meta.client) window.location.reload();
|
||||
};
|
||||
const contactSupport = () => {
|
||||
if (!import.meta.client) return;
|
||||
const subject = encodeURIComponent(
|
||||
`网站错误反馈 — ${title.value} — ${route.fullPath}`
|
||||
);
|
||||
const body = encodeURIComponent(`错误信息:
|
||||
Status: ${status.value} ${statusMessage.value}
|
||||
Path: ${route.fullPath}
|
||||
|
||||
请描述您在做什么以及复现步骤:`);
|
||||
// 请替换为真实的支持邮箱或工单链接
|
||||
window.location.href = `mailto:hello@example.com?subject=${subject}&body=${body}`;
|
||||
};
|
||||
|
||||
const isDev = import.meta.dev;
|
||||
|
||||
// 格式化错误信息
|
||||
const formattedDebugInfo = computed(() => {
|
||||
if (!props.error) return isDev ? "无错误对象传入。" : "错误详情已隐藏。";
|
||||
|
||||
if (isDev) {
|
||||
try {
|
||||
return {
|
||||
status: props.error.statusCode,
|
||||
message: props.error.message || props.error.statusMessage,
|
||||
url: props.error.url,
|
||||
stack: props.error.stack,
|
||||
};
|
||||
} catch (e) {
|
||||
return { error: String(props.error) };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: props.error.statusCode || "N/A",
|
||||
message: props.error.message || props.error.statusMessage || "N/A",
|
||||
};
|
||||
});
|
||||
|
||||
// 错误图标
|
||||
const errorIcon = computed(() => {
|
||||
switch (status.value) {
|
||||
case 404:
|
||||
return "mdi:magnify-close";
|
||||
case 403:
|
||||
return "mdi:lock-alert";
|
||||
case 500:
|
||||
return "mdi:server-off";
|
||||
default:
|
||||
return "mdi:alert-circle";
|
||||
}
|
||||
});
|
||||
|
||||
// 错误颜色
|
||||
const errorColor = computed(() => {
|
||||
switch (status.value) {
|
||||
case 404:
|
||||
return "#fb9e3a"; // 使用主题色
|
||||
case 403:
|
||||
return "#e74c3c";
|
||||
case 500:
|
||||
return "#c0392b";
|
||||
default:
|
||||
return "#fb9e3a"; // 使用主题色
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main
|
||||
class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4"
|
||||
>
|
||||
<div
|
||||
class="max-w-3xl w-full bg-white dark:bg-gray-800 shadow-lg rounded-lg overflow-hidden"
|
||||
>
|
||||
<!-- 顶部状态栏 -->
|
||||
<div class="h-2 w-full" :style="{ backgroundColor: errorColor }"></div>
|
||||
|
||||
<div class="p-8 md:p-10">
|
||||
<div class="flex flex-col md:flex-row items-center gap-8">
|
||||
<!-- 错误代码和图标 -->
|
||||
<div class="flex-none text-center">
|
||||
<div class="relative">
|
||||
<div
|
||||
class="w-32 h-32 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||
:style="{ backgroundColor: errorColor + '20' }"
|
||||
>
|
||||
<div class="text-4xl" :style="{ color: errorColor }">
|
||||
<span v-if="status === 404">404</span>
|
||||
<span v-else-if="status === 403">403</span>
|
||||
<span v-else-if="status === 500">500</span>
|
||||
<span v-else>{{ status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="text-sm text-gray-500 dark:text-gray-400 font-medium mt-2"
|
||||
>
|
||||
{{ statusMessage || title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息和操作 -->
|
||||
<div class="flex-1">
|
||||
<h1
|
||||
class="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-3"
|
||||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p class="text-gray-600 dark:text-gray-300 leading-relaxed mb-6">
|
||||
{{ friendlyMessage }}
|
||||
</p>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="flex flex-wrap gap-3 mb-6">
|
||||
<button
|
||||
@click="goHome"
|
||||
class="px-5 py-2.5 rounded-md text-white font-medium transition-colors flex items-center gap-2"
|
||||
style="background-color: var(--color-primary)"
|
||||
>
|
||||
<span>返回首页</span>
|
||||
</button>
|
||||
<button
|
||||
@click="goBack"
|
||||
class="px-5 py-2.5 rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
返回上页
|
||||
</button>
|
||||
<button
|
||||
@click="reloadPage"
|
||||
class="px-5 py-2.5 rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
重新加载
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 报告问题按钮 -->
|
||||
<div class="mb-6">
|
||||
<button
|
||||
@click="contactSupport"
|
||||
class="px-5 py-2.5 rounded-md bg-gray-800 dark:bg-gray-700 text-white font-medium hover:bg-gray-700 dark:hover:bg-gray-600 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<span>报告问题</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
||||
如果这是由链接或书签导致的,请尝试返回或访问主页。
|
||||
</div>
|
||||
|
||||
<!-- 错误详情 -->
|
||||
<details
|
||||
class="border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden"
|
||||
>
|
||||
<summary
|
||||
class="cursor-pointer p-3 bg-gray-50 dark:bg-gray-900 font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
错误详情({{ isDev ? "开发模式" : "精简信息" }})
|
||||
</summary>
|
||||
<div
|
||||
class="p-4 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 overflow-auto max-h-64"
|
||||
>
|
||||
<div v-if="typeof formattedDebugInfo === 'object'">
|
||||
<div v-if="isDev" class="space-y-4">
|
||||
<div v-if="formattedDebugInfo.status">
|
||||
<strong>状态码:</strong> {{ formattedDebugInfo.status }}
|
||||
</div>
|
||||
<div v-if="formattedDebugInfo.message">
|
||||
<strong>消息:</strong> {{ formattedDebugInfo.message }}
|
||||
</div>
|
||||
<div v-if="formattedDebugInfo.url">
|
||||
<strong>URL:</strong> {{ formattedDebugInfo.url }}
|
||||
</div>
|
||||
<div v-if="formattedDebugInfo.stack" class="mt-3">
|
||||
<strong>堆栈跟踪:</strong>
|
||||
<pre
|
||||
class="mt-2 text-xs bg-gray-50 dark:bg-gray-900 p-3 rounded overflow-x-auto"
|
||||
>{{ formattedDebugInfo.stack }}</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="formattedDebugInfo.status">
|
||||
<strong>状态码:</strong> {{ formattedDebugInfo.status }}
|
||||
</div>
|
||||
<div v-if="formattedDebugInfo.message">
|
||||
<strong>消息:</strong> {{ formattedDebugInfo.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ formattedDebugInfo }}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义样式补充 */
|
||||
details[open] summary {
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* 暗色模式适配 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
details[open] summary {
|
||||
border-bottom-color: #4b5563;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 640px) {
|
||||
.text-4xl {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
44
app/layouts/admin-dashboard.vue
Normal file
44
app/layouts/admin-dashboard.vue
Normal 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>
|
||||
@@ -1,15 +1,62 @@
|
||||
<template>
|
||||
<div>
|
||||
<AppHeader />
|
||||
<slot />
|
||||
<AppFooter />
|
||||
<div class="min-h-screen flex flex-col">
|
||||
<!-- 导航栏 -->
|
||||
<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>
|
||||
|
||||
<!-- 主体部分 -->
|
||||
<main class="flex-1">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
27
app/pages/admin/contents/events/index.vue
Normal file
27
app/pages/admin/contents/events/index.vue
Normal 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>
|
||||
27
app/pages/admin/contents/hall-of-fames/index.vue
Normal file
27
app/pages/admin/contents/hall-of-fames/index.vue
Normal 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>
|
||||
34
app/pages/admin/contents/news/index.vue
Normal file
34
app/pages/admin/contents/news/index.vue
Normal 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>
|
||||
29
app/pages/admin/dashboard/index.vue
Normal file
29
app/pages/admin/dashboard/index.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<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,
|
||||
alias: ['/admin']
|
||||
})
|
||||
useHead({
|
||||
title: pageTitle
|
||||
})
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
165
app/pages/admin/manage/members/index.vue
Normal file
165
app/pages/admin/manage/members/index.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar :title="pageTitle" toggle>
|
||||
<template #leading>
|
||||
<UDashboardSidebarCollapse />
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<AdminManageMembersAddModal />
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
</template>
|
||||
<template #body>
|
||||
<UTable
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:loading="status === 'pending'"
|
||||
class="flex-1 h-80"
|
||||
/>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TableColumn } from "@nuxt/ui";
|
||||
import type { Row } from '@tanstack/vue-table'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
|
||||
const pageTitle = "会员籍管理";
|
||||
definePageMeta({
|
||||
layout: "admin-dashboard",
|
||||
title: pageTitle,
|
||||
});
|
||||
useHead({
|
||||
title: pageTitle,
|
||||
});
|
||||
|
||||
const UAvatar = resolveComponent("UAvatar");
|
||||
const UButton = resolveComponent("UButton");
|
||||
const UBadge = resolveComponent("UBadge");
|
||||
const UDropdownMenu = resolveComponent("UDropdownMenu");
|
||||
|
||||
const toast = useToast()
|
||||
const { copy } = useClipboard()
|
||||
|
||||
type User = {
|
||||
id: number;
|
||||
name: string;
|
||||
username: string;
|
||||
email: string;
|
||||
avatar: { src: string };
|
||||
company: { name: string };
|
||||
};
|
||||
|
||||
const { data, status } = await useFetch<User[]>(
|
||||
"https://jsonplaceholder.typicode.com/users",
|
||||
{
|
||||
key: "table-users",
|
||||
transform: (data) => {
|
||||
return (
|
||||
data?.map((user) => ({
|
||||
...user,
|
||||
avatar: {
|
||||
src: `https://i.pravatar.cc/120?img=${user.id}`,
|
||||
alt: `${user.name} avatar`,
|
||||
},
|
||||
})) || []
|
||||
);
|
||||
},
|
||||
lazy: true,
|
||||
}
|
||||
);
|
||||
|
||||
const columns: TableColumn<User>[] = [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
cell: ({ row }) => {
|
||||
return h("div", { class: "flex items-center gap-3" }, [
|
||||
h(UAvatar, {
|
||||
...row.original.avatar,
|
||||
size: "lg",
|
||||
}),
|
||||
h("div", undefined, [
|
||||
h("p", { class: "font-medium text-highlighted" }, row.original.name),
|
||||
h("p", { class: "" }, `@${row.original.username}`),
|
||||
]),
|
||||
]);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "email",
|
||||
header: "Email",
|
||||
},
|
||||
{
|
||||
accessorKey: "company",
|
||||
header: "Company",
|
||||
cell: ({ row }) => row.original.company.name,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
return h(
|
||||
"div",
|
||||
{ class: "text-right" },
|
||||
h(
|
||||
UDropdownMenu,
|
||||
{
|
||||
content: {
|
||||
align: "end",
|
||||
},
|
||||
items: getRowItems(row),
|
||||
"aria-label": "Actions dropdown",
|
||||
},
|
||||
() =>
|
||||
h(UButton, {
|
||||
icon: "i-lucide-ellipsis-vertical",
|
||||
color: "neutral",
|
||||
variant: "ghost",
|
||||
class: "ml-auto",
|
||||
"aria-label": "Actions dropdown",
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function getRowItems(row: Row<object>) {
|
||||
return [
|
||||
{
|
||||
type: 'label',
|
||||
label: 'Actions'
|
||||
},
|
||||
{
|
||||
label: 'Copy payment ID',
|
||||
onSelect() {
|
||||
copy(row.original.id)
|
||||
|
||||
toast.add({
|
||||
title: 'Payment ID copied to clipboard!',
|
||||
color: 'success',
|
||||
icon: 'i-lucide-circle-check'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'View customer'
|
||||
},
|
||||
{
|
||||
label: 'View payment details'
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
6
app/plugins/md-editor.client.ts
Normal file
6
app/plugins/md-editor.client.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { MdEditor } from 'md-editor-v3'
|
||||
import 'md-editor-v3/lib/style.css'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.component('MdEditor', MdEditor)
|
||||
})
|
||||
11
package.json
11
package.json
@@ -12,19 +12,20 @@
|
||||
"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",
|
||||
"md-editor-v3": "^6.0.1",
|
||||
"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"
|
||||
|
||||
2723
pnpm-lock.yaml
generated
2723
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user