diff --git a/.gemini/settings.json b/.gemini/settings.json deleted file mode 100644 index 666b003..0000000 --- a/.gemini/settings.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "general": { - "defaultApprovalMode": "plan" - }, - "ui": { - "footer": { - "hideModelInfo": false, - "hideContextPercentage": false - }, - "showMemoryUsage": true, - "showModelInfoInChat": true - }, - "model": { - "name": "gemini-3-pro" - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index c58ff45..cb85851 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ frontend-dev.err.log vue/dist/ .env.oss.local 账号密码.txt -.history/ \ No newline at end of file +.history/ +.vscode/ +.gemini/ +.codex/ \ No newline at end of file diff --git a/.history/backend/src/test/java/com/yoyuzh/auth/AuthServiceTest_20260308193550.java b/.history/backend/src/test/java/com/yoyuzh/auth/AuthServiceTest_20260308193550.java deleted file mode 100644 index d54392b..0000000 --- a/.history/backend/src/test/java/com/yoyuzh/auth/AuthServiceTest_20260308193550.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.yoyuzh.auth; - -import com.yoyuzh.auth.dto.AuthResponse; -import com.yoyuzh.auth.dto.LoginRequest; -import com.yoyuzh.auth.dto.RegisterRequest; -import com.yoyuzh.common.BusinessException; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.time.LocalDateTime; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class AuthServiceTest { - - @Mock - private UserRepository userRepository; - - @Mock - private PasswordEncoder passwordEncoder; - - @Mock - private AuthenticationManager authenticationManager; - - @Mock - private JwtTokenProvider jwtTokenProvider; - - @InjectMocks - private AuthService authService; - - @Test - void shouldRegisterUserWithEncryptedPassword() { - RegisterRequest request = new RegisterRequest("alice", "alice@example.com", "plain-password"); - when(userRepository.existsByUsername("alice")).thenReturn(false); - when(userRepository.existsByEmail("alice@example.com")).thenReturn(false); - when(passwordEncoder.encode("plain-password")).thenReturn("encoded-password"); - when(userRepository.save(any(User.class))).thenAnswer(invocation -> { - User user = invocation.getArgument(0); - user.setId(1L); - user.setCreatedAt(LocalDateTime.now()); - return user; - }); - when(jwtTokenProvider.generateToken(1L, "alice")).thenReturn("jwt-token"); - - AuthResponse response = authService.register(request); - - assertThat(response.token()).isEqualTo("jwt-token"); - assertThat(response.user().username()).isEqualTo("alice"); - verify(passwordEncoder).encode("plain-password"); - } - - @Test - void shouldRejectDuplicateUsernameOnRegister() { - RegisterRequest request = new RegisterRequest("alice", "alice@example.com", "plain-password"); - when(userRepository.existsByUsername("alice")).thenReturn(true); - - assertThatThrownBy(() -> authService.register(request)) - .isInstanceOf(BusinessException.class) - .hasMessageContaining("用户名已存在"); - } - - @Test - void shouldLoginAndReturnToken() { - LoginRequest request = new LoginRequest("alice", "plain-password"); - User user = new User(); - user.setId(1L); - user.setUsername("alice"); - user.setEmail("alice@example.com"); - user.setPasswordHash("encoded-password"); - user.setCreatedAt(LocalDateTime.now()); - when(userRepository.findByUsername("alice")).thenReturn(Optional.of(user)); - when(jwtTokenProvider.generateToken(1L, "alice")).thenReturn("jwt-token"); - - AuthResponse response = authService.login(request); - - verify(authenticationManager).authenticate( - new UsernamePasswordAuthenticationToken("alice", "plain-password")); - assertThat(response.token()).isEqualTo("jwt-token"); - assertThat(response.user().email()).isEqualTo("alice@example.com"); - } - - @Test - void shouldThrowBusinessExceptionWhenAuthenticationFails() { - LoginRequest request = new LoginRequest("alice", "wrong-password"); - when(authenticationManager.authenticate(any())) - .thenThrow(new BadCredentialsException("bad credentials")); - - assertThatThrownBy(() -> authService.login(request)) - .isInstanceOf(BusinessException.class) - .hasMessageContaining("用户名或密码错误"); - } -} diff --git a/.history/backend/src/test/java/com/yoyuzh/auth/AuthServiceTest_20260314112620.java b/.history/backend/src/test/java/com/yoyuzh/auth/AuthServiceTest_20260314112620.java deleted file mode 100644 index d54392b..0000000 --- a/.history/backend/src/test/java/com/yoyuzh/auth/AuthServiceTest_20260314112620.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.yoyuzh.auth; - -import com.yoyuzh.auth.dto.AuthResponse; -import com.yoyuzh.auth.dto.LoginRequest; -import com.yoyuzh.auth.dto.RegisterRequest; -import com.yoyuzh.common.BusinessException; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.time.LocalDateTime; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class AuthServiceTest { - - @Mock - private UserRepository userRepository; - - @Mock - private PasswordEncoder passwordEncoder; - - @Mock - private AuthenticationManager authenticationManager; - - @Mock - private JwtTokenProvider jwtTokenProvider; - - @InjectMocks - private AuthService authService; - - @Test - void shouldRegisterUserWithEncryptedPassword() { - RegisterRequest request = new RegisterRequest("alice", "alice@example.com", "plain-password"); - when(userRepository.existsByUsername("alice")).thenReturn(false); - when(userRepository.existsByEmail("alice@example.com")).thenReturn(false); - when(passwordEncoder.encode("plain-password")).thenReturn("encoded-password"); - when(userRepository.save(any(User.class))).thenAnswer(invocation -> { - User user = invocation.getArgument(0); - user.setId(1L); - user.setCreatedAt(LocalDateTime.now()); - return user; - }); - when(jwtTokenProvider.generateToken(1L, "alice")).thenReturn("jwt-token"); - - AuthResponse response = authService.register(request); - - assertThat(response.token()).isEqualTo("jwt-token"); - assertThat(response.user().username()).isEqualTo("alice"); - verify(passwordEncoder).encode("plain-password"); - } - - @Test - void shouldRejectDuplicateUsernameOnRegister() { - RegisterRequest request = new RegisterRequest("alice", "alice@example.com", "plain-password"); - when(userRepository.existsByUsername("alice")).thenReturn(true); - - assertThatThrownBy(() -> authService.register(request)) - .isInstanceOf(BusinessException.class) - .hasMessageContaining("用户名已存在"); - } - - @Test - void shouldLoginAndReturnToken() { - LoginRequest request = new LoginRequest("alice", "plain-password"); - User user = new User(); - user.setId(1L); - user.setUsername("alice"); - user.setEmail("alice@example.com"); - user.setPasswordHash("encoded-password"); - user.setCreatedAt(LocalDateTime.now()); - when(userRepository.findByUsername("alice")).thenReturn(Optional.of(user)); - when(jwtTokenProvider.generateToken(1L, "alice")).thenReturn("jwt-token"); - - AuthResponse response = authService.login(request); - - verify(authenticationManager).authenticate( - new UsernamePasswordAuthenticationToken("alice", "plain-password")); - assertThat(response.token()).isEqualTo("jwt-token"); - assertThat(response.user().email()).isEqualTo("alice@example.com"); - } - - @Test - void shouldThrowBusinessExceptionWhenAuthenticationFails() { - LoginRequest request = new LoginRequest("alice", "wrong-password"); - when(authenticationManager.authenticate(any())) - .thenThrow(new BadCredentialsException("bad credentials")); - - assertThatThrownBy(() -> authService.login(request)) - .isInstanceOf(BusinessException.class) - .hasMessageContaining("用户名或密码错误"); - } -} diff --git a/.history/front/dist/index_20260317151413.html b/.history/front/dist/index_20260317151413.html deleted file mode 100644 index f999315..0000000 --- a/.history/front/dist/index_20260317151413.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - My Google AI Studio App - - - - -
- - - diff --git a/.history/front/dist/index_20260317151445.html b/.history/front/dist/index_20260317151445.html deleted file mode 100644 index 82eb65d..0000000 --- a/.history/front/dist/index_20260317151445.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - My_ - - - - -
- - - diff --git a/.history/front/dist/index_20260317151458.html b/.history/front/dist/index_20260317151458.html deleted file mode 100644 index a5167ce..0000000 --- a/.history/front/dist/index_20260317151458.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - My Person - - - - -
- - - diff --git a/.history/front/dist/index_20260317151502.html b/.history/front/dist/index_20260317151502.html deleted file mode 100644 index a9eb91e..0000000 --- a/.history/front/dist/index_20260317151502.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - My Personal - - - - -
- - - diff --git a/.history/front/dist/index_20260317151507.html b/.history/front/dist/index_20260317151507.html deleted file mode 100644 index 12abf90..0000000 --- a/.history/front/dist/index_20260317151507.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - My Personal Por - - - - -
- - - diff --git a/.history/front/dist/index_20260317151508.html b/.history/front/dist/index_20260317151508.html deleted file mode 100644 index 86d72bc..0000000 --- a/.history/front/dist/index_20260317151508.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - My Personal <Portal></Portal> - - - - -
- - - diff --git a/.history/front/dist/index_20260317151511.html b/.history/front/dist/index_20260317151511.html deleted file mode 100644 index 05d2b23..0000000 --- a/.history/front/dist/index_20260317151511.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - My Personal </Portal> - - - - -
- - - diff --git a/.history/front/dist/index_20260317151516.html b/.history/front/dist/index_20260317151516.html deleted file mode 100644 index 52ffd5c..0000000 --- a/.history/front/dist/index_20260317151516.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - My Personal P - - - - -
- - - diff --git a/.history/front/dist/index_20260317151518.html b/.history/front/dist/index_20260317151518.html deleted file mode 100644 index 37746ac..0000000 --- a/.history/front/dist/index_20260317151518.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - My Personal Portal - - - - -
- - - diff --git a/.history/front/src/pages/School_20260314124227.tsx b/.history/front/src/pages/School_20260314124227.tsx deleted file mode 100644 index 4b99a94..0000000 --- a/.history/front/src/pages/School_20260314124227.tsx +++ /dev/null @@ -1,430 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { motion } from 'motion/react'; -import { Award, BookOpen, Calendar, ChevronRight, GraduationCap, Lock, Search, User } from 'lucide-react'; - -import { Button } from '@/src/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/src/components/ui/card'; -import { Input } from '@/src/components/ui/input'; -import { apiRequest } from '@/src/lib/api'; -import { readCachedValue, writeCachedValue } from '@/src/lib/cache'; -import { getSchoolResultsCacheKey, readStoredSchoolQuery, writeStoredSchoolQuery } from '@/src/lib/page-cache'; -import { buildScheduleTable } from '@/src/lib/schedule-table'; -import type { CourseResponse, GradeResponse } from '@/src/lib/types'; -import { cn } from '@/src/lib/utils'; - -function formatSections(startTime?: number | null, endTime?: number | null) { - if (!startTime || !endTime) { - return '节次待定'; - } - - return `第 ${startTime}-${endTime} 节`; -} - -export default function School() { - const storedQuery = readStoredSchoolQuery(); - const initialStudentId = storedQuery?.studentId ?? '2023123456'; - const initialSemester = storedQuery?.semester ?? '2025-spring'; - const initialCachedResults = readCachedValue<{ - queried: boolean; - schedule: CourseResponse[]; - grades: GradeResponse[]; - }>(getSchoolResultsCacheKey(initialStudentId, initialSemester)); - - const [activeTab, setActiveTab] = useState<'schedule' | 'grades'>('schedule'); - const [studentId, setStudentId] = useState(initialStudentId); - const [password, setPassword] = useState('password123'); - const [semester, setSemester] = useState(initialSemester); - const [loading, setLoading] = useState(false); - const [queried, setQueried] = useState(initialCachedResults?.queried ?? false); - const [schedule, setSchedule] = useState(initialCachedResults?.schedule ?? []); - const [grades, setGrades] = useState(initialCachedResults?.grades ?? []); - - const averageGrade = useMemo(() => { - if (grades.length === 0) { - return '0.0'; - } - - const sum = grades.reduce((total, item) => total + (item.grade ?? 0), 0); - return (sum / grades.length).toFixed(1); - }, [grades]); - - const loadSchoolData = async ( - nextStudentId: string, - nextSemester: string, - options: { background?: boolean } = {}, - ) => { - const cacheKey = getSchoolResultsCacheKey(nextStudentId, nextSemester); - const cachedResults = readCachedValue<{ - queried: boolean; - schedule: CourseResponse[]; - grades: GradeResponse[]; - }>(cacheKey); - - if (!options.background) { - setLoading(true); - } - - writeStoredSchoolQuery({ - studentId: nextStudentId, - semester: nextSemester, - }); - - try { - const queryString = new URLSearchParams({ - studentId: nextStudentId, - semester: nextSemester, - }).toString(); - - const [scheduleData, gradeData] = await Promise.all([ - apiRequest(`/cqu/schedule?${queryString}`), - apiRequest(`/cqu/grades?${queryString}`), - ]); - - setQueried(true); - setSchedule(scheduleData); - setGrades(gradeData); - writeCachedValue(cacheKey, { - queried: true, - studentId: nextStudentId, - semester: nextSemester, - schedule: scheduleData, - grades: gradeData, - }); - } catch { - if (!cachedResults) { - setQueried(false); - setSchedule([]); - setGrades([]); - } - } finally { - if (!options.background) { - setLoading(false); - } - } - }; - - useEffect(() => { - if (!storedQuery) { - return; - } - - loadSchoolData(storedQuery.studentId, storedQuery.semester, { - background: true, - }).catch(() => undefined); - }, []); - - const handleQuery = async (event: React.FormEvent) => { - event.preventDefault(); - await loadSchoolData(studentId, semester); - }; - - return ( -
-
- - - - - 教务查询 - - 输入学号、密码和学期后同步课表与成绩。 - - -
-
- -
- - setStudentId(event.target.value)} className="pl-9 bg-black/20" required /> -
-
-
- -
- - setPassword(event.target.value)} className="pl-9 bg-black/20" required /> -
-
-
- - -
- -
- - -
-
-
-
- - - - - - 数据摘要 - - 展示当前缓存或最近一次查询结果。 - - - {queried ? ( -
- - - -
- ) : ( -
- -

暂无缓存数据,请先执行查询。

-
- )} -
-
-
- -
- - -
- - - {activeTab === 'schedule' ? : } - -
- ); -} - -function DatabaseIcon(props: React.SVGProps) { - return ( - - - - - - ); -} - -function SummaryItem({ - label, - value, - icon: Icon, -}: { - label: string; - value: string; - icon: React.ComponentType<{ className?: string }>; -}) { - return ( -
-
- -
-
-

{label}

-

{value}

-
-
- ); -} - -function ScheduleView({ queried, schedule }: { queried: boolean; schedule: CourseResponse[] }) { - if (!queried) { - return ( - - - -

请先查询课表

-
-
- ); - } - - const days = ['周一', '周二', '周三', '周四', '周五']; - const periodLabels: Record<'morning' | 'afternoon' | 'evening', string> = { - morning: '上午', - afternoon: '下午', - evening: '晚上', - }; - const rows = buildScheduleTable(schedule); - - return ( - - - 本周课表 - 上午 4 节、下午 4 节、晚上 4 节。没有课的格子保持为空。 - - -
- - - - - {days.map((day) => ( - - ))} - - - - {rows.map((row) => ( - - - {row.slots.map((slot, index) => { - if (slot.type === 'covered') { - return null; - } - - if (slot.type === 'empty') { - return ( - - ); - })} - - ))} - -
- 节次 - - {day} -
-
-

{periodLabels[row.period]}

-

第 {row.section} 节

-
-
- ); - } - - return ( - -
-

- {formatSections(slot.course?.startTime, slot.course?.endTime)} -

-

- {slot.course?.courseName} -

-

- {slot.course?.classroom ?? '教室待定'} -

-

- {slot.course?.teacher ?? '任课教师待定'} -

-
-
-
-
-
- ); -} - -function GradesView({ queried, grades }: { queried: boolean; grades: GradeResponse[] }) { - if (!queried) { - return ( - - - -

请先查询成绩

-
-
- ); - } - - const terms = grades.reduce>((accumulator, grade) => { - const term = grade.semester ?? '未分类'; - if (!accumulator[term]) { - accumulator[term] = []; - } - accumulator[term].push(grade.grade ?? 0); - return accumulator; - }, {}); - - const getScoreStyle = (score: number) => { - if (score >= 95) return 'bg-[#336EFF]/50 text-white'; - if (score >= 90) return 'bg-[#336EFF]/40 text-white/90'; - if (score >= 85) return 'bg-[#336EFF]/30 text-white/80'; - if (score >= 80) return 'bg-slate-700/60 text-white/70'; - if (score >= 75) return 'bg-slate-700/40 text-white/60'; - return 'bg-slate-800/60 text-white/50'; - }; - - return ( - - - 成绩热力图 - - -
- {Object.entries(terms).map(([term, scores]) => ( -
-

{term}

-
- {scores.map((score, index) => ( -
- {score} -
- ))} -
-
- ))} - {Object.keys(terms).length === 0 &&
暂无成绩数据
} -
-
-
- ); -} diff --git a/.history/front/src/pages/School_20260314213832.tsx b/.history/front/src/pages/School_20260314213832.tsx deleted file mode 100644 index 2269f12..0000000 --- a/.history/front/src/pages/School_20260314213832.tsx +++ /dev/null @@ -1,430 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { motion } from 'motion/react'; -import { Award, BookOpen, Calendar, ChevronRight, GraduationCap, Lock, MapPin, Search, User } from 'lucide-react'; - -import { Button } from '@/src/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/src/components/ui/card'; -import { Input } from '@/src/components/ui/input'; -import { apiRequest } from '@/src/lib/api'; -import { readCachedValue, writeCachedValue } from '@/src/lib/cache'; -import { getSchoolResultsCacheKey, readStoredSchoolQuery, writeStoredSchoolQuery } from '@/src/lib/page-cache'; -import { buildScheduleTable } from '@/src/lib/schedule-table'; -import type { CourseResponse, GradeResponse } from '@/src/lib/types'; -import { cn } from '@/src/lib/utils'; - -function formatSections(startTime?: number | null, endTime?: number | null) { - if (!startTime || !endTime) { - return '节次待定'; - } - - return `第 ${startTime}-${endTime} 节`; -} - -export default function School() { - const storedQuery = readStoredSchoolQuery(); - const initialStudentId = storedQuery?.studentId ?? '2023123456'; - const initialSemester = storedQuery?.semester ?? '2025-spring'; - const initialCachedResults = readCachedValue<{ - queried: boolean; - schedule: CourseResponse[]; - grades: GradeResponse[]; - }>(getSchoolResultsCacheKey(initialStudentId, initialSemester)); - - const [activeTab, setActiveTab] = useState<'schedule' | 'grades'>('schedule'); - const [studentId, setStudentId] = useState(initialStudentId); - const [password, setPassword] = useState('password123'); - const [semester, setSemester] = useState(initialSemester); - const [loading, setLoading] = useState(false); - const [queried, setQueried] = useState(initialCachedResults?.queried ?? false); - const [schedule, setSchedule] = useState(initialCachedResults?.schedule ?? []); - const [grades, setGrades] = useState(initialCachedResults?.grades ?? []); - - const averageGrade = useMemo(() => { - if (grades.length === 0) { - return '0.0'; - } - - const sum = grades.reduce((total, item) => total + (item.grade ?? 0), 0); - return (sum / grades.length).toFixed(1); - }, [grades]); - - const loadSchoolData = async ( - nextStudentId: string, - nextSemester: string, - options: { background?: boolean } = {}, - ) => { - const cacheKey = getSchoolResultsCacheKey(nextStudentId, nextSemester); - const cachedResults = readCachedValue<{ - queried: boolean; - schedule: CourseResponse[]; - grades: GradeResponse[]; - }>(cacheKey); - - if (!options.background) { - setLoading(true); - } - - writeStoredSchoolQuery({ - studentId: nextStudentId, - semester: nextSemester, - }); - - try { - const queryString = new URLSearchParams({ - studentId: nextStudentId, - semester: nextSemester, - }).toString(); - - const [scheduleData, gradeData] = await Promise.all([ - apiRequest(`/cqu/schedule?${queryString}`), - apiRequest(`/cqu/grades?${queryString}`), - ]); - - setQueried(true); - setSchedule(scheduleData); - setGrades(gradeData); - writeCachedValue(cacheKey, { - queried: true, - studentId: nextStudentId, - semester: nextSemester, - schedule: scheduleData, - grades: gradeData, - }); - } catch { - if (!cachedResults) { - setQueried(false); - setSchedule([]); - setGrades([]); - } - } finally { - if (!options.background) { - setLoading(false); - } - } - }; - - useEffect(() => { - if (!storedQuery) { - return; - } - - loadSchoolData(storedQuery.studentId, storedQuery.semester, { - background: true, - }).catch(() => undefined); - }, []); - - const handleQuery = async (event: React.FormEvent) => { - event.preventDefault(); - await loadSchoolData(studentId, semester); - }; - - return ( -
-
- - - - - 教务查询 - - 输入学号、密码和学期后同步课表与成绩。 - - -
-
- -
- - setStudentId(event.target.value)} className="pl-9 bg-black/20" required /> -
-
-
- -
- - setPassword(event.target.value)} className="pl-9 bg-black/20" required /> -
-
-
- - -
- -
- - -
-
-
-
- - - - - - 数据摘要 - - 展示当前缓存或最近一次查询结果。 - - - {queried ? ( -
- - - -
- ) : ( -
- -

暂无缓存数据,请先执行查询。

-
- )} -
-
-
- -
- - -
- - - {activeTab === 'schedule' ? : } - -
- ); -} - -function DatabaseIcon(props: React.SVGProps) { - return ( - - - - - - ); -} - -function SummaryItem({ - label, - value, - icon: Icon, -}: { - label: string; - value: string; - icon: React.ComponentType<{ className?: string }>; -}) { - return ( -
-
- -
-
-

{label}

-

{value}

-
-
- ); -} - -function ScheduleView({ queried, schedule }: { queried: boolean; schedule: CourseResponse[] }) { - if (!queried) { - return ( - - - -

请先查询课表

-
-
- ); - } - - const days = ['周一', '周二', '周三', '周四', '周五']; - const periodLabels: Record<'morning' | 'afternoon' | 'evening', string> = { - morning: '上午', - afternoon: '下午', - evening: '晚上', - }; - const rows = buildScheduleTable(schedule); - - return ( - - - 本周课表 - 上午 4 节、下午 4 节、晚上 4 节。没有课的格子保持为空。 - - -
- - - - - {days.map((day) => ( - - ))} - - - - {rows.map((row) => ( - - - {row.slots.map((slot, index) => { - if (slot.type === 'covered') { - return null; - } - - if (slot.type === 'empty') { - return ( - - ); - })} - - ))} - -
- 节次 - - {day} -
-
-

{periodLabels[row.period]}

-

第 {row.section} 节

-
-
- ); - } - - return ( - -
-

- {formatSections(slot.course?.startTime, slot.course?.endTime)} -

-

- {slot.course?.courseName} -

-

- {slot.course?.classroom ?? '教室待定'} -

-

- {slot.course?.teacher ?? '任课教师待定'} -

-
-
-
-
-
- ); -} - -function GradesView({ queried, grades }: { queried: boolean; grades: GradeResponse[] }) { - if (!queried) { - return ( - - - -

请先查询成绩

-
-
- ); - } - - const terms = grades.reduce>((accumulator, grade) => { - const term = grade.semester ?? '未分类'; - if (!accumulator[term]) { - accumulator[term] = []; - } - accumulator[term].push(grade.grade ?? 0); - return accumulator; - }, {}); - - const getScoreStyle = (score: number) => { - if (score >= 95) return 'bg-[#336EFF]/50 text-white'; - if (score >= 90) return 'bg-[#336EFF]/40 text-white/90'; - if (score >= 85) return 'bg-[#336EFF]/30 text-white/80'; - if (score >= 80) return 'bg-slate-700/60 text-white/70'; - if (score >= 75) return 'bg-slate-700/40 text-white/60'; - return 'bg-slate-800/60 text-white/50'; - }; - - return ( - - - 成绩热力图 - - -
- {Object.entries(terms).map(([term, scores]) => ( -
-

{term}

-
- {scores.map((score, index) => ( -
- {score} -
- ))} -
-
- ))} - {Object.keys(terms).length === 0 &&
暂无成绩数据
} -
-
-
- ); -} diff --git a/.history/front/src/pages/School_20260314213920.tsx b/.history/front/src/pages/School_20260314213920.tsx deleted file mode 100644 index 315c262..0000000 --- a/.history/front/src/pages/School_20260314213920.tsx +++ /dev/null @@ -1,512 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { motion } from 'motion/react'; -import { Award, BookOpen, Calendar, ChevronRight, GraduationCap, Lock, MapPin, Search, User } from 'lucide-react'; - -import { Button } from '@/src/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/src/components/ui/card'; -import { Input } from '@/src/components/ui/input'; -import { apiRequest } from '@/src/lib/api'; -import { readCachedValue, writeCachedValue } from '@/src/lib/cache'; -import { getSchoolResultsCacheKey, readStoredSchoolQuery, writeStoredSchoolQuery } from '@/src/lib/page-cache'; -import { buildScheduleTable } from '@/src/lib/schedule-table'; -import type { CourseResponse, GradeResponse } from '@/src/lib/types'; -import { cn } from '@/src/lib/utils'; - -function formatSections(startTime?: number | null, endTime?: number | null) { - if (!startTime || !endTime) { - return '节次待定'; - } - - return `第 ${startTime}-${endTime} 节`; -} - -export default function School() { - const storedQuery = readStoredSchoolQuery(); - const initialStudentId = storedQuery?.studentId ?? '2023123456'; - const initialSemester = storedQuery?.semester ?? '2025-spring'; - const initialCachedResults = readCachedValue<{ - queried: boolean; - schedule: CourseResponse[]; - grades: GradeResponse[]; - }>(getSchoolResultsCacheKey(initialStudentId, initialSemester)); - - const [activeTab, setActiveTab] = useState<'schedule' | 'grades'>('schedule'); - const [studentId, setStudentId] = useState(initialStudentId); - const [password, setPassword] = useState('password123'); - const [semester, setSemester] = useState(initialSemester); - const [loading, setLoading] = useState(false); - const [queried, setQueried] = useState(initialCachedResults?.queried ?? false); - const [schedule, setSchedule] = useState(initialCachedResults?.schedule ?? []); - const [grades, setGrades] = useState(initialCachedResults?.grades ?? []); - - const averageGrade = useMemo(() => { - if (grades.length === 0) { - return '0.0'; - } - - const sum = grades.reduce((total, item) => total + (item.grade ?? 0), 0); - return (sum / grades.length).toFixed(1); - }, [grades]); - - const loadSchoolData = async ( - nextStudentId: string, - nextSemester: string, - options: { background?: boolean } = {}, - ) => { - const cacheKey = getSchoolResultsCacheKey(nextStudentId, nextSemester); - const cachedResults = readCachedValue<{ - queried: boolean; - schedule: CourseResponse[]; - grades: GradeResponse[]; - }>(cacheKey); - - if (!options.background) { - setLoading(true); - } - - writeStoredSchoolQuery({ - studentId: nextStudentId, - semester: nextSemester, - }); - - try { - const queryString = new URLSearchParams({ - studentId: nextStudentId, - semester: nextSemester, - }).toString(); - - const [scheduleData, gradeData] = await Promise.all([ - apiRequest(`/cqu/schedule?${queryString}`), - apiRequest(`/cqu/grades?${queryString}`), - ]); - - setQueried(true); - setSchedule(scheduleData); - setGrades(gradeData); - writeCachedValue(cacheKey, { - queried: true, - studentId: nextStudentId, - semester: nextSemester, - schedule: scheduleData, - grades: gradeData, - }); - } catch { - if (!cachedResults) { - setQueried(false); - setSchedule([]); - setGrades([]); - } - } finally { - if (!options.background) { - setLoading(false); - } - } - }; - - useEffect(() => { - if (!storedQuery) { - return; - } - - loadSchoolData(storedQuery.studentId, storedQuery.semester, { - background: true, - }).catch(() => undefined); - }, []); - - const handleQuery = async (event: React.FormEvent) => { - event.preventDefault(); - await loadSchoolData(studentId, semester); - }; - - return ( -
-
- - - - - 教务查询 - - 输入学号、密码和学期后同步课表与成绩。 - - -
-
- -
- - setStudentId(event.target.value)} className="pl-9 bg-black/20" required /> -
-
-
- -
- - setPassword(event.target.value)} className="pl-9 bg-black/20" required /> -
-
-
- - -
- -
- - -
-
-
-
- - - - - - 数据摘要 - - 展示当前缓存或最近一次查询结果。 - - - {queried ? ( -
- - - -
- ) : ( -
- -

暂无缓存数据,请先执行查询。

-
- )} -
-
-
- -
- - -
- - - {activeTab === 'schedule' ? : } - -
- ); -} - -function DatabaseIcon(props: React.SVGProps) { - return ( - - - - - - ); -} - -function SummaryItem({ - label, - value, - icon: Icon, -}: { - label: string; - value: string; - icon: React.ComponentType<{ className?: string }>; -}) { - return ( -
-
- -
-
-

{label}

-

{value}

-
-
- ); -} - -function getCourseTheme(courseName?: string) { - if (!courseName) { - return { - bg: 'bg-slate-500/10', - border: 'border-slate-500/20', - text: 'text-slate-300', - }; - } - - const themes = [ - { - bg: 'bg-blue-500/20 hover:bg-blue-500/30', - border: 'border-blue-400/30', - text: 'text-blue-100', - }, - { - bg: 'bg-indigo-500/20 hover:bg-indigo-500/30', - border: 'border-indigo-400/30', - text: 'text-indigo-100', - }, - { - bg: 'bg-cyan-500/20 hover:bg-cyan-500/30', - border: 'border-cyan-400/30', - text: 'text-cyan-100', - }, - { - bg: 'bg-sky-500/20 hover:bg-sky-500/30', - border: 'border-sky-400/30', - text: 'text-sky-100', - }, - { - bg: 'bg-violet-500/20 hover:bg-violet-500/30', - border: 'border-violet-400/30', - text: 'text-violet-100', - }, - { - bg: 'bg-teal-500/20 hover:bg-teal-500/30', - border: 'border-teal-400/30', - text: 'text-teal-100', - }, - ]; - - let hash = 0; - for (let i = 0; i < courseName.length; i += 1) { - hash = courseName.charCodeAt(i) + ((hash << 5) - hash); - } - return themes[Math.abs(hash) % themes.length]; -} - -function ScheduleView({ queried, schedule }: { queried: boolean; schedule: CourseResponse[] }) { - if (!queried) { - return ( - - - -

请先查询课表

-
-
- ); - } - - const days = ['周一', '周二', '周三', '周四', '周五']; - const rows = buildScheduleTable(schedule); - - return ( - - -
-
- 我的课表 - 2025 春季学期 -
-
- - - {' '} - 必修 - - - - {' '} - 选修 - -
-
-
- -
-
- - - - - ))} - - - - {rows.map((row, index) => ( - - {/* Morning/Afternoon label */} - {index % 4 === 0 && ( - - )} - - {/* Section Number */} - - - {/* Cells */} - {row.slots.map((slot, colIdx) => { - if (slot.type === 'covered') return null; - if (slot.type === 'empty') { - return ( - - ); - })} - - ))} - -
- - {days.map((d) => ( - - {d} -
-
- {row.section <= 4 ? '上午' : row.section <= 8 ? '下午' : '晚上'} -
-
-
{row.section}
-
- ); - } - - const theme = getCourseTheme(slot.course?.courseName ?? ''); - return ( - -
-
-

- {slot.course?.courseName} -

-

- - {' '} - {slot.course?.classroom ?? '未知'} -

-
-
- - {slot.course?.teacher ?? '老师'} - -
-
-
-
-
-
-
- ); -} - -function GradesView({ queried, grades }: { queried: boolean; grades: GradeResponse[] }) { - if (!queried) { - return ( - - - -

请先查询成绩

-
-
- ); - } - - const terms = grades.reduce>((accumulator, grade) => { - const term = grade.semester ?? '未分类'; - if (!accumulator[term]) { - accumulator[term] = []; - } - accumulator[term].push(grade.grade ?? 0); - return accumulator; - }, {}); - - const getScoreStyle = (score: number) => { - if (score >= 95) return 'bg-[#336EFF]/50 text-white'; - if (score >= 90) return 'bg-[#336EFF]/40 text-white/90'; - if (score >= 85) return 'bg-[#336EFF]/30 text-white/80'; - if (score >= 80) return 'bg-slate-700/60 text-white/70'; - if (score >= 75) return 'bg-slate-700/40 text-white/60'; - return 'bg-slate-800/60 text-white/50'; - }; - - return ( - - - 成绩热力图 - - -
- {Object.entries(terms).map(([term, scores]) => ( -
-

{term}

-
- {scores.map((score, index) => ( -
- {score} -
- ))} -
-
- ))} - {Object.keys(terms).length === 0 &&
暂无成绩数据
} -
-
-
- ); -} diff --git a/.history/front/src/pages/School_20260314213942.tsx b/.history/front/src/pages/School_20260314213942.tsx deleted file mode 100644 index f4c1e9d..0000000 --- a/.history/front/src/pages/School_20260314213942.tsx +++ /dev/null @@ -1,504 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { motion } from 'motion/react'; -import { Award, BookOpen, Calendar, Lock, MapPin, Search, User } from 'lucide-react'; - -import { Button } from '@/src/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/src/components/ui/card'; -import { Input } from '@/src/components/ui/input'; -import { apiRequest } from '@/src/lib/api'; -import { readCachedValue, writeCachedValue } from '@/src/lib/cache'; -import { getSchoolResultsCacheKey, readStoredSchoolQuery, writeStoredSchoolQuery } from '@/src/lib/page-cache'; -import { buildScheduleTable } from '@/src/lib/schedule-table'; -import type { CourseResponse, GradeResponse } from '@/src/lib/types'; -import { cn } from '@/src/lib/utils'; - -export default function School() { - const storedQuery = readStoredSchoolQuery(); - const initialStudentId = storedQuery?.studentId ?? '2023123456'; - const initialSemester = storedQuery?.semester ?? '2025-spring'; - const initialCachedResults = readCachedValue<{ - queried: boolean; - schedule: CourseResponse[]; - grades: GradeResponse[]; - }>(getSchoolResultsCacheKey(initialStudentId, initialSemester)); - - const [activeTab, setActiveTab] = useState<'schedule' | 'grades'>('schedule'); - const [studentId, setStudentId] = useState(initialStudentId); - const [password, setPassword] = useState('password123'); - const [semester, setSemester] = useState(initialSemester); - const [loading, setLoading] = useState(false); - const [queried, setQueried] = useState(initialCachedResults?.queried ?? false); - const [schedule, setSchedule] = useState(initialCachedResults?.schedule ?? []); - const [grades, setGrades] = useState(initialCachedResults?.grades ?? []); - - const averageGrade = useMemo(() => { - if (grades.length === 0) { - return '0.0'; - } - - const sum = grades.reduce((total, item) => total + (item.grade ?? 0), 0); - return (sum / grades.length).toFixed(1); - }, [grades]); - - const loadSchoolData = async ( - nextStudentId: string, - nextSemester: string, - options: { background?: boolean } = {}, - ) => { - const cacheKey = getSchoolResultsCacheKey(nextStudentId, nextSemester); - const cachedResults = readCachedValue<{ - queried: boolean; - schedule: CourseResponse[]; - grades: GradeResponse[]; - }>(cacheKey); - - if (!options.background) { - setLoading(true); - } - - writeStoredSchoolQuery({ - studentId: nextStudentId, - semester: nextSemester, - }); - - try { - const queryString = new URLSearchParams({ - studentId: nextStudentId, - semester: nextSemester, - }).toString(); - - const [scheduleData, gradeData] = await Promise.all([ - apiRequest(`/cqu/schedule?${queryString}`), - apiRequest(`/cqu/grades?${queryString}`), - ]); - - setQueried(true); - setSchedule(scheduleData); - setGrades(gradeData); - writeCachedValue(cacheKey, { - queried: true, - studentId: nextStudentId, - semester: nextSemester, - schedule: scheduleData, - grades: gradeData, - }); - } catch { - if (!cachedResults) { - setQueried(false); - setSchedule([]); - setGrades([]); - } - } finally { - if (!options.background) { - setLoading(false); - } - } - }; - - useEffect(() => { - if (!storedQuery) { - return; - } - - loadSchoolData(storedQuery.studentId, storedQuery.semester, { - background: true, - }).catch(() => undefined); - }, []); - - const handleQuery = async (event: React.FormEvent) => { - event.preventDefault(); - await loadSchoolData(studentId, semester); - }; - - return ( -
-
- - - - - 教务查询 - - 输入学号、密码和学期后同步课表与成绩。 - - -
-
- -
- - setStudentId(event.target.value)} className="pl-9 bg-black/20" required /> -
-
-
- -
- - setPassword(event.target.value)} className="pl-9 bg-black/20" required /> -
-
-
- - -
- -
- - -
-
-
-
- - - - - - 数据摘要 - - 展示当前缓存或最近一次查询结果。 - - - {queried ? ( -
- - - -
- ) : ( -
- -

暂无缓存数据,请先执行查询。

-
- )} -
-
-
- -
- - -
- - - {activeTab === 'schedule' ? : } - -
- ); -} - -function DatabaseIcon(props: React.SVGProps) { - return ( - - - - - - ); -} - -function SummaryItem({ - label, - value, - icon: Icon, -}: { - label: string; - value: string; - icon: React.ComponentType<{ className?: string }>; -}) { - return ( -
-
- -
-
-

{label}

-

{value}

-
-
- ); -} - -function getCourseTheme(courseName?: string) { - if (!courseName) { - return { - bg: 'bg-slate-500/10', - border: 'border-slate-500/20', - text: 'text-slate-300', - }; - } - - const themes = [ - { - bg: 'bg-blue-500/20 hover:bg-blue-500/30', - border: 'border-blue-400/30', - text: 'text-blue-100', - }, - { - bg: 'bg-indigo-500/20 hover:bg-indigo-500/30', - border: 'border-indigo-400/30', - text: 'text-indigo-100', - }, - { - bg: 'bg-cyan-500/20 hover:bg-cyan-500/30', - border: 'border-cyan-400/30', - text: 'text-cyan-100', - }, - { - bg: 'bg-sky-500/20 hover:bg-sky-500/30', - border: 'border-sky-400/30', - text: 'text-sky-100', - }, - { - bg: 'bg-violet-500/20 hover:bg-violet-500/30', - border: 'border-violet-400/30', - text: 'text-violet-100', - }, - { - bg: 'bg-teal-500/20 hover:bg-teal-500/30', - border: 'border-teal-400/30', - text: 'text-teal-100', - }, - ]; - - let hash = 0; - for (let i = 0; i < courseName.length; i += 1) { - hash = courseName.charCodeAt(i) + ((hash << 5) - hash); - } - return themes[Math.abs(hash) % themes.length]; -} - -function ScheduleView({ queried, schedule }: { queried: boolean; schedule: CourseResponse[] }) { - if (!queried) { - return ( - - - -

请先查询课表

-
-
- ); - } - - const days = ['周一', '周二', '周三', '周四', '周五']; - const rows = buildScheduleTable(schedule); - - return ( - - -
-
- 我的课表 - 2025 春季学期 -
-
- - - {' '} - 必修 - - - - {' '} - 选修 - -
-
-
- -
-
- - - - - ))} - - - - {rows.map((row, index) => ( - - {/* Morning/Afternoon label */} - {index % 4 === 0 && ( - - )} - - {/* Section Number */} - - - {/* Cells */} - {row.slots.map((slot, colIdx) => { - if (slot.type === 'covered') return null; - if (slot.type === 'empty') { - return ( - - ); - })} - - ))} - -
- - {days.map((d) => ( - - {d} -
-
- {row.section <= 4 ? '上午' : row.section <= 8 ? '下午' : '晚上'} -
-
-
{row.section}
-
- ); - } - - const theme = getCourseTheme(slot.course?.courseName ?? ''); - return ( - -
-
-

- {slot.course?.courseName} -

-

- - {' '} - {slot.course?.classroom ?? '未知'} -

-
-
- - {slot.course?.teacher ?? '老师'} - -
-
-
-
-
-
-
- ); -} - -function GradesView({ queried, grades }: { queried: boolean; grades: GradeResponse[] }) { - if (!queried) { - return ( - - - -

请先查询成绩

-
-
- ); - } - - const terms = grades.reduce>((accumulator, grade) => { - const term = grade.semester ?? '未分类'; - if (!accumulator[term]) { - accumulator[term] = []; - } - accumulator[term].push(grade.grade ?? 0); - return accumulator; - }, {}); - - const getScoreStyle = (score: number) => { - if (score >= 95) return 'bg-[#336EFF]/50 text-white'; - if (score >= 90) return 'bg-[#336EFF]/40 text-white/90'; - if (score >= 85) return 'bg-[#336EFF]/30 text-white/80'; - if (score >= 80) return 'bg-slate-700/60 text-white/70'; - if (score >= 75) return 'bg-slate-700/40 text-white/60'; - return 'bg-slate-800/60 text-white/50'; - }; - - return ( - - - 成绩热力图 - - -
- {Object.entries(terms).map(([term, scores]) => ( -
-

{term}

-
- {scores.map((score, index) => ( -
- {score} -
- ))} -
-
- ))} - {Object.keys(terms).length === 0 &&
暂无成绩数据
} -
-
-
- ); -} diff --git a/.history/开发测试账号_20260314122451.md b/.history/开发测试账号_20260314122451.md deleted file mode 100644 index a0c463a..0000000 --- a/.history/开发测试账号_20260314122451.md +++ /dev/null @@ -1,18 +0,0 @@ -# 开发测试账号 - -以下账号会在后端以 `dev` profile 启动时自动初始化。 - -## 门户账号 - -| 门户用户名 | 门户密码 | 教务学号 | 教务密码 | 查询学期 | 网盘示例文件 | -| --- | --- | --- | --- | --- | --- | -| `portal-demo` | `portal123456` | `2023123456` | `portal123456` | `2025-spring` | `迎新资料.txt`、`课程规划.md`、`campus-shot.png` | -| `portal-study` | `study123456` | `2022456789` | `study123456` | `2024-fall` | `实验数据.csv`、`论文草稿.md`、`data-chart.png` | -| `portal-design` | `design123456` | `2021789012` | `design123456` | `2024-spring` | `素材清单.txt`、`作品说明.md`、`ui-mockup.png` | - -## 使用说明 - -- 先用上表中的“门户用户名 / 门户密码”登录站点。 -- 登录后进入网盘页,每个用户都会看到自己的 `下载 / 文档 / 图片` 目录,以及各自不同的样例文件。 -- 进入教务页后,填入对应的“教务学号 / 教务密码 / 查询学期”即可看到该用户对应的 mock 教务数据。 -- 当前开发环境的教务密码字段仅用于前端占位,后端主要依据登录态、学号和学期返回该用户的 mock 数据。为避免混淆,直接填表中的教务密码即可。 diff --git a/.history/开发测试账号_20260314225231.md b/.history/开发测试账号_20260314225231.md deleted file mode 100644 index 2773819..0000000 --- a/.history/开发测试账号_20260314225231.md +++ /dev/null @@ -1,18 +0,0 @@ -# 开发测试账号 - -以下账号会在后端以 `dev` profile 启动时自动初始化。 - -## 门户账号 - -| 门户用户名 | 门户密码 | 教务学号 | 教务密码 | 查询学期 | 网盘示例文件 | -| --- | --- | --- | --- | --- | --- | -| `CCC` | `portal123456` | `2023123456` | `portal123456` | `2025-spring` | `迎新资料.txt`、`课程规划.md`、`campus-shot.png` | -| `portal-study` | `study123456` | `2022456789` | `study123456` | `2024-fall` | `实验数据.csv`、`论文草稿.md`、`data-chart.png` | -| `portal-design` | `design123456` | `2021789012` | `design123456` | `2024-spring` | `素材清单.txt`、`作品说明.md`、`ui-mockup.png` | - -## 使用说明 - -- 先用上表中的“门户用户名 / 门户密码”登录站点。 -- 登录后进入网盘页,每个用户都会看到自己的 `下载 / 文档 / 图片` 目录,以及各自不同的样例文件。 -- 进入教务页后,填入对应的“教务学号 / 教务密码 / 查询学期”即可看到该用户对应的 mock 教务数据。 -- 当前开发环境的教务密码字段仅用于前端占位,后端主要依据登录态、学号和学期返回该用户的 mock 数据。为避免混淆,直接填表中的教务密码即可。 diff --git a/.history/开发测试账号_20260314225233.md b/.history/开发测试账号_20260314225233.md deleted file mode 100644 index a0c463a..0000000 --- a/.history/开发测试账号_20260314225233.md +++ /dev/null @@ -1,18 +0,0 @@ -# 开发测试账号 - -以下账号会在后端以 `dev` profile 启动时自动初始化。 - -## 门户账号 - -| 门户用户名 | 门户密码 | 教务学号 | 教务密码 | 查询学期 | 网盘示例文件 | -| --- | --- | --- | --- | --- | --- | -| `portal-demo` | `portal123456` | `2023123456` | `portal123456` | `2025-spring` | `迎新资料.txt`、`课程规划.md`、`campus-shot.png` | -| `portal-study` | `study123456` | `2022456789` | `study123456` | `2024-fall` | `实验数据.csv`、`论文草稿.md`、`data-chart.png` | -| `portal-design` | `design123456` | `2021789012` | `design123456` | `2024-spring` | `素材清单.txt`、`作品说明.md`、`ui-mockup.png` | - -## 使用说明 - -- 先用上表中的“门户用户名 / 门户密码”登录站点。 -- 登录后进入网盘页,每个用户都会看到自己的 `下载 / 文档 / 图片` 目录,以及各自不同的样例文件。 -- 进入教务页后,填入对应的“教务学号 / 教务密码 / 查询学期”即可看到该用户对应的 mock 教务数据。 -- 当前开发环境的教务密码字段仅用于前端占位,后端主要依据登录态、学号和学期返回该用户的 mock 数据。为避免混淆,直接填表中的教务密码即可。 diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 3b66410..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "git.ignoreLimitWarning": true -} \ No newline at end of file