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:
10
server/api/settings/index.get.ts
Normal file
10
server/api/settings/index.get.ts
Normal 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
|
||||
})
|
||||
21
server/api/settings/save.post.ts
Normal file
21
server/api/settings/save.post.ts
Normal 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 }
|
||||
})
|
||||
87
server/api/stats/index.get.ts
Normal file
87
server/api/stats/index.get.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
11
server/api/students/[id].delete.ts
Normal file
11
server/api/students/[id].delete.ts
Normal 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 }
|
||||
})
|
||||
23
server/api/students/[id].put.ts
Normal file
23
server/api/students/[id].put.ts
Normal 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
|
||||
})
|
||||
6
server/api/students/clear.post.ts
Normal file
6
server/api/students/clear.post.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { prisma } from '~/server/utils/prisma'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await prisma.student.deleteMany()
|
||||
return { success: true }
|
||||
})
|
||||
28
server/api/students/import.post.ts
Normal file
28
server/api/students/import.post.ts
Normal 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 }
|
||||
})
|
||||
8
server/api/students/index.get.ts
Normal file
8
server/api/students/index.get.ts
Normal 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
|
||||
})
|
||||
21
server/api/students/index.post.ts
Normal file
21
server/api/students/index.post.ts
Normal 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
|
||||
})
|
||||
Reference in New Issue
Block a user