feat(p1): complete P1 foundation stage
Some checks failed
CI Go / test (push) Has been cancelled
CI Proto / lint (push) Has been cancelled
CI Python / test (push) Has been cancelled
CI TypeScript / test (push) Has been cancelled

- monorepo: pnpm workspace + go.work + pyproject.toml + commitlint/husky
- infra: docker-compose (minimal + full profiles) + init-sql + prometheus
- arch-scan: multi-language scanner skeleton (TS/Go/Python/Proto)
- shared-proto: buf v2 + classes.proto (ClassService CRUD contract)
- api-gateway: Go/Gin + JWT HS256 auth + reverse proxy + request ID
- classes: NestJS golden template (error system + observability + middleware + CRUD + tests)
- teacher-portal: Next.js + paper-feel UI design system
- CI/CD: 4 workflows (go/ts/py/proto)
- docs: migration guide + project_rules + coding-standards + git-workflow + ui-design-system + 004 + 9 module READMEs + known-issues + spec/plan migration + roadmap
This commit is contained in:
SpecialX
2026-07-07 23:39:37 +08:00
commit 2ba4250165
100 changed files with 15242 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--bg-paper: hsl(40, 20%, 98%);
--color-ink: hsl(25, 3%, 15%);
--color-ink-muted: hsl(30, 5%, 45%);
--color-accent: hsl(220, 60%, 35%);
--color-rule: hsl(30, 10%, 90%);
--font-serif: 'Fraunces', Georgia, serif;
--font-sans: 'Inter', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
html, body {
background: var(--bg-paper);
color: var(--color-ink);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-serif);
font-weight: 600;
letter-spacing: -0.01em;
}
/* 纸感分隔线 */
.rule {
border-top: 1px solid var(--color-rule);
}
.rule-thin {
border-top: 2px solid var(--color-rule);
}
/* 左侧竖线标记(节点展开样式)*/
.mark-left {
border-left: 2px solid var(--color-rule);
padding-left: 12px;
}

View File

@@ -0,0 +1,24 @@
import './globals.css';
import type { Metadata } from 'next';
import { Inter, Fraunces, JetBrains_Mono } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
const fraunces = Fraunces({ subsets: ['latin'], variable: '--font-fraunces' });
const mono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-mono' });
export const metadata: Metadata = {
title: 'Edu Teacher Portal',
description: 'K12 智慧教务平台 - 教师端',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN" className={`${inter.variable} ${fraunces.variable} ${mono.variable}`}>
<body>{children}</body>
</html>
);
}

View File

@@ -0,0 +1,219 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
interface ClassItem {
id: string;
name: string;
gradeId: string;
description?: string;
createdAt: number;
updatedAt: number;
}
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: { code: string; message: string };
}
export default function HomePage() {
const [classes, setClasses] = useState<ClassItem[]>([]);
const [loading, setLoading] = useState(false);
const [name, setName] = useState('');
const [gradeId, setGradeId] = useState('550e8400-e29b-41d4-a716-446655440000');
const [description, setDescription] = useState('');
const [error, setError] = useState<string | null>(null);
const fetchClasses = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await fetch('/api/v1/classes');
const json: ApiResponse<ClassItem[]> = await res.json();
if (json.success && json.data) {
setClasses(json.data);
} else {
setError(json.error?.message || 'Failed to load');
}
} catch (e) {
setError(e instanceof Error ? e.message : 'Network error');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchClasses();
}, [fetchClasses]);
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) return;
try {
const res = await fetch('/api/v1/classes', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer dev-token' },
body: JSON.stringify({ name, gradeId, description }),
});
const json = await res.json();
if (!json.success) {
setError(json.error?.message || 'Create failed');
return;
}
setName('');
setDescription('');
await fetchClasses();
} catch (e) {
setError(e instanceof Error ? e.message : 'Network error');
}
};
const handleDelete = async (id: string) => {
try {
const res = await fetch(`/api/v1/classes/${id}`, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer dev-token' },
});
const json = await res.json();
if (!json.success) {
setError(json.error?.message || 'Delete failed');
return;
}
await fetchClasses();
} catch (e) {
setError(e instanceof Error ? e.message : 'Network error');
}
};
return (
<div className="min-h-screen" style={{ background: 'var(--bg-paper)' }}>
<header className="border-b" style={{ borderColor: 'var(--color-rule)' }}>
<div className="max-w-6xl mx-auto px-8 py-6">
<h1 className="text-3xl" style={{ fontFamily: 'var(--font-serif)', color: 'var(--color-ink)' }}>
</h1>
<p className="mt-1 text-sm" style={{ color: 'var(--color-ink-muted)' }}>
P1 - classes CRUD
</p>
</div>
</header>
<main className="max-w-6xl mx-auto px-8 py-8 grid grid-cols-12 gap-8">
{/* 左侧:创建表单 */}
<aside className="col-span-4">
<h2 className="text-xl mb-4" style={{ fontFamily: 'var(--font-serif)' }}></h2>
<div className="rule-thin mb-4" />
<form onSubmit={handleCreate} className="space-y-4">
<div>
<label className="block text-xs uppercase tracking-wide mb-1" style={{ color: 'var(--color-ink-muted)' }}>
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 bg-transparent border-b focus:outline-none focus:border-b-2"
style={{ borderColor: 'var(--color-rule)', borderRadius: '6px 6px 0 0' }}
placeholder="如:高三(1)班"
required
/>
</div>
<div>
<label className="block text-xs uppercase tracking-wide mb-1" style={{ color: 'var(--color-ink-muted)' }}>
ID
</label>
<input
type="text"
value={gradeId}
onChange={(e) => setGradeId(e.target.value)}
className="w-full px-3 py-2 bg-transparent border-b text-sm font-mono"
style={{ borderColor: 'var(--color-rule)', borderRadius: '6px 6px 0 0' }}
/>
</div>
<div>
<label className="block text-xs uppercase tracking-wide mb-1" style={{ color: 'var(--color-ink-muted)' }}>
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full px-3 py-2 bg-transparent border-b resize-none"
style={{ borderColor: 'var(--color-rule)', borderRadius: '6px 6px 0 0' }}
rows={3}
/>
</div>
<button
type="submit"
className="px-4 py-2 text-white text-sm tracking-wide transition-opacity hover:opacity-90"
style={{ background: 'var(--color-accent)', borderRadius: '6px' }}
>
</button>
</form>
</aside>
{/* 中间:班级列表(纸面)*/}
<section className="col-span-8">
<div className="flex items-baseline justify-between mb-4">
<h2 className="text-xl" style={{ fontFamily: 'var(--font-serif)' }}>
<span className="ml-2 text-sm font-sans" style={{ color: 'var(--color-ink-muted)' }}>
{classes.length}
</span>
</h2>
<button
onClick={fetchClasses}
className="text-xs uppercase tracking-wide hover:opacity-70"
style={{ color: 'var(--color-accent)' }}
>
</button>
</div>
<div className="rule-thin mb-6" />
{error && (
<div className="mark-left mb-4 py-2" style={{ borderColor: 'var(--color-accent)' }}>
<p className="text-sm" style={{ color: 'var(--color-accent)' }}>{error}</p>
</div>
)}
{loading ? (
<p className="text-sm" style={{ color: 'var(--color-ink-muted)' }}>...</p>
) : classes.length === 0 ? (
<p className="text-sm italic" style={{ color: 'var(--color-ink-muted)' }}>
</p>
) : (
<ul className="space-y-0">
{classes.map((cls) => (
<li key={cls.id} className="py-4 grid grid-cols-12 gap-4 items-baseline" style={{ borderBottom: '1px solid var(--color-rule)' }}>
<div className="col-span-7">
<h3 className="text-lg" style={{ fontFamily: 'var(--font-serif)', color: 'var(--color-ink)' }}>
{cls.name}
</h3>
{cls.description && (
<p className="mt-1 text-sm" style={{ color: 'var(--color-ink-muted)' }}>{cls.description}</p>
)}
</div>
<div className="col-span-3 text-xs font-mono" style={{ color: 'var(--color-ink-muted)' }}>
{cls.id.slice(0, 8)}...
</div>
<div className="col-span-2 text-right">
<button
onClick={() => handleDelete(cls.id)}
className="text-xs uppercase tracking-wide hover:opacity-70"
style={{ color: 'var(--color-ink-muted)' }}
>
</button>
</div>
</li>
))}
</ul>
)}
</section>
</main>
</div>
);
}