feat: initial commit with Nuxt 3 student management system

- Add Nuxt 3 + Prisma + SQLite full-stack setup
- Add student CRUD API with batch import/export
- Add stats dashboard with gender/class distribution
- Add target community settings feature
- Add Docker deployment support (Dockerfile + docker-compose)
- Add README with development and deployment instructions
This commit is contained in:
2026-03-21 02:00:55 +08:00
commit 05c33b1fe8
25 changed files with 15749 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async () => {
const settings = await prisma.settings.findMany()
const result: Record<string, string> = {}
settings.forEach(s => {
result[s.key] = s.value
})
return result
})

View File

@@ -0,0 +1,21 @@
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
if (body.key === 'targetCommunities' && Array.isArray(body.value)) {
await prisma.settings.upsert({
where: { key: body.key },
create: { key: body.key, value: JSON.stringify(body.value) },
update: { value: JSON.stringify(body.value) }
})
} else {
await prisma.settings.upsert({
where: { key: body.key },
create: { key: body.key, value: body.value },
update: { value: body.value }
})
}
return { success: true }
})

View File

@@ -0,0 +1,87 @@
import { prisma, defaultCommunities } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const students = await prisma.student.findMany()
// 获取设置的目标小区
const settings = await prisma.settings.findMany({
where: { key: 'targetCommunities' }
})
const targetCommunities = settings.length > 0
? JSON.parse(settings[0].value)
: defaultCommunities
// 基础统计
const total = students.length
const genderStats = {
male: students.filter(s => s.gender === '男').length,
female: students.filter(s => s.gender === '女').length
}
// 班级统计
const classStats: Record<string, { count: number; male: number; female: number }> = {}
students.forEach(s => {
if (!classStats[s.className]) {
classStats[s.className] = { count: 0, male: 0, female: 0 }
}
classStats[s.className].count++
if (s.gender === '男') classStats[s.className].male++
else if (s.gender === '女') classStats[s.className].female++
})
// 年龄段统计
const currentYear = new Date().getFullYear()
const ageGroups = {
'3岁以下': 0,
'3-4岁': 0,
'4-5岁': 0,
'5-6岁': 0,
'6岁以上': 0
}
students.forEach(s => {
if (!s.birthday) return
const birthYear = parseInt(s.birthday.substring(0, 4))
if (isNaN(birthYear)) return
const age = currentYear - birthYear
if (age < 3) ageGroups['3岁以下']++
else if (age < 4) ageGroups['3-4岁']++
else if (age < 5) ageGroups['4-5岁']++
else if (age <= 6) ageGroups['5-6岁']++
else ageGroups['6岁以上']++
})
// 目标小区统计
const addressStats: Record<string, { count: number; male: number; female: number }> = {}
targetCommunities.forEach(name => {
addressStats[name] = { count: 0, male: 0, female: 0 }
})
students.forEach(s => {
if (!s.address) return
targetCommunities.forEach(community => {
if (s.address.includes(community)) {
addressStats[community].count++
if (s.gender === '男') addressStats[community].male++
else if (s.gender === '女') addressStats[community].female++
}
})
})
const filteredAddressStats = Object.entries(addressStats)
.filter(([_, v]) => v.count > 0)
.map(([name, v]) => ({ name, ...v }))
.sort((a, b) => b.count - a.count)
const targetCommunityTotal = filteredAddressStats.reduce((sum, s) => sum + s.count, 0)
return {
total,
genderStats,
classStats: Object.entries(classStats).map(([name, v]) => ({ name, ...v })),
ageGroups,
targetCommunities,
addressStats: filteredAddressStats,
targetCommunityTotal,
targetCommunityMale: filteredAddressStats.reduce((sum, s) => sum + s.male, 0),
targetCommunityFemale: filteredAddressStats.reduce((sum, s) => sum + s.female, 0)
}
})

View File

@@ -0,0 +1,11 @@
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const id = Number(event.context.params?.id)
await prisma.student.delete({
where: { id }
})
return { success: true }
})

View File

@@ -0,0 +1,23 @@
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const id = Number(event.context.params?.id)
const body = await readBody(event)
const student = await prisma.student.update({
where: { id },
data: {
className: body.className,
name: body.name,
gender: body.gender,
birthday: body.birthday || null,
address: body.address || null,
fatherName: body.fatherName || null,
fatherPhone: body.fatherPhone || null,
motherName: body.motherName || null,
motherPhone: body.motherPhone || null
}
})
return student
})

View File

@@ -0,0 +1,6 @@
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
await prisma.student.deleteMany()
return { success: true }
})

View File

@@ -0,0 +1,28 @@
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
if (!Array.isArray(body.students)) {
throw createError({
statusCode: 400,
message: 'Invalid data format'
})
}
const result = await prisma.student.createMany({
data: body.students.map((s: any) => ({
className: s.className,
name: s.name,
gender: s.gender || '',
birthday: s.birthday || null,
address: s.address || null,
fatherName: s.fatherName || null,
fatherPhone: s.fatherPhone || null,
motherName: s.motherName || null,
motherPhone: s.motherPhone || null
}))
})
return { count: result.count }
})

View File

@@ -0,0 +1,8 @@
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const students = await prisma.student.findMany({
orderBy: { id: 'desc' }
})
return students
})

View File

@@ -0,0 +1,21 @@
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const student = await prisma.student.create({
data: {
className: body.className,
name: body.name,
gender: body.gender,
birthday: body.birthday || null,
address: body.address || null,
fatherName: body.fatherName || null,
fatherPhone: body.fatherPhone || null,
motherName: body.motherName || null,
motherPhone: body.motherPhone || null
}
})
return student
})