commit 28e150788943592946bb3c5033cf8772f8700a3b Author: colden Date: Sat Dec 20 12:20:43 2025 +0800 formula project diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100755 index 0000000..25ee900 --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,110 @@ + + + 4.0.0 + + f1 + f1db + 1.0-SNAPSHOT + + + 17 + 17 + 17 + UTF-8 + UTF-8 + 3.0.2 + + + + + com.mysql + mysql-connector-j + 9.5.0 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + org.springframework.boot + spring-boot-starter-security + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 17 + 17 + UTF-8 + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + f1.Main + false + + + + repackage + + repackage + + + + + + + \ No newline at end of file diff --git a/backend/src/main/java/f1/AppConfig.java b/backend/src/main/java/f1/AppConfig.java new file mode 100755 index 0000000..4d27ab9 --- /dev/null +++ b/backend/src/main/java/f1/AppConfig.java @@ -0,0 +1,46 @@ +package f1; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import f1.db.*; + +@Configuration +public class AppConfig { + + @Value("${spring.datasource.url}") + private String dbUrl; + + @Value("${spring.datasource.username}") + private String dbUsername; + + @Value("${spring.datasource.password}") + private String dbPassword; + + @Bean + public Database database() { + Database db = new Database(dbUrl, dbUsername, dbPassword); + db.init(); + return db; + } + + @Bean + public UserDao userDao(Database db) { + return new UserDao(db); + } + + @Bean + public DriverDao driverDao(Database db) { + return new DriverDao(db); + } + + @Bean + public TeamDao teamDao(Database db) { + return new TeamDao(db); + } + + @Bean + public RaceDao raceDao(Database db) { + return new RaceDao(db); + } +} diff --git a/backend/src/main/java/f1/Main.java b/backend/src/main/java/f1/Main.java new file mode 100755 index 0000000..ae6e688 --- /dev/null +++ b/backend/src/main/java/f1/Main.java @@ -0,0 +1,38 @@ +package f1; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import f1.db.Database; +import jakarta.annotation.PostConstruct; + +@SpringBootApplication +@Component +public class Main { + public static String JDBC_URL; + public static String JDBC_USER; + public static String JDBC_PASSWORD; + + @Value("${spring.datasource.url}") + private String dbUrl; + + @Value("${spring.datasource.username}") + private String dbUsername; + + @Value("${spring.datasource.password}") + private String dbPassword; + + @PostConstruct + public void init() { + JDBC_URL = dbUrl; + JDBC_USER = dbUsername; + JDBC_PASSWORD = dbPassword; + } + + public static void main(String[] args) { + SpringApplication.run(Main.class, args); + var db = new Database(JDBC_URL, JDBC_USER, JDBC_PASSWORD); + db.init(); + } +} diff --git a/backend/src/main/java/f1/controller/AuthController.java b/backend/src/main/java/f1/controller/AuthController.java new file mode 100755 index 0000000..6071661 --- /dev/null +++ b/backend/src/main/java/f1/controller/AuthController.java @@ -0,0 +1,90 @@ +package f1.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import f1.Main; +import f1.db.Database; +import f1.db.UserDao; +import f1.entity.User; +import f1.security.JwtUtil; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Map; + +@RestController +@RequestMapping("/api/auth") +public class AuthController { + + private Database db; + private UserDao userDao; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private JwtUtil jwtUtil; + + public AuthController() { + this.db = new Database(Main.JDBC_URL, Main.JDBC_USER, Main.JDBC_PASSWORD); + db.init(); + this.userDao = new UserDao(db); + } + + @PostMapping("/register") + public ResponseEntity register(@RequestBody User user) { + if (userDao.getUserByUsername(user.getUsername()) != null) { + return ResponseEntity.status(HttpStatus.CONFLICT).body("Username already exists"); + } + + String encodedPassword = passwordEncoder.encode(user.getPassword()); + user.setPassword(encodedPassword); + + boolean success = userDao.createUser(user); + if (success) { + return ResponseEntity.status(HttpStatus.CREATED).body("User registered successfully"); + } else { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Registration failed"); + } + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody Map credentials, HttpServletResponse response) { + String username = credentials.get("username"); + String password = credentials.get("password"); + + User user = userDao.getUserByUsername(username); + if (user == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid username or password"); + } + + if (!passwordEncoder.matches(password, user.getPassword())) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid username or password"); + } + + String token = jwtUtil.generateToken(user.getUsername()); + + Cookie cookie = new Cookie("auth_token", token); + cookie.setHttpOnly(true); + cookie.setSecure(false); + cookie.setPath("/"); // 全局有效 + cookie.setMaxAge(24 * 60 * 60); // 24小时过期 + + response.addCookie(cookie); + + return ResponseEntity.ok("Login successful"); + } + + @PostMapping("/logout") + public ResponseEntity logout(HttpServletResponse response) { + Cookie cookie = new Cookie("auth_token", null); + cookie.setHttpOnly(true); + cookie.setPath("/"); + cookie.setMaxAge(0); // 立即过期 + response.addCookie(cookie); + return ResponseEntity.ok("Logged out"); + } +} \ No newline at end of file diff --git a/backend/src/main/java/f1/controller/CommentController.java b/backend/src/main/java/f1/controller/CommentController.java new file mode 100755 index 0000000..947c67d --- /dev/null +++ b/backend/src/main/java/f1/controller/CommentController.java @@ -0,0 +1,82 @@ +package f1.controller; + +import f1.db.UserDao; +import f1.entity.CommentItem; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.security.Principal; + +@RestController +@RequestMapping("/api/comments") +public class CommentController { + + @Autowired + private UserDao userDao; + + @GetMapping + public List> getComments(@RequestParam(defaultValue = "10") int limit, + @RequestParam(defaultValue = "0") int offset, + @RequestParam(required = false) Integer pageSize) { + List allComments = userDao.getRootComments(limit, offset); + List> pages = new ArrayList<>(); + + if (allComments.isEmpty()) { + return pages; + } + + int size = (pageSize == null || pageSize <= 0) ? allComments.size() : pageSize; + + for (int i = 0; i < allComments.size(); i += size) { + int end = Math.min(allComments.size(), i + size); + pages.add(allComments.subList(i, end)); + } + + return pages; + } + + @PostMapping + public boolean addRootComment(@RequestBody Map payload) { + int userId = (int) payload.get("user_id"); + String content = (String) payload.get("content"); + return userDao.addComment(userId, null, null, content); + } + + @GetMapping("/{id}/replies") + public Map getChildComments(@PathVariable int id) { + Map response = new HashMap<>(); + response.put("parent", userDao.getCommentById(id)); + response.put("replies", userDao.getChildComments(id)); + return response; + } + + @PostMapping("/{id}/replies") + public boolean addChildComment(@PathVariable int id, @RequestBody Map payload) { + int userId = (int) payload.get("user_id"); + String content = (String) payload.get("content"); + Integer responseId = payload.containsKey("response_id") ? (Integer) payload.get("response_id") : null; + + if (responseId == null) { + responseId = id; + } + + return userDao.addComment(userId, responseId, id, content); + } + + @DeleteMapping("/{id}") + public boolean deleteComment(@PathVariable int id, Principal principal) { + CommentItem comment = userDao.getCommentById(id); + if (comment == null) { + return false; + } + if (!comment.getUsername().equals(principal.getName())) { + return false; + } + return userDao.deleteComment(id); + } +} diff --git a/backend/src/main/java/f1/controller/F1Controller.java b/backend/src/main/java/f1/controller/F1Controller.java new file mode 100755 index 0000000..b34e9a6 --- /dev/null +++ b/backend/src/main/java/f1/controller/F1Controller.java @@ -0,0 +1,115 @@ +package f1.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import f1.db.DriverDao; +import f1.db.TeamDao; +import f1.entity.Driver; +import f1.entity.DriverHistoryItem; +import f1.entity.DriverStatistic; +import f1.entity.QualifyingResultItem; +import f1.entity.RaceResultItem; +import f1.entity.SeasonDriver; +import f1.entity.SeasonScheduleItem; +import f1.entity.Team; +import f1.entity.TeamHistoryItem; +import f1.entity.TeamStatistic; +import f1.db.RaceDao; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +@RestController +@RequestMapping("/api") +public class F1Controller { + + @Autowired + private DriverDao driverDao; + @Autowired + private TeamDao teamDao; + @Autowired + private RaceDao raceDao; + + @GetMapping("/drivers") + public List getAllDrivers() { + return driverDao.getAllDrivers(); + } + + @GetMapping("/teams") + public List getAllTeams() { + return teamDao.getAllTeams(); + } + + @GetMapping("/season-drivers") + public List getDriversBySeason(@RequestParam int season) { + return driverDao.getDriversBySeason(season); + } + + @GetMapping("/standings/teams") + public List> getSeasonTeamStandings(@RequestParam int season) { + return teamDao.getSeasonTop5TeamStandings(season); + } + + @GetMapping("/standings/drivers") + public List> getSeasonDriverStandings(@RequestParam int season) { + return driverDao.getSeasonTop5DriverStandings(season); + } + + @GetMapping("/prix") + public List getSeasonSchedule(@RequestParam int season) { + return raceDao.getSeasonSchedule(season); + } + + @GetMapping("/teams/{id}/drivers") + public List getDriverNames(@PathVariable int id, @RequestParam int season) { + return driverDao.getDriverNamesByTeamAndSeason(id, season); + } + + @GetMapping("/drivers/{id}/statistics") + public Map getDriverStatistic(@PathVariable int id, @RequestParam int season) { + Map result = new HashMap<>(); + result.put("formal", driverDao.getDriverStatistic(id, season, false)); + result.put("sprint", driverDao.getDriverStatistic(id, season, true)); + return result; + } + + @GetMapping("/teams/{id}/statistics") + public TeamStatistic getTeamStatistic(@PathVariable int id, @RequestParam int season) { + DriverStatistic formal = teamDao.getTeamStatistic(id, season, false); + DriverStatistic sprint = teamDao.getTeamStatistic(id, season, true); + List drivers = driverDao.getDriverNamesByTeamAndSeason(id, season); + + Map standing = teamDao.getTeamStanding(id, season); + int rank = standing.getOrDefault("ranking", 0); + int totalScore = standing.getOrDefault("score", 0); + + return new TeamStatistic(formal, sprint, drivers, rank, totalScore); + } + + @GetMapping("/prix/{prixId}/race") + public List getRaceResult(@PathVariable int prixId, + @RequestParam(defaultValue = "false") boolean isSprint) { + return raceDao.getRaceResults(prixId, isSprint); + } + + @GetMapping("/prix/{prixId}/qualifying") + public List getQualifyingResult(@PathVariable int prixId, + @RequestParam(defaultValue = "false") boolean isSprint) { + return raceDao.getQualifyingResults(prixId, isSprint); + } + + @GetMapping("/drivers/{id}/results") + public List getDriverHistory(@PathVariable int id, @RequestParam int season) { + return driverDao.getDriverHistory(id, season); + } + + @GetMapping("/teams/{id}/results") + public List getTeamHistory(@PathVariable int id, @RequestParam int season) { + return teamDao.getTeamHistory(id, season); + } +} \ No newline at end of file diff --git a/backend/src/main/java/f1/controller/UserController.java b/backend/src/main/java/f1/controller/UserController.java new file mode 100755 index 0000000..d742c21 --- /dev/null +++ b/backend/src/main/java/f1/controller/UserController.java @@ -0,0 +1,35 @@ +package f1.controller; + +import f1.db.UserDao; +import f1.entity.User; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; + +@RestController +@RequestMapping("/api/user") +public class UserController { + + @Autowired + private UserDao userDao; + + @GetMapping + public User getCurrentUser(Principal principal) { + if (principal == null) { + return null; // Or throw exception / return 401 + } + String username = principal.getName(); + // getUserByUsername returns full user, but password is now @JsonIgnore-d + return userDao.getUserByUsername(username); + } + + @GetMapping("/{id}") + public User getUserById(@PathVariable int id) { + return userDao.getUserById(id); + } +} diff --git a/backend/src/main/java/f1/db/BaseDao.java b/backend/src/main/java/f1/db/BaseDao.java new file mode 100755 index 0000000..28a3a35 --- /dev/null +++ b/backend/src/main/java/f1/db/BaseDao.java @@ -0,0 +1,16 @@ +package f1.db; + +import java.sql.Connection; +import java.sql.SQLException; + +public abstract class BaseDao { + protected Database db; + + public BaseDao(Database db) { + this.db = db; + } + + protected Connection getConnection() throws SQLException { + return db.getConnection(); + } +} diff --git a/backend/src/main/java/f1/db/Database.java b/backend/src/main/java/f1/db/Database.java new file mode 100755 index 0000000..96a081c --- /dev/null +++ b/backend/src/main/java/f1/db/Database.java @@ -0,0 +1,66 @@ +package f1.db; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.IOException; + +public class Database { + private HikariDataSource dataSource; + + public Database(String url, String user, String password) { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl(url); + config.setUsername(user); + config.setPassword(password); + config.addDataSourceProperty("cachePrepStmts", "true"); + config.addDataSourceProperty("prepStmtCacheSize", "250"); + config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + config.setMaximumPoolSize(10); + + this.dataSource = new HikariDataSource(config); + } + + public Connection getConnection() throws SQLException { + return dataSource.getConnection(); + } + + public void close() { + if (dataSource != null) { + dataSource.close(); + } + } + + public void init() { + try (Connection con = getConnection()) { + executeSqlFile(con, "/init.sql"); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + private void executeSqlFile(Connection con, String resourcePath) { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(getClass().getResourceAsStream(resourcePath))); + var stmt = con.createStatement()) { + StringBuilder sql = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.startsWith("--") || line.isEmpty()) { + continue; + } + sql.append(line).append(" "); + if (line.endsWith(";")) { + stmt.executeUpdate(sql.toString().trim()); + sql.setLength(0); + } + } + } catch (IOException | SQLException e) { + e.printStackTrace(); + } + } +} diff --git a/backend/src/main/java/f1/db/DriverDao.java b/backend/src/main/java/f1/db/DriverDao.java new file mode 100755 index 0000000..895eeef --- /dev/null +++ b/backend/src/main/java/f1/db/DriverDao.java @@ -0,0 +1,191 @@ +package f1.db; + +import f1.entity.Driver; +import f1.entity.DriverHistoryItem; +import f1.entity.DriverStatistic; +import f1.entity.SeasonDriver; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DriverDao extends BaseDao { + + public DriverDao(Database db) { + super(db); + } + + public ArrayList getAllDrivers() { + ArrayList drivers = new ArrayList<>(); + try (var con = getConnection(); + var stmt = con.createStatement(); + var rs = stmt.executeQuery("SELECT * FROM driver")) { + while (rs.next()) { + int id = rs.getInt("id"); + String name = rs.getString("name"); + String country = rs.getString("country"); + java.sql.Date birthday = rs.getDate("birthday"); + drivers.add(new Driver(id, name, country, birthday)); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return drivers; + } + + public ArrayList getDriversBySeason(int season) { + ArrayList seasonDrivers = new ArrayList<>(); + try (var con = getConnection(); + var stmt = con + .prepareStatement("SELECT * FROM season_driver WHERE season = ? ORDER BY team, car_num")) { + stmt.setInt(1, season); + var rs = stmt.executeQuery(); + while (rs.next()) { + int id = rs.getInt("id"); + String name = rs.getString("name"); + String team = rs.getString("team"); + String country = rs.getString("country"); + java.sql.Date birthday = rs.getDate("birthday"); + int carNum = rs.getInt("car_num"); + int seasonVal = rs.getInt("season"); + seasonDrivers.add(new SeasonDriver(id, name, team, country, birthday, carNum, seasonVal)); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return seasonDrivers; + } + + public List> getSeasonTop5DriverStandings(int season) { + List> result = new ArrayList<>(); + String sql = "SELECT sd.name, sd.team, standings.total_score, standings.ranking " + + "FROM season_driver sd " + + "JOIN ( " + + " SELECT rr.driver_id, SUM(rr.score) AS total_score, " + + " RANK() OVER (ORDER BY SUM(rr.score) DESC) AS ranking " + + " FROM race_result rr " + + " JOIN prix p ON rr.prix_id = p.id " + + " WHERE p.season = ? " + + " GROUP BY rr.driver_id " + + ") standings ON sd.id = standings.driver_id " + + "WHERE sd.season = ? " + + "ORDER BY standings.ranking ASC " + + "LIMIT 5"; + + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setInt(1, season); + stmt.setInt(2, season); + var rs = stmt.executeQuery(); + while (rs.next()) { + Map item = new HashMap<>(); + item.put("driver_name", rs.getString("name")); + item.put("team_name", rs.getString("team")); + item.put("total_score", rs.getInt("total_score")); + item.put("ranking", rs.getInt("ranking")); + result.add(item); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return result; + } + + public ArrayList getDriverNamesByTeamAndSeason(int teamId, int season) { + ArrayList names = new ArrayList<>(); + String sql = "SELECT d.name FROM driver d JOIN contract c ON d.id = c.driver_id WHERE c.season = ? AND c.team_id = ?"; + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setInt(1, season); + stmt.setInt(2, teamId); + var rs = stmt.executeQuery(); + while (rs.next()) { + names.add(rs.getString("name")); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return names; + } + + public DriverStatistic getDriverStatistic(int driverId, int season, boolean isSprint) { + return getStatistic("rr.driver_id = ?", driverId, season, isSprint); + } + + private DriverStatistic getStatistic(String whereClause, int id, int season, boolean isSprint) { + DriverStatistic statistic = null; + String sql = """ + SELECT + COUNT(DISTINCT prix_id) AS total_cnt, + SUM(score) AS score_sum, + SUM(end_position <= 3) AS medal, + SUM(end_position = 1) AS gold, + SUM(start_position = 1) AS pole, + SUM(end_position <= 10) AS top_ten, + SUM(finish_time IN ('DNF', 'DNS', 'DSQ')) AS unfinished, + SUM(fastest_time = fastest.fastest) AS fastest_lap + FROM + race_result rr + NATURAL JOIN ( + SELECT + rr.prix_id , + MIN(rr.fastest_time) AS fastest + FROM + race_result rr + WHERE + rr.fastest_time NOT IN ('DNF', 'DNS', 'DSQ') + AND rr.is_sprint = ? + AND rr.prix_id IN (SELECT id FROM prix WHERE season = ?) + GROUP BY + rr.prix_id) fastest + WHERE + %s + AND rr.is_sprint = ? + AND rr.prix_id IN (SELECT id FROM prix WHERE season = ?) + """.formatted(whereClause); + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setBoolean(1, isSprint); + stmt.setInt(2, season); + stmt.setInt(3, id); + stmt.setBoolean(4, isSprint); + stmt.setInt(5, season); + var rs = stmt.executeQuery(); + if (rs.next()) { + int totalCnt = rs.getInt("total_cnt"); + int scoreSum = rs.getInt("score_sum"); + int medal = rs.getInt("medal"); + int gold = rs.getInt("gold"); + int pole = rs.getInt("pole"); + int topTen = rs.getInt("top_ten"); + int unfinished = rs.getInt("unfinished"); + int fastestLap = rs.getInt("fastest_lap"); + statistic = new DriverStatistic(totalCnt, scoreSum, medal, gold, pole, topTen, unfinished, + fastestLap); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return statistic; + } + + public ArrayList getDriverHistory(int driverId, int season) { + ArrayList history = new ArrayList<>(); + String sql = "SELECT prix_name, team_name, pos, score, is_sprint FROM prix_result WHERE driver_id = ? AND season = ? ORDER BY round, is_sprint"; + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setInt(1, driverId); + stmt.setInt(2, season); + var rs = stmt.executeQuery(); + while (rs.next()) { + history.add(new DriverHistoryItem( + rs.getString("prix_name"), + rs.getString("team_name"), + rs.getInt("pos"), + rs.getInt("score"), + rs.getBoolean("is_sprint"))); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return history; + } +} diff --git a/backend/src/main/java/f1/db/RaceDao.java b/backend/src/main/java/f1/db/RaceDao.java new file mode 100755 index 0000000..661b2e0 --- /dev/null +++ b/backend/src/main/java/f1/db/RaceDao.java @@ -0,0 +1,120 @@ +package f1.db; + +import java.sql.SQLException; +import java.util.ArrayList; + +import f1.entity.QualifyingResultItem; +import f1.entity.RaceResultItem; +import f1.entity.SeasonScheduleItem; + +public class RaceDao extends BaseDao { + + public RaceDao(Database db) { + super(db); + } + + public ArrayList getRaceResults(int prixId, boolean isSprint) { + ArrayList results = new ArrayList<>(); + String sql = "SELECT pos, car_num, driver_name, team_name, finish_time, score FROM prix_result WHERE prix_id = ? AND is_sprint = ? ORDER BY pos"; + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setInt(1, prixId); + stmt.setBoolean(2, isSprint); + var rs = stmt.executeQuery(); + while (rs.next()) { + results.add(new RaceResultItem( + rs.getInt("pos"), + rs.getInt("car_num"), + rs.getString("driver_name"), + rs.getString("team_name"), + rs.getString("finish_time"), + rs.getInt("score"))); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return results; + } + + public ArrayList getQualifyingResults(int prixId, boolean isSprint) { + ArrayList results = new ArrayList<>(); + String sql = """ + SELECT + qr.position, + sd.car_num, + sd.name, + sd.team, + MAX(CASE WHEN section = 1 THEN fastest_time END) AS q1_time, + MAX(CASE WHEN section = 2 THEN fastest_time END) AS q2_time, + MAX(CASE WHEN section = 3 THEN fastest_time END) AS q3_time + FROM + qualifying_result qr + JOIN + prix p ON qr.prix_id = p.id + JOIN + season_driver sd ON qr.driver_id = sd.id AND sd.season = p.season + WHERE + qr.prix_id = ? + AND qr.is_sprint = ? + GROUP BY + qr.driver_id, qr.position, sd.car_num, sd.name, sd.team + ORDER BY + qr.position + """; + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setInt(1, prixId); + stmt.setBoolean(2, isSprint); + var rs = stmt.executeQuery(); + while (rs.next()) { + results.add(new QualifyingResultItem( + rs.getInt("position"), + rs.getInt("car_num"), + rs.getString("name"), + rs.getString("team"), + rs.getString("q1_time"), + rs.getString("q2_time"), + rs.getString("q3_time"))); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return results; + } + + public ArrayList getSeasonSchedule(int season) { + ArrayList schedule = new ArrayList<>(); + String sql = """ + SELECT + p.id, + p.round, + p.name, + p.have_sprint, + p.circuit_id, + c.country, + c.location + FROM + prix p + JOIN circuit c ON c.id = p.circuit_id + WHERE + p.season = ? + ORDER BY + p.round + """; + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setInt(1, season); + var rs = stmt.executeQuery(); + while (rs.next()) { + schedule.add(new SeasonScheduleItem( + rs.getInt("id"), + rs.getInt("round"), + rs.getString("name"), + rs.getBoolean("have_sprint"), + rs.getInt("circuit_id"), + rs.getString("country"), + rs.getString("location"))); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return schedule; + } +} diff --git a/backend/src/main/java/f1/db/TeamDao.java b/backend/src/main/java/f1/db/TeamDao.java new file mode 100755 index 0000000..5bbdb3c --- /dev/null +++ b/backend/src/main/java/f1/db/TeamDao.java @@ -0,0 +1,158 @@ +package f1.db; + +import f1.entity.DriverStatistic; +import f1.entity.Team; +import f1.entity.TeamHistoryItem; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.time.LocalDate; + +public class TeamDao extends BaseDao { + + public TeamDao(Database db) { + super(db); + } + + public ArrayList getAllTeams() { + ArrayList teams = new ArrayList<>(); + try (var con = getConnection(); + var stmt = con.createStatement(); + var rs = stmt.executeQuery("SELECT * FROM team")) { + while (rs.next()) { + int id = rs.getInt("id"); + String name = rs.getString("name"); + String country = rs.getString("country"); + String engineSupplier = rs.getString("engine_supplier"); + int setupYear = rs.getInt("setup_time"); + java.sql.Date setupTime = java.sql.Date.valueOf(LocalDate.of(setupYear, 1, 1)); + teams.add(new Team(id, name, country, engineSupplier, setupTime)); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return teams; + } + + public Map getTeamStanding(int teamId, int season) { + Map result = new HashMap<>(); + String sql = "SELECT total_score, ranking FROM season_team_standings WHERE season = ? AND team_id = ?"; + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setInt(1, season); + stmt.setInt(2, teamId); + var rs = stmt.executeQuery(); + if (rs.next()) { + result.put("score", rs.getInt("total_score")); + result.put("ranking", rs.getInt("ranking")); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return result; + } + + public List> getSeasonTop5TeamStandings(int season) { + List> result = new ArrayList<>(); + String sql = "SELECT t.name, s.total_score, s.ranking " + + "FROM season_team_standings s " + + "JOIN team t ON s.team_id = t.id " + + "WHERE s.season = ? " + + "ORDER BY s.ranking ASC " + + "LIMIT 5"; + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setInt(1, season); + var rs = stmt.executeQuery(); + while (rs.next()) { + Map item = new HashMap<>(); + item.put("team_name", rs.getString("name")); + item.put("total_score", rs.getInt("total_score")); + item.put("ranking", rs.getInt("ranking")); + result.add(item); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return result; + } + + public DriverStatistic getTeamStatistic(int teamId, int season, boolean isSprint) { + return getStatistic("rr.team_id = ?", teamId, season, isSprint); + } + + private DriverStatistic getStatistic(String whereClause, int id, int season, boolean isSprint) { + DriverStatistic statistic = null; + String sql = """ + SELECT + COUNT(DISTINCT prix_id) AS total_cnt, + SUM(score) AS score_sum, + SUM(end_position <= 3) AS medal, + SUM(end_position = 1) AS gold, + SUM(start_position = 1) AS pole, + SUM(end_position <= 10) AS top_ten, + SUM(finish_time IN ('DNF', 'DNS', 'DSQ')) AS unfinished, + SUM(fastest_time = fastest.fastest) AS fastest_lap + FROM + race_result rr + NATURAL JOIN ( + SELECT + rr.prix_id , + MIN(rr.fastest_time) AS fastest + FROM + race_result rr + WHERE + rr.fastest_time NOT IN ('DNF', 'DNS', 'DSQ') + AND rr.is_sprint = ? + AND rr.prix_id IN (SELECT id FROM prix WHERE season = ?) + GROUP BY + rr.prix_id) fastest + WHERE + %s + AND rr.is_sprint = ? + AND rr.prix_id IN (SELECT id FROM prix WHERE season = ?) + """.formatted(whereClause); + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setBoolean(1, isSprint); + stmt.setInt(2, season); + stmt.setInt(3, id); + stmt.setBoolean(4, isSprint); + stmt.setInt(5, season); + var rs = stmt.executeQuery(); + if (rs.next()) { + int totalCnt = rs.getInt("total_cnt"); + int scoreSum = rs.getInt("score_sum"); + int medal = rs.getInt("medal"); + int gold = rs.getInt("gold"); + int pole = rs.getInt("pole"); + int topTen = rs.getInt("top_ten"); + int unfinished = rs.getInt("unfinished"); + int fastestLap = rs.getInt("fastest_lap"); + statistic = new DriverStatistic(totalCnt, scoreSum, medal, gold, pole, topTen, unfinished, + fastestLap); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return statistic; + } + + public ArrayList getTeamHistory(int teamId, int season) { + ArrayList history = new ArrayList<>(); + String sql = "SELECT prix_name, SUM(score) as total_score FROM prix_result WHERE team_id = ? AND season = ? GROUP BY prix_id, prix_name, round ORDER BY round"; + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setInt(1, teamId); + stmt.setInt(2, season); + var rs = stmt.executeQuery(); + while (rs.next()) { + history.add(new TeamHistoryItem( + rs.getString("prix_name"), + rs.getInt("total_score"))); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return history; + } +} diff --git a/backend/src/main/java/f1/db/UserDao.java b/backend/src/main/java/f1/db/UserDao.java new file mode 100755 index 0000000..102bbc4 --- /dev/null +++ b/backend/src/main/java/f1/db/UserDao.java @@ -0,0 +1,200 @@ +package f1.db; + +import f1.entity.CommentItem; +import f1.entity.User; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class UserDao extends BaseDao { + + public UserDao(Database db) { + super(db); + } + + public boolean createUser(User user) { + String sql = "INSERT INTO user (username, password, email, country, avatar) VALUES (?, ?, ?, ?, ?)"; + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setString(1, user.getUsername()); + stmt.setString(2, user.getPassword()); + stmt.setString(3, user.getEmail()); + stmt.setString(4, user.getCountry()); + stmt.setString(5, user.getAvatar()); + int affectedRows = stmt.executeUpdate(); + return affectedRows > 0; + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + public User getUserByUsername(String username) { + String sql = "SELECT * FROM user WHERE username = ?"; + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setString(1, username); + var rs = stmt.executeQuery(); + if (rs.next()) { + return new User( + rs.getInt("id"), + rs.getString("username"), + rs.getString("password"), + rs.getString("email"), + rs.getString("country"), + rs.getString("avatar")); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public User getUserById(int id) { + String sql = "SELECT username, email, country, avatar FROM user WHERE id = ?"; + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setInt(1, id); + var rs = stmt.executeQuery(); + if (rs.next()) { + // Password is not selected, so we pass null + return new User( + id, + rs.getString("username"), + null, // password + rs.getString("email"), + rs.getString("country"), + rs.getString("avatar")); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public boolean addComment(int userId, Integer responseId, Integer rootId, String content) { + String sql = "INSERT INTO comment (user_id, response_id, root_id, content) VALUES (?, ?, ?, ?)"; + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setInt(1, userId); + + if (responseId == null) { + stmt.setNull(2, java.sql.Types.INTEGER); + } else { + stmt.setInt(2, responseId); + } + + if (rootId == null) { + stmt.setNull(3, java.sql.Types.INTEGER); + } else { + stmt.setInt(3, rootId); + } + + stmt.setString(4, content); + int affectedRows = stmt.executeUpdate(); + return affectedRows > 0; + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + public boolean deleteComment(int commentId) { + String sql = "DELETE FROM comment WHERE id = ?"; + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setInt(1, commentId); + int affectedRows = stmt.executeUpdate(); + return affectedRows > 0; + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + public List getRootComments(int limit, int offset) { + List comments = new ArrayList<>(); + String sql = "SELECT c.id, c.user_id, u.username, c.content, COUNT(replies.id) AS reply_count " + + "FROM comment c " + + "JOIN user u ON c.user_id = u.id " + + "LEFT JOIN comment replies ON replies.root_id = c.id " + + "WHERE c.root_id IS NULL " + + "GROUP BY c.id " + + "ORDER BY c.id DESC " + + "LIMIT ? OFFSET ?"; + + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setInt(1, limit); + stmt.setInt(2, offset); + var rs = stmt.executeQuery(); + while (rs.next()) { + comments.add(new CommentItem( + rs.getInt("id"), + rs.getInt("user_id"), + rs.getString("username"), + null, // response_id is null for root + null, // root_id is null for root + rs.getString("content"), + rs.getInt("reply_count"))); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return comments; + } + + public List getChildComments(int rootId) { + List comments = new ArrayList<>(); + String sql = "SELECT c.id, c.user_id, u.username, c.response_id, c.content, " + + "parent_c.user_id AS reply_to_user_id, parent_u.username AS reply_to_username " + + "FROM comment c " + + "JOIN user u ON c.user_id = u.id " + + "LEFT JOIN comment parent_c ON c.response_id = parent_c.id " + + "LEFT JOIN user parent_u ON parent_c.user_id = parent_u.id " + + "WHERE c.root_id = ? " + + "ORDER BY c.id ASC"; + + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setInt(1, rootId); + var rs = stmt.executeQuery(); + while (rs.next()) { + comments.add(new CommentItem( + rs.getInt("id"), + rs.getInt("user_id"), + rs.getString("username"), + (Integer) rs.getObject("response_id"), + null, // root_id is known and not needed in response + rs.getString("content"), + (Integer) rs.getObject("reply_to_user_id"), + rs.getString("reply_to_username"))); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return comments; + } + + public CommentItem getCommentById(int id) { + String sql = "SELECT c.id, c.user_id, u.username, c.content, COUNT(replies.id) AS reply_count " + + "FROM comment c " + + "JOIN user u ON c.user_id = u.id " + + "LEFT JOIN comment replies ON replies.root_id = c.id " + + "WHERE c.id = ? " + + "GROUP BY c.id"; + + try (var con = getConnection(); var stmt = con.prepareStatement(sql)) { + stmt.setInt(1, id); + var rs = stmt.executeQuery(); + if (rs.next()) { + return new CommentItem( + rs.getInt("id"), + rs.getInt("user_id"), + rs.getString("username"), + null, + null, + rs.getString("content"), + rs.getInt("reply_count") + ); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/backend/src/main/java/f1/entity/CommentItem.java b/backend/src/main/java/f1/entity/CommentItem.java new file mode 100755 index 0000000..47a4066 --- /dev/null +++ b/backend/src/main/java/f1/entity/CommentItem.java @@ -0,0 +1,90 @@ +package f1.entity; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CommentItem { + private int id; + + @JsonProperty("user_id") + private int userId; + + private String username; + + @JsonProperty("response_id") + private Integer responseId; + + @JsonProperty("root_id") + private Integer rootId; + + private String content; + + @JsonProperty("reply_count") + private int replyCount; + + @JsonProperty("reply_to_user_id") + private Integer replyToUserId; + + @JsonProperty("reply_to_username") + private String replyToUsername; + + public CommentItem(int id, int userId, String username, Integer responseId, Integer rootId, String content, + int replyCount) { + this.id = id; + this.userId = userId; + this.username = username; + this.responseId = responseId; + this.rootId = rootId; + this.content = content; + this.replyCount = replyCount; + } + + public CommentItem(int id, int userId, String username, Integer responseId, Integer rootId, String content, + Integer replyToUserId, String replyToUsername) { + this.id = id; + this.userId = userId; + this.username = username; + this.responseId = responseId; + this.rootId = rootId; + this.content = content; + this.replyToUserId = replyToUserId; + this.replyToUsername = replyToUsername; + } + + public int getId() { + return id; + } + + public int getUserId() { + return userId; + } + + public String getUsername() { + return username; + } + + public Integer getResponseId() { + return responseId; + } + + public Integer getRootId() { + return rootId; + } + + public String getContent() { + return content; + } + + public int getReplyCount() { + return replyCount; + } + + public Integer getReplyToUserId() { + return replyToUserId; + } + + public String getReplyToUsername() { + return replyToUsername; + } +} diff --git a/backend/src/main/java/f1/entity/Driver.java b/backend/src/main/java/f1/entity/Driver.java new file mode 100755 index 0000000..48f5855 --- /dev/null +++ b/backend/src/main/java/f1/entity/Driver.java @@ -0,0 +1,49 @@ +package f1.entity; + +import java.sql.Date; + +public class Driver { + private int id; + private String name; + private String country; + private Date birthday; + + public Driver(int id, String name, String country, Date birthday) { + this.id = id; + this.name = name; + this.country = country; + this.birthday = birthday; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public String getCountry() { + return country; + } + + public Date getBirthday() { + return birthday; + } + + public void setId(int id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public void setCountry(String country) { + this.country = country; + } + + public void setBirthday(Date birthday) { + this.birthday = birthday; + } +} \ No newline at end of file diff --git a/backend/src/main/java/f1/entity/DriverHistoryItem.java b/backend/src/main/java/f1/entity/DriverHistoryItem.java new file mode 100755 index 0000000..eb2f4c9 --- /dev/null +++ b/backend/src/main/java/f1/entity/DriverHistoryItem.java @@ -0,0 +1,45 @@ +package f1.entity; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class DriverHistoryItem { + @JsonProperty("prix_name") + private String prixName; + + @JsonProperty("team_name") + private String teamName; + + private int position; + private int score; + + @JsonProperty("is_sprint") + private boolean isSprint; + + public DriverHistoryItem(String prixName, String teamName, int position, int score, boolean isSprint) { + this.prixName = prixName; + this.teamName = teamName; + this.position = position; + this.score = score; + this.isSprint = isSprint; + } + + public String getPrixName() { + return prixName; + } + + public String getTeamName() { + return teamName; + } + + public int getPosition() { + return position; + } + + public int getScore() { + return score; + } + + public boolean isSprint() { + return isSprint; + } +} diff --git a/backend/src/main/java/f1/entity/DriverStatistic.java b/backend/src/main/java/f1/entity/DriverStatistic.java new file mode 100755 index 0000000..763ddd0 --- /dev/null +++ b/backend/src/main/java/f1/entity/DriverStatistic.java @@ -0,0 +1,88 @@ +package f1.entity; + +public class DriverStatistic { + private int totalCnt; + private int scoreSum; + private int medal; + private int gold; + private int pole; + private int topTen; + private int unfinished; + private int fastestLap; + + public DriverStatistic(int totalCnt, int scoreSum, int medal, int gold, int pole, int topTen, int unfinished, + int fastestLap) { + this.totalCnt = totalCnt; + this.scoreSum = scoreSum; + this.medal = medal; + this.gold = gold; + this.pole = pole; + this.topTen = topTen; + this.unfinished = unfinished; + this.fastestLap = fastestLap; + } + + public int getTotalCnt() { + return totalCnt; + } + + public int getScoreSum() { + return scoreSum; + } + + public int getMedal() { + return medal; + } + + public int getGold() { + return gold; + } + + public int getPole() { + return pole; + } + + public int getTopTen() { + return topTen; + } + + public int getUnfinished() { + return unfinished; + } + + public int getFastestLap() { + return fastestLap; + } + + public void setTotalCnt(int totalCnt) { + this.totalCnt = totalCnt; + } + + public void setScoreSum(int scoreSum) { + this.scoreSum = scoreSum; + } + + public void setMedal(int medal) { + this.medal = medal; + } + + public void setGold(int gold) { + this.gold = gold; + } + + public void setPole(int pole) { + this.pole = pole; + } + + public void setTopTen(int topTen) { + this.topTen = topTen; + } + + public void setUnfinished(int unfinished) { + this.unfinished = unfinished; + } + + public void setFastestLap(int fastestLap) { + this.fastestLap = fastestLap; + } +} \ No newline at end of file diff --git a/backend/src/main/java/f1/entity/QualifyingResultItem.java b/backend/src/main/java/f1/entity/QualifyingResultItem.java new file mode 100755 index 0000000..7590895 --- /dev/null +++ b/backend/src/main/java/f1/entity/QualifyingResultItem.java @@ -0,0 +1,89 @@ +package f1.entity; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class QualifyingResultItem { + private int position; + + @JsonProperty("car_num") + private int carNum; + + private String name; + private String team; + + @JsonProperty("q1") + private String q1Time; + + @JsonProperty("q2") + private String q2Time; + + @JsonProperty("q3") + private String q3Time; + + public QualifyingResultItem(int position, int carNum, String name, String team, String q1Time, String q2Time, + String q3Time) { + this.position = position; + this.carNum = carNum; + this.name = name; + this.team = team; + this.q1Time = q1Time; + this.q2Time = q2Time; + this.q3Time = q3Time; + } + + public int getPosition() { + return position; + } + + public void setPosition(int position) { + this.position = position; + } + + public int getCarNum() { + return carNum; + } + + public void setCarNum(int carNum) { + this.carNum = carNum; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getTeam() { + return team; + } + + public void setTeam(String team) { + this.team = team; + } + + public String getQ1Time() { + return q1Time; + } + + public void setQ1Time(String q1Time) { + this.q1Time = q1Time; + } + + public String getQ2Time() { + return q2Time; + } + + public void setQ2Time(String q2Time) { + this.q2Time = q2Time; + } + + public String getQ3Time() { + return q3Time; + } + + public void setQ3Time(String q3Time) { + this.q3Time = q3Time; + } +} diff --git a/backend/src/main/java/f1/entity/RaceResultItem.java b/backend/src/main/java/f1/entity/RaceResultItem.java new file mode 100755 index 0000000..2884a20 --- /dev/null +++ b/backend/src/main/java/f1/entity/RaceResultItem.java @@ -0,0 +1,75 @@ +package f1.entity; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class RaceResultItem { + private int pos; + + @JsonProperty("car_num") + private int carNum; + + private String name; + private String team; + + @JsonProperty("finish_time") + private String finishTime; + + private int score; + + public RaceResultItem(int pos, int carNum, String name, String team, String finishTime, int score) { + this.pos = pos; + this.carNum = carNum; + this.name = name; + this.team = team; + this.finishTime = finishTime; + this.score = score; + } + + public int getPos() { + return pos; + } + + public void setPos(int pos) { + this.pos = pos; + } + + public int getCarNum() { + return carNum; + } + + public void setCarNum(int carNum) { + this.carNum = carNum; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getTeam() { + return team; + } + + public void setTeam(String team) { + this.team = team; + } + + public String getFinishTime() { + return finishTime; + } + + public void setFinishTime(String finishTime) { + this.finishTime = finishTime; + } + + public int getScore() { + return score; + } + + public void setScore(int score) { + this.score = score; + } +} diff --git a/backend/src/main/java/f1/entity/SeasonDriver.java b/backend/src/main/java/f1/entity/SeasonDriver.java new file mode 100755 index 0000000..f3b80f7 --- /dev/null +++ b/backend/src/main/java/f1/entity/SeasonDriver.java @@ -0,0 +1,40 @@ +package f1.entity; + +import java.sql.Date; + +public class SeasonDriver extends Driver { + private String team; + private int carNum; + private int season; + + public SeasonDriver(int id, String name, String team, String country, Date birthday, int carNum, int season) { + super(id, name, country, birthday); + this.team = team; + this.carNum = carNum; + this.season = season; + } + + public String getTeam() { + return team; + } + + public int getCarNum() { + return carNum; + } + + public int getSeason() { + return season; + } + + public void setTeam(String team) { + this.team = team; + } + + public void setCarNum(int carNum) { + this.carNum = carNum; + } + + public void setSeason(int season) { + this.season = season; + } +} \ No newline at end of file diff --git a/backend/src/main/java/f1/entity/SeasonScheduleItem.java b/backend/src/main/java/f1/entity/SeasonScheduleItem.java new file mode 100755 index 0000000..b36d482 --- /dev/null +++ b/backend/src/main/java/f1/entity/SeasonScheduleItem.java @@ -0,0 +1,85 @@ +package f1.entity; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class SeasonScheduleItem { + private int id; + private int round; + private String name; + + @JsonProperty("have_sprint") + private boolean haveSprint; + + @JsonProperty("circuit_id") + private int circuitId; + + private String country; + private String location; + + public SeasonScheduleItem(int id, int round, String name, boolean haveSprint, int circuitId, String country, + String location) { + this.id = id; + this.round = round; + this.name = name; + this.haveSprint = haveSprint; + this.circuitId = circuitId; + this.country = country; + this.location = location; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getRound() { + return round; + } + + public void setRound(int round) { + this.round = round; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isHaveSprint() { + return haveSprint; + } + + public void setHaveSprint(boolean haveSprint) { + this.haveSprint = haveSprint; + } + + public int getCircuitId() { + return circuitId; + } + + public void setCircuitId(int circuitId) { + this.circuitId = circuitId; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } +} diff --git a/backend/src/main/java/f1/entity/Team.java b/backend/src/main/java/f1/entity/Team.java new file mode 100755 index 0000000..f04d129 --- /dev/null +++ b/backend/src/main/java/f1/entity/Team.java @@ -0,0 +1,59 @@ +package f1.entity; + +import java.sql.Date; + +public class Team { + private int id; + private String name; + private String country; + private String engineSupplier; + private Date setupTime; + + public Team(int id, String name, String country, String engineSupplier, Date setupTime) { + this.id = id; + this.name = name; + this.country = country; + this.engineSupplier = engineSupplier; + this.setupTime = setupTime; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public String getCountry() { + return country; + } + + public String getEngineSupplier() { + return engineSupplier; + } + + public Date getSetupTime() { + return setupTime; + } + + public void setId(int id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public void setCountry(String country) { + this.country = country; + } + + public void setEngineSupplier(String engineSupplier) { + this.engineSupplier = engineSupplier; + } + + public void setSetupTime(Date setupTime) { + this.setupTime = setupTime; + } +} \ No newline at end of file diff --git a/backend/src/main/java/f1/entity/TeamHistoryItem.java b/backend/src/main/java/f1/entity/TeamHistoryItem.java new file mode 100755 index 0000000..82e99d2 --- /dev/null +++ b/backend/src/main/java/f1/entity/TeamHistoryItem.java @@ -0,0 +1,24 @@ +package f1.entity; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TeamHistoryItem { + @JsonProperty("prix_name") + private String prixName; + + @JsonProperty("total_score") + private int totalScore; + + public TeamHistoryItem(String prixName, int totalScore) { + this.prixName = prixName; + this.totalScore = totalScore; + } + + public String getPrixName() { + return prixName; + } + + public int getTotalScore() { + return totalScore; + } +} diff --git a/backend/src/main/java/f1/entity/TeamStatistic.java b/backend/src/main/java/f1/entity/TeamStatistic.java new file mode 100755 index 0000000..4151a54 --- /dev/null +++ b/backend/src/main/java/f1/entity/TeamStatistic.java @@ -0,0 +1,64 @@ +package f1.entity; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class TeamStatistic { + private DriverStatistic formal; + private DriverStatistic sprint; + private List drivers; + private int rank; + + @JsonProperty("total_score") + private int totalScore; + + public TeamStatistic(DriverStatistic formal, DriverStatistic sprint, List drivers, int rank, + int totalScore) { + this.formal = formal; + this.sprint = sprint; + this.drivers = drivers; + this.rank = rank; + this.totalScore = totalScore; + } + + public DriverStatistic getFormal() { + return formal; + } + + public void setFormal(DriverStatistic formal) { + this.formal = formal; + } + + public DriverStatistic getSprint() { + return sprint; + } + + public void setSprint(DriverStatistic sprint) { + this.sprint = sprint; + } + + public List getDrivers() { + return drivers; + } + + public void setDrivers(List drivers) { + this.drivers = drivers; + } + + public int getRank() { + return rank; + } + + public void setRank(int rank) { + this.rank = rank; + } + + public int getTotalScore() { + return totalScore; + } + + public void setTotalScore(int totalScore) { + this.totalScore = totalScore; + } +} diff --git a/backend/src/main/java/f1/entity/User.java b/backend/src/main/java/f1/entity/User.java new file mode 100755 index 0000000..455a891 --- /dev/null +++ b/backend/src/main/java/f1/entity/User.java @@ -0,0 +1,73 @@ +package f1.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public class User { + private int id; + private String username; + @JsonIgnore + private String password; + private String email; + private String country; + private String avatar; + + public User() { + } + + public User(int id, String username, String password, String email, String country, String avatar) { + this.id = id; + this.username = username; + this.password = password; + this.email = email; + this.country = country; + this.avatar = avatar; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getAvatar() { + return avatar; + } + + public void setAvatar(String avatar) { + this.avatar = avatar; + } +} \ No newline at end of file diff --git a/backend/src/main/java/f1/security/JwtFilter.java b/backend/src/main/java/f1/security/JwtFilter.java new file mode 100755 index 0000000..6ab60ee --- /dev/null +++ b/backend/src/main/java/f1/security/JwtFilter.java @@ -0,0 +1,50 @@ +package f1.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.ArrayList; + +@Component +public class JwtFilter extends OncePerRequestFilter { + + @Autowired + private JwtUtil jwtUtil; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + String token = null; + if (request.getCookies() != null) { + for (Cookie cookie : request.getCookies()) { + if ("auth_token".equals(cookie.getName())) { + token = cookie.getValue(); + break; + } + } + } + + if (token != null && jwtUtil.validateToken(token)) { + String username = jwtUtil.getUsernameFromToken(token); + + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + username, null, new ArrayList<>()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/backend/src/main/java/f1/security/JwtUtil.java b/backend/src/main/java/f1/security/JwtUtil.java new file mode 100755 index 0000000..1a73cdc --- /dev/null +++ b/backend/src/main/java/f1/security/JwtUtil.java @@ -0,0 +1,56 @@ +package f1.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import java.security.Key; +import java.util.Date; + +@Component +public class JwtUtil { + + @Value("${jwt.secret}") + private String secretKeyString; + + private Key secretKey; + private static final long EXPIRATION_TIME = 86400000; // 24小时 (毫秒) + + @PostConstruct + public void init() { + byte[] keyBytes = Decoders.BASE64.decode(secretKeyString); + this.secretKey = Keys.hmacShaKeyFor(keyBytes); + } + + public String generateToken(String username) { + return Jwts.builder() + .setSubject(username) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + public String getUsernameFromToken(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody(); + return claims.getSubject(); + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token); + return true; + } catch (Exception e) { + return false; + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/f1/security/SecurityConfig.java b/backend/src/main/java/f1/security/SecurityConfig.java new file mode 100755 index 0000000..e037729 --- /dev/null +++ b/backend/src/main/java/f1/security/SecurityConfig.java @@ -0,0 +1,39 @@ +package f1.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.http.HttpMethod; + +@Configuration +public class SecurityConfig { + + @Autowired + private JwtFilter jwtFilter; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf().disable() + .authorizeHttpRequests() + // Blacklist: Protect specific endpoints + .requestMatchers(HttpMethod.GET, "/api/user").authenticated() + .requestMatchers(HttpMethod.POST, "/api/comments/**").authenticated() + .requestMatchers(HttpMethod.DELETE, "/api/comments/**").authenticated() + // Whitelist: Allow everything else (static resources, other APIs) + .anyRequest().permitAll() + .and() + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); + return http.build(); + } +} \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties new file mode 100755 index 0000000..2d2777c --- /dev/null +++ b/backend/src/main/resources/application.properties @@ -0,0 +1,4 @@ +spring.datasource.url=jdbc:mysql://124.70.86.207:3306/h_db23373332 +spring.datasource.username=u23373332 +spring.datasource.password= +jwt.secret= diff --git a/backend/src/main/resources/application.properties.example b/backend/src/main/resources/application.properties.example new file mode 100755 index 0000000..e2a08f6 --- /dev/null +++ b/backend/src/main/resources/application.properties.example @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:mysql://your-host:3306/your-db +spring.datasource.username=your-username +spring.datasource.password=your-password +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +jwt.secret=24VSSNj6bDX7mwjIsidnJQYg3Lk6Ht9YvqYZYj7aDTc= +jwt.secret=your-base64-encoded-secret-key-min-32-bytes \ No newline at end of file diff --git a/backend/src/main/resources/init.sql b/backend/src/main/resources/init.sql new file mode 100755 index 0000000..e51e331 --- /dev/null +++ b/backend/src/main/resources/init.sql @@ -0,0 +1,148 @@ +-- 车手表 +CREATE TABLE IF NOT EXISTS driver( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100), + country VARCHAR(100), + birthday DATE +); + +-- 车队表 +CREATE TABLE IF NOT EXISTS team( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100), + country VARCHAR(100), + engine_supplier VARCHAR(100), + setup_time YEAR +); + +-- 合同表 +CREATE TABLE IF NOT EXISTS contract( + id INT AUTO_INCREMENT PRIMARY KEY, + driver_id INT, + team_id INT, + season YEAR, + car_num INT, + is_reserve BOOLEAN, + FOREIGN KEY(driver_id) REFERENCES driver(id), + FOREIGN KEY(team_id) REFERENCES team(id) +); + +-- 赛道表 +CREATE TABLE IF NOT EXISTS circuit( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100), + location VARCHAR(100), + country VARCHAR(100), + drs_zones INT +); + +-- 赛事表 +CREATE TABLE IF NOT EXISTS prix( + id INT AUTO_INCREMENT PRIMARY KEY, + season YEAR, + round INT, + name VARCHAR(100), + circuit_id INT, + have_sprint BOOLEAN, + FOREIGN KEY(circuit_id) REFERENCES circuit(id) +); + +-- 排位赛结果表 +CREATE TABLE IF NOT EXISTS qualifying_result( + prix_id INT, + driver_id INT, + team_id INT, + fastest_time VARCHAR(20), + position INT, + is_sprint BOOLEAN, + section INT, + FOREIGN KEY(prix_id) REFERENCES prix(id), + FOREIGN KEY(driver_id) REFERENCES driver(id), + FOREIGN KEY(team_id) REFERENCES team(id) +); + +-- 正赛结果表 +CREATE TABLE IF NOT EXISTS race_result( + prix_id INT, + driver_id INT, + team_id INT, + start_position INT, + end_position INT, + finish_time VARCHAR(20), + fastest_time VARCHAR(20), + score INT, + is_sprint BOOLEAN, + FOREIGN KEY(prix_id) REFERENCES prix(id), + FOREIGN KEY(driver_id) REFERENCES driver(id), + FOREIGN KEY(team_id) REFERENCES team(id) +); + +-- 用户表 +CREATE TABLE IF NOT EXISTS user( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(100) UNIQUE, + password VARCHAR(100), + email VARCHAR(100), + country VARCHAR(100), + avatar VARCHAR(500) +); + +-- 评论表 +CREATE TABLE IF NOT EXISTS comment( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT, + response_id INT, + root_id INT, + content VARCHAR(500), + FOREIGN KEY(user_id) REFERENCES user(id), + FOREIGN KEY(response_id) REFERENCES comment(id) ON DELETE CASCADE, + FOREIGN KEY(root_id) REFERENCES comment(id) ON DELETE CASCADE +); + +-- 赛季车手信息视图 +CREATE OR REPLACE VIEW season_driver( + id, name, team, country, birthday, car_num, season +) AS +SELECT D.id, + D.name, + T.name, + D.country, + D.birthday, + C.car_num, + C.season +FROM contract C +JOIN team T ON T.id = C.team_id +JOIN driver D ON C.driver_id = D.id; + +-- 赛季车队积分榜视图 +CREATE OR REPLACE VIEW season_team_standings AS +SELECT + p.season, + rr.team_id, + SUM(rr.score) AS total_score, + RANK() OVER (PARTITION BY p.season ORDER BY SUM(rr.score) DESC) AS ranking +FROM race_result rr +JOIN prix p ON rr.prix_id = p.id +GROUP BY p.season, rr.team_id; + +CREATE OR REPLACE VIEW prix_result AS +SELECT + p.season, + p.id AS prix_id, + rr.driver_id, + rr.team_id, + p.name AS prix_name, + p.round, + sd.name AS driver_name, + sd.car_num, + sd.team AS team_name, + rr.end_position AS pos, + rr.finish_time, + rr.score, + rr.is_sprint +FROM + race_result rr +JOIN + prix p ON rr.prix_id = p.id +JOIN + season_driver sd ON rr.driver_id = sd.id AND sd.season = p.season; \ No newline at end of file diff --git a/backend/target/classes/application.properties b/backend/target/classes/application.properties new file mode 100755 index 0000000..2d2777c --- /dev/null +++ b/backend/target/classes/application.properties @@ -0,0 +1,4 @@ +spring.datasource.url=jdbc:mysql://124.70.86.207:3306/h_db23373332 +spring.datasource.username=u23373332 +spring.datasource.password= +jwt.secret= diff --git a/backend/target/classes/application.properties.example b/backend/target/classes/application.properties.example new file mode 100755 index 0000000..e2a08f6 --- /dev/null +++ b/backend/target/classes/application.properties.example @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:mysql://your-host:3306/your-db +spring.datasource.username=your-username +spring.datasource.password=your-password +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +jwt.secret=24VSSNj6bDX7mwjIsidnJQYg3Lk6Ht9YvqYZYj7aDTc= +jwt.secret=your-base64-encoded-secret-key-min-32-bytes \ No newline at end of file diff --git a/backend/target/classes/f1/AppConfig.class b/backend/target/classes/f1/AppConfig.class new file mode 100644 index 0000000..4799999 Binary files /dev/null and b/backend/target/classes/f1/AppConfig.class differ diff --git a/backend/target/classes/f1/Main.class b/backend/target/classes/f1/Main.class new file mode 100644 index 0000000..0b4d2e5 Binary files /dev/null and b/backend/target/classes/f1/Main.class differ diff --git a/backend/target/classes/f1/controller/AuthController.class b/backend/target/classes/f1/controller/AuthController.class new file mode 100644 index 0000000..0f96307 Binary files /dev/null and b/backend/target/classes/f1/controller/AuthController.class differ diff --git a/backend/target/classes/f1/controller/CommentController.class b/backend/target/classes/f1/controller/CommentController.class new file mode 100644 index 0000000..44bd814 Binary files /dev/null and b/backend/target/classes/f1/controller/CommentController.class differ diff --git a/backend/target/classes/f1/controller/F1Controller.class b/backend/target/classes/f1/controller/F1Controller.class new file mode 100644 index 0000000..e0bc3cb Binary files /dev/null and b/backend/target/classes/f1/controller/F1Controller.class differ diff --git a/backend/target/classes/f1/controller/UserController.class b/backend/target/classes/f1/controller/UserController.class new file mode 100644 index 0000000..1ae2a25 Binary files /dev/null and b/backend/target/classes/f1/controller/UserController.class differ diff --git a/backend/target/classes/f1/db/BaseDao.class b/backend/target/classes/f1/db/BaseDao.class new file mode 100644 index 0000000..d245c8c Binary files /dev/null and b/backend/target/classes/f1/db/BaseDao.class differ diff --git a/backend/target/classes/f1/db/Database.class b/backend/target/classes/f1/db/Database.class new file mode 100644 index 0000000..8053d3a Binary files /dev/null and b/backend/target/classes/f1/db/Database.class differ diff --git a/backend/target/classes/f1/db/DriverDao.class b/backend/target/classes/f1/db/DriverDao.class new file mode 100644 index 0000000..ff980a9 Binary files /dev/null and b/backend/target/classes/f1/db/DriverDao.class differ diff --git a/backend/target/classes/f1/db/RaceDao.class b/backend/target/classes/f1/db/RaceDao.class new file mode 100644 index 0000000..02c1f19 Binary files /dev/null and b/backend/target/classes/f1/db/RaceDao.class differ diff --git a/backend/target/classes/f1/db/TeamDao.class b/backend/target/classes/f1/db/TeamDao.class new file mode 100644 index 0000000..7d48580 Binary files /dev/null and b/backend/target/classes/f1/db/TeamDao.class differ diff --git a/backend/target/classes/f1/db/UserDao.class b/backend/target/classes/f1/db/UserDao.class new file mode 100644 index 0000000..d8001bd Binary files /dev/null and b/backend/target/classes/f1/db/UserDao.class differ diff --git a/backend/target/classes/f1/entity/CommentItem.class b/backend/target/classes/f1/entity/CommentItem.class new file mode 100644 index 0000000..fd5eede Binary files /dev/null and b/backend/target/classes/f1/entity/CommentItem.class differ diff --git a/backend/target/classes/f1/entity/Driver.class b/backend/target/classes/f1/entity/Driver.class new file mode 100644 index 0000000..66888ab Binary files /dev/null and b/backend/target/classes/f1/entity/Driver.class differ diff --git a/backend/target/classes/f1/entity/DriverHistoryItem.class b/backend/target/classes/f1/entity/DriverHistoryItem.class new file mode 100644 index 0000000..08c9ccf Binary files /dev/null and b/backend/target/classes/f1/entity/DriverHistoryItem.class differ diff --git a/backend/target/classes/f1/entity/DriverStatistic.class b/backend/target/classes/f1/entity/DriverStatistic.class new file mode 100644 index 0000000..21d6419 Binary files /dev/null and b/backend/target/classes/f1/entity/DriverStatistic.class differ diff --git a/backend/target/classes/f1/entity/QualifyingResultItem.class b/backend/target/classes/f1/entity/QualifyingResultItem.class new file mode 100644 index 0000000..c19a456 Binary files /dev/null and b/backend/target/classes/f1/entity/QualifyingResultItem.class differ diff --git a/backend/target/classes/f1/entity/RaceResultItem.class b/backend/target/classes/f1/entity/RaceResultItem.class new file mode 100644 index 0000000..c793438 Binary files /dev/null and b/backend/target/classes/f1/entity/RaceResultItem.class differ diff --git a/backend/target/classes/f1/entity/SeasonDriver.class b/backend/target/classes/f1/entity/SeasonDriver.class new file mode 100644 index 0000000..8e4ff8f Binary files /dev/null and b/backend/target/classes/f1/entity/SeasonDriver.class differ diff --git a/backend/target/classes/f1/entity/SeasonScheduleItem.class b/backend/target/classes/f1/entity/SeasonScheduleItem.class new file mode 100644 index 0000000..9541cf5 Binary files /dev/null and b/backend/target/classes/f1/entity/SeasonScheduleItem.class differ diff --git a/backend/target/classes/f1/entity/Team.class b/backend/target/classes/f1/entity/Team.class new file mode 100644 index 0000000..f1eeb81 Binary files /dev/null and b/backend/target/classes/f1/entity/Team.class differ diff --git a/backend/target/classes/f1/entity/TeamHistoryItem.class b/backend/target/classes/f1/entity/TeamHistoryItem.class new file mode 100644 index 0000000..87ec08d Binary files /dev/null and b/backend/target/classes/f1/entity/TeamHistoryItem.class differ diff --git a/backend/target/classes/f1/entity/TeamStatistic.class b/backend/target/classes/f1/entity/TeamStatistic.class new file mode 100644 index 0000000..b9d118c Binary files /dev/null and b/backend/target/classes/f1/entity/TeamStatistic.class differ diff --git a/backend/target/classes/f1/entity/User.class b/backend/target/classes/f1/entity/User.class new file mode 100644 index 0000000..fdbc4af Binary files /dev/null and b/backend/target/classes/f1/entity/User.class differ diff --git a/backend/target/classes/f1/security/JwtFilter.class b/backend/target/classes/f1/security/JwtFilter.class new file mode 100644 index 0000000..75148ca Binary files /dev/null and b/backend/target/classes/f1/security/JwtFilter.class differ diff --git a/backend/target/classes/f1/security/JwtUtil.class b/backend/target/classes/f1/security/JwtUtil.class new file mode 100644 index 0000000..5596486 Binary files /dev/null and b/backend/target/classes/f1/security/JwtUtil.class differ diff --git a/backend/target/classes/f1/security/SecurityConfig.class b/backend/target/classes/f1/security/SecurityConfig.class new file mode 100644 index 0000000..b2ac322 Binary files /dev/null and b/backend/target/classes/f1/security/SecurityConfig.class differ diff --git a/backend/target/classes/init.sql b/backend/target/classes/init.sql new file mode 100755 index 0000000..e51e331 --- /dev/null +++ b/backend/target/classes/init.sql @@ -0,0 +1,148 @@ +-- 车手表 +CREATE TABLE IF NOT EXISTS driver( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100), + country VARCHAR(100), + birthday DATE +); + +-- 车队表 +CREATE TABLE IF NOT EXISTS team( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100), + country VARCHAR(100), + engine_supplier VARCHAR(100), + setup_time YEAR +); + +-- 合同表 +CREATE TABLE IF NOT EXISTS contract( + id INT AUTO_INCREMENT PRIMARY KEY, + driver_id INT, + team_id INT, + season YEAR, + car_num INT, + is_reserve BOOLEAN, + FOREIGN KEY(driver_id) REFERENCES driver(id), + FOREIGN KEY(team_id) REFERENCES team(id) +); + +-- 赛道表 +CREATE TABLE IF NOT EXISTS circuit( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100), + location VARCHAR(100), + country VARCHAR(100), + drs_zones INT +); + +-- 赛事表 +CREATE TABLE IF NOT EXISTS prix( + id INT AUTO_INCREMENT PRIMARY KEY, + season YEAR, + round INT, + name VARCHAR(100), + circuit_id INT, + have_sprint BOOLEAN, + FOREIGN KEY(circuit_id) REFERENCES circuit(id) +); + +-- 排位赛结果表 +CREATE TABLE IF NOT EXISTS qualifying_result( + prix_id INT, + driver_id INT, + team_id INT, + fastest_time VARCHAR(20), + position INT, + is_sprint BOOLEAN, + section INT, + FOREIGN KEY(prix_id) REFERENCES prix(id), + FOREIGN KEY(driver_id) REFERENCES driver(id), + FOREIGN KEY(team_id) REFERENCES team(id) +); + +-- 正赛结果表 +CREATE TABLE IF NOT EXISTS race_result( + prix_id INT, + driver_id INT, + team_id INT, + start_position INT, + end_position INT, + finish_time VARCHAR(20), + fastest_time VARCHAR(20), + score INT, + is_sprint BOOLEAN, + FOREIGN KEY(prix_id) REFERENCES prix(id), + FOREIGN KEY(driver_id) REFERENCES driver(id), + FOREIGN KEY(team_id) REFERENCES team(id) +); + +-- 用户表 +CREATE TABLE IF NOT EXISTS user( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(100) UNIQUE, + password VARCHAR(100), + email VARCHAR(100), + country VARCHAR(100), + avatar VARCHAR(500) +); + +-- 评论表 +CREATE TABLE IF NOT EXISTS comment( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT, + response_id INT, + root_id INT, + content VARCHAR(500), + FOREIGN KEY(user_id) REFERENCES user(id), + FOREIGN KEY(response_id) REFERENCES comment(id) ON DELETE CASCADE, + FOREIGN KEY(root_id) REFERENCES comment(id) ON DELETE CASCADE +); + +-- 赛季车手信息视图 +CREATE OR REPLACE VIEW season_driver( + id, name, team, country, birthday, car_num, season +) AS +SELECT D.id, + D.name, + T.name, + D.country, + D.birthday, + C.car_num, + C.season +FROM contract C +JOIN team T ON T.id = C.team_id +JOIN driver D ON C.driver_id = D.id; + +-- 赛季车队积分榜视图 +CREATE OR REPLACE VIEW season_team_standings AS +SELECT + p.season, + rr.team_id, + SUM(rr.score) AS total_score, + RANK() OVER (PARTITION BY p.season ORDER BY SUM(rr.score) DESC) AS ranking +FROM race_result rr +JOIN prix p ON rr.prix_id = p.id +GROUP BY p.season, rr.team_id; + +CREATE OR REPLACE VIEW prix_result AS +SELECT + p.season, + p.id AS prix_id, + rr.driver_id, + rr.team_id, + p.name AS prix_name, + p.round, + sd.name AS driver_name, + sd.car_num, + sd.team AS team_name, + rr.end_position AS pos, + rr.finish_time, + rr.score, + rr.is_sprint +FROM + race_result rr +JOIN + prix p ON rr.prix_id = p.id +JOIN + season_driver sd ON rr.driver_id = sd.id AND sd.season = p.season; \ No newline at end of file diff --git a/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100755 index 0000000..e69de29 diff --git a/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100755 index 0000000..5ac687f --- /dev/null +++ b/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,27 @@ +/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/Driver.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/QualifyingResultItem.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/controller/UserController.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/db/Database.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/security/SecurityConfig.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/security/JwtFilter.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/Main.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/db/TeamDao.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/User.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/AppConfig.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/SeasonDriver.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/security/JwtUtil.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/CommentItem.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/RaceResultItem.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/controller/AuthController.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/DriverStatistic.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/db/UserDao.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/TeamHistoryItem.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/Team.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/db/DriverDao.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/TeamStatistic.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/SeasonScheduleItem.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/DriverHistoryItem.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/controller/CommentController.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/db/RaceDao.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/controller/F1Controller.java +/Users/colden/编程/web/formula1/backend/src/main/java/f1/db/BaseDao.java diff --git a/frontend/env.d.ts b/frontend/env.d.ts new file mode 100755 index 0000000..11f02fe --- /dev/null +++ b/frontend/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/index.html b/frontend/index.html new file mode 100755 index 0000000..874049c --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100755 index 0000000..a4240f5 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/frontend/src/api/comments.ts b/frontend/src/api/comments.ts new file mode 100755 index 0000000..6d0f27f --- /dev/null +++ b/frontend/src/api/comments.ts @@ -0,0 +1,38 @@ +import { apiJSON } from './http' + +export interface CommentItem { + id: number + username: string + content: string + user_id: number + parentId?: number + toUser?: string +} + +export interface CommentListResp { + items: CommentItem[] + total: number +} + +export const fetchLatestComments = (page: number, pageSize: number) => { + const offset = (page - 1) * pageSize + return apiJSON(`/api/comments?limit=100&offset=${offset}&pageSize=${pageSize}`) +} + +export const postComment = (user_id: number, content: string) => { + return apiJSON<{}>('/api/comments', { + method: 'POST', + body: JSON.stringify({ user_id, content }) + }) +} + +export const fetchCommentThread = (id: number) => { + return apiJSON(`/api/comments/${id}/replies`) +} + +export const postReply = (rootId: number, content: string, user_id: number, response_id: number = -1) => { + return apiJSON<{}>(`/api/comments/${rootId}/replies`, { + method: 'POST', + body: response_id === -1 ? JSON.stringify({ user_id, content }) : JSON.stringify({ user_id, content, response_id }) + }) +} diff --git a/frontend/src/api/http.ts b/frontend/src/api/http.ts new file mode 100755 index 0000000..fd669bf --- /dev/null +++ b/frontend/src/api/http.ts @@ -0,0 +1,27 @@ +import { getToken } from '@/utils/auth' + +export async function apiFetch(path: string, options: RequestInit = {}) { + const token = getToken() + const headers: Record = { + 'Content-Type': 'application/json', + ...(options.headers as Record || {}), + } + if (token) headers['Authorization'] = `Bearer ${token}` + const res = await fetch(path, { + ...options, + headers, + credentials: 'include', + }) + return res +} + +export async function apiJSON(path: string, options: RequestInit = {}) { + const res = await apiFetch(path, options) + if (!res.ok) throw new Error(`请求失败: ${res.status}`) + const ct = res.headers.get('content-type') || '' + if (ct.includes('application/json')) return res.json() as Promise + const text = await res.text() + // 文本成功兼容 + if (/success/i.test(text)) return ({ success: true } as unknown) as T + throw new Error(text || '请求失败') +} diff --git a/frontend/src/assets/.DS_Store b/frontend/src/assets/.DS_Store new file mode 100755 index 0000000..cbc3ec1 Binary files /dev/null and b/frontend/src/assets/.DS_Store differ diff --git a/frontend/src/assets/Formula1-Black.woff2 b/frontend/src/assets/Formula1-Black.woff2 new file mode 100755 index 0000000..8fbb381 Binary files /dev/null and b/frontend/src/assets/Formula1-Black.woff2 differ diff --git a/frontend/src/assets/drivers/alb.avif b/frontend/src/assets/drivers/alb.avif new file mode 100755 index 0000000..a663b18 Binary files /dev/null and b/frontend/src/assets/drivers/alb.avif differ diff --git a/frontend/src/assets/drivers/alo.avif b/frontend/src/assets/drivers/alo.avif new file mode 100755 index 0000000..2ed74eb Binary files /dev/null and b/frontend/src/assets/drivers/alo.avif differ diff --git a/frontend/src/assets/drivers/ant.avif b/frontend/src/assets/drivers/ant.avif new file mode 100755 index 0000000..da0923c Binary files /dev/null and b/frontend/src/assets/drivers/ant.avif differ diff --git a/frontend/src/assets/drivers/ber.avif b/frontend/src/assets/drivers/ber.avif new file mode 100755 index 0000000..e8507be Binary files /dev/null and b/frontend/src/assets/drivers/ber.avif differ diff --git a/frontend/src/assets/drivers/bor.avif b/frontend/src/assets/drivers/bor.avif new file mode 100755 index 0000000..4b8b6d8 Binary files /dev/null and b/frontend/src/assets/drivers/bor.avif differ diff --git a/frontend/src/assets/drivers/col.avif b/frontend/src/assets/drivers/col.avif new file mode 100755 index 0000000..3a17a7a Binary files /dev/null and b/frontend/src/assets/drivers/col.avif differ diff --git a/frontend/src/assets/drivers/gas.avif b/frontend/src/assets/drivers/gas.avif new file mode 100755 index 0000000..4e0b8a7 Binary files /dev/null and b/frontend/src/assets/drivers/gas.avif differ diff --git a/frontend/src/assets/drivers/haj.avif b/frontend/src/assets/drivers/haj.avif new file mode 100755 index 0000000..6993d08 Binary files /dev/null and b/frontend/src/assets/drivers/haj.avif differ diff --git a/frontend/src/assets/drivers/ham.avif b/frontend/src/assets/drivers/ham.avif new file mode 100755 index 0000000..b0602f3 Binary files /dev/null and b/frontend/src/assets/drivers/ham.avif differ diff --git a/frontend/src/assets/drivers/hul.avif b/frontend/src/assets/drivers/hul.avif new file mode 100755 index 0000000..df96b5d Binary files /dev/null and b/frontend/src/assets/drivers/hul.avif differ diff --git a/frontend/src/assets/drivers/lando.avif b/frontend/src/assets/drivers/lando.avif new file mode 100755 index 0000000..3deabb1 Binary files /dev/null and b/frontend/src/assets/drivers/lando.avif differ diff --git a/frontend/src/assets/drivers/law.avif b/frontend/src/assets/drivers/law.avif new file mode 100755 index 0000000..7c63279 Binary files /dev/null and b/frontend/src/assets/drivers/law.avif differ diff --git a/frontend/src/assets/drivers/lec.avif b/frontend/src/assets/drivers/lec.avif new file mode 100755 index 0000000..fc572f2 Binary files /dev/null and b/frontend/src/assets/drivers/lec.avif differ diff --git a/frontend/src/assets/drivers/max.avif b/frontend/src/assets/drivers/max.avif new file mode 100755 index 0000000..ce4113d Binary files /dev/null and b/frontend/src/assets/drivers/max.avif differ diff --git a/frontend/src/assets/drivers/oc.avif b/frontend/src/assets/drivers/oc.avif new file mode 100755 index 0000000..9dfd282 Binary files /dev/null and b/frontend/src/assets/drivers/oc.avif differ diff --git a/frontend/src/assets/drivers/piastri.avif b/frontend/src/assets/drivers/piastri.avif new file mode 100755 index 0000000..194e4d2 Binary files /dev/null and b/frontend/src/assets/drivers/piastri.avif differ diff --git a/frontend/src/assets/drivers/russell.avif b/frontend/src/assets/drivers/russell.avif new file mode 100755 index 0000000..762efa3 Binary files /dev/null and b/frontend/src/assets/drivers/russell.avif differ diff --git a/frontend/src/assets/drivers/sai.avif b/frontend/src/assets/drivers/sai.avif new file mode 100755 index 0000000..f4a37c4 Binary files /dev/null and b/frontend/src/assets/drivers/sai.avif differ diff --git a/frontend/src/assets/drivers/str.avif b/frontend/src/assets/drivers/str.avif new file mode 100755 index 0000000..976ba01 Binary files /dev/null and b/frontend/src/assets/drivers/str.avif differ diff --git a/frontend/src/assets/drivers/yuki.avif b/frontend/src/assets/drivers/yuki.avif new file mode 100755 index 0000000..01a2434 Binary files /dev/null and b/frontend/src/assets/drivers/yuki.avif differ diff --git a/frontend/src/assets/far.jpg b/frontend/src/assets/far.jpg new file mode 100755 index 0000000..30cb68f Binary files /dev/null and b/frontend/src/assets/far.jpg differ diff --git a/frontend/src/assets/heying.jpg b/frontend/src/assets/heying.jpg new file mode 100755 index 0000000..9e81806 Binary files /dev/null and b/frontend/src/assets/heying.jpg differ diff --git a/frontend/src/assets/icon/font/demo.css b/frontend/src/assets/icon/font/demo.css new file mode 100755 index 0000000..a67054a --- /dev/null +++ b/frontend/src/assets/icon/font/demo.css @@ -0,0 +1,539 @@ +/* Logo 字体 */ +@font-face { + font-family: "iconfont logo"; + src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834'); + src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'), + url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'), + url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'), + url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg'); +} + +.logo { + font-family: "iconfont logo"; + font-size: 160px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* tabs */ +.nav-tabs { + position: relative; +} + +.nav-tabs .nav-more { + position: absolute; + right: 0; + bottom: 0; + height: 42px; + line-height: 42px; + color: #666; +} + +#tabs { + border-bottom: 1px solid #eee; +} + +#tabs li { + cursor: pointer; + width: 100px; + height: 40px; + line-height: 40px; + text-align: center; + font-size: 16px; + border-bottom: 2px solid transparent; + position: relative; + z-index: 1; + margin-bottom: -1px; + color: #666; +} + + +#tabs .active { + border-bottom-color: #f00; + color: #222; +} + +.tab-container .content { + display: none; +} + +/* 页面布局 */ +.main { + padding: 30px 100px; + width: 960px; + margin: 0 auto; +} + +.main .logo { + color: #333; + text-align: left; + margin-bottom: 30px; + line-height: 1; + height: 110px; + margin-top: -50px; + overflow: hidden; + *zoom: 1; +} + +.main .logo a { + font-size: 160px; + color: #333; +} + +.helps { + margin-top: 40px; +} + +.helps pre { + padding: 20px; + margin: 10px 0; + border: solid 1px #e7e1cd; + background-color: #fffdef; + overflow: auto; +} + +.icon_lists { + width: 100% !important; + overflow: hidden; + *zoom: 1; +} + +.icon_lists li { + width: 100px; + margin-bottom: 10px; + margin-right: 20px; + text-align: center; + list-style: none !important; + cursor: default; +} + +.icon_lists li .code-name { + line-height: 1.2; +} + +.icon_lists .icon { + display: block; + height: 100px; + line-height: 100px; + font-size: 42px; + margin: 10px auto; + color: #333; + -webkit-transition: font-size 0.25s linear, width 0.25s linear; + -moz-transition: font-size 0.25s linear, width 0.25s linear; + transition: font-size 0.25s linear, width 0.25s linear; +} + +.icon_lists .icon:hover { + font-size: 100px; +} + +.icon_lists .svg-icon { + /* 通过设置 font-size 来改变图标大小 */ + width: 1em; + /* 图标和文字相邻时,垂直对齐 */ + vertical-align: -0.15em; + /* 通过设置 color 来改变 SVG 的颜色/fill */ + fill: currentColor; + /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示 + normalize.css 中也包含这行 */ + overflow: hidden; +} + +.icon_lists li .name, +.icon_lists li .code-name { + color: #666; +} + +/* markdown 样式 */ +.markdown { + color: #666; + font-size: 14px; + line-height: 1.8; +} + +.highlight { + line-height: 1.5; +} + +.markdown img { + vertical-align: middle; + max-width: 100%; +} + +.markdown h1 { + color: #404040; + font-weight: 500; + line-height: 40px; + margin-bottom: 24px; +} + +.markdown h2, +.markdown h3, +.markdown h4, +.markdown h5, +.markdown h6 { + color: #404040; + margin: 1.6em 0 0.6em 0; + font-weight: 500; + clear: both; +} + +.markdown h1 { + font-size: 28px; +} + +.markdown h2 { + font-size: 22px; +} + +.markdown h3 { + font-size: 16px; +} + +.markdown h4 { + font-size: 14px; +} + +.markdown h5 { + font-size: 12px; +} + +.markdown h6 { + font-size: 12px; +} + +.markdown hr { + height: 1px; + border: 0; + background: #e9e9e9; + margin: 16px 0; + clear: both; +} + +.markdown p { + margin: 1em 0; +} + +.markdown>p, +.markdown>blockquote, +.markdown>.highlight, +.markdown>ol, +.markdown>ul { + width: 80%; +} + +.markdown ul>li { + list-style: circle; +} + +.markdown>ul li, +.markdown blockquote ul>li { + margin-left: 20px; + padding-left: 4px; +} + +.markdown>ul li p, +.markdown>ol li p { + margin: 0.6em 0; +} + +.markdown ol>li { + list-style: decimal; +} + +.markdown>ol li, +.markdown blockquote ol>li { + margin-left: 20px; + padding-left: 4px; +} + +.markdown code { + margin: 0 3px; + padding: 0 5px; + background: #eee; + border-radius: 3px; +} + +.markdown strong, +.markdown b { + font-weight: 600; +} + +.markdown>table { + border-collapse: collapse; + border-spacing: 0px; + empty-cells: show; + border: 1px solid #e9e9e9; + width: 95%; + margin-bottom: 24px; +} + +.markdown>table th { + white-space: nowrap; + color: #333; + font-weight: 600; +} + +.markdown>table th, +.markdown>table td { + border: 1px solid #e9e9e9; + padding: 8px 16px; + text-align: left; +} + +.markdown>table th { + background: #F7F7F7; +} + +.markdown blockquote { + font-size: 90%; + color: #999; + border-left: 4px solid #e9e9e9; + padding-left: 0.8em; + margin: 1em 0; +} + +.markdown blockquote p { + margin: 0; +} + +.markdown .anchor { + opacity: 0; + transition: opacity 0.3s ease; + margin-left: 8px; +} + +.markdown .waiting { + color: #ccc; +} + +.markdown h1:hover .anchor, +.markdown h2:hover .anchor, +.markdown h3:hover .anchor, +.markdown h4:hover .anchor, +.markdown h5:hover .anchor, +.markdown h6:hover .anchor { + opacity: 1; + display: inline-block; +} + +.markdown>br, +.markdown>p>br { + clear: both; +} + + +.hljs { + display: block; + background: white; + padding: 0.5em; + color: #333333; + overflow-x: auto; +} + +.hljs-comment, +.hljs-meta { + color: #969896; +} + +.hljs-string, +.hljs-variable, +.hljs-template-variable, +.hljs-strong, +.hljs-emphasis, +.hljs-quote { + color: #df5000; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-type { + color: #a71d5d; +} + +.hljs-literal, +.hljs-symbol, +.hljs-bullet, +.hljs-attribute { + color: #0086b3; +} + +.hljs-section, +.hljs-name { + color: #63a35c; +} + +.hljs-tag { + color: #333333; +} + +.hljs-title, +.hljs-attr, +.hljs-selector-id, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #795da3; +} + +.hljs-addition { + color: #55a532; + background-color: #eaffea; +} + +.hljs-deletion { + color: #bd2c00; + background-color: #ffecec; +} + +.hljs-link { + text-decoration: underline; +} + +/* 代码高亮 */ +/* PrismJS 1.15.0 +https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ +/** + * prism.js default theme for JavaScript, CSS and HTML + * Based on dabblet (http://dabblet.com) + * @author Lea Verou + */ +code[class*="language-"], +pre[class*="language-"] { + color: black; + background: none; + text-shadow: 0 1px white; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre[class*="language-"]::-moz-selection, +pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, +code[class*="language-"] ::-moz-selection { + text-shadow: none; + background: #b3d4fc; +} + +pre[class*="language-"]::selection, +pre[class*="language-"] ::selection, +code[class*="language-"]::selection, +code[class*="language-"] ::selection { + text-shadow: none; + background: #b3d4fc; +} + +@media print { + + code[class*="language-"], + pre[class*="language-"] { + text-shadow: none; + } +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +:not(pre)>code[class*="language-"], +pre[class*="language-"] { + background: #f5f2f0; +} + +/* Inline code */ +:not(pre)>code[class*="language-"] { + padding: .1em; + border-radius: .3em; + white-space: normal; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: slategray; +} + +.token.punctuation { + color: #999; +} + +.namespace { + opacity: .7; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.deleted { + color: #905; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #690; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: #9a6e3a; + background: hsla(0, 0%, 100%, .5); +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #07a; +} + +.token.function, +.token.class-name { + color: #DD4A68; +} + +.token.regex, +.token.important, +.token.variable { + color: #e90; +} + +.token.important, +.token.bold { + font-weight: bold; +} + +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} diff --git a/frontend/src/assets/icon/font/demo_index.html b/frontend/src/assets/icon/font/demo_index.html new file mode 100755 index 0000000..c41e426 --- /dev/null +++ b/frontend/src/assets/icon/font/demo_index.html @@ -0,0 +1,303 @@ + + + + + iconfont Demo + + + + + + + + + + + + + +
+

+ + +

+ +
+
+
    + +
  • + +
    cup
    +
    &#xe637;
    +
  • + +
  • + +
    race car
    +
    &#xe600;
    +
  • + +
  • + +
    sports_icon_racing car@2x
    +
    &#xe67b;
    +
  • + +
  • + +
    Home, homepage, menu
    +
    &#xe9db;
    +
  • + +
  • + +
    icon_task_details_milestone
    +
    &#xe6c4;
    +
  • + +
+
+

Unicode 引用

+
+ +

Unicode 是字体在网页端最原始的应用方式,特点是:

+
    +
  • 支持按字体的方式去动态调整图标大小,颜色等等。
  • +
  • 默认情况下不支持多色,直接添加多色图标会自动去色。
  • +
+
+

注意:新版 iconfont 支持两种方式引用多色图标:SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)

+
+

Unicode 使用步骤如下:

+

第一步:拷贝项目下面生成的 @font-face

+
@font-face {
+  font-family: 'iconfont';
+  src: url('iconfont.woff2?t=1764608411765') format('woff2'),
+       url('iconfont.woff?t=1764608411765') format('woff'),
+       url('iconfont.ttf?t=1764608411765') format('truetype');
+}
+
+

第二步:定义使用 iconfont 的样式

+
.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+

第三步:挑选相应图标并获取字体编码,应用于页面

+
+<span class="iconfont">&#x33;</span>
+
+
+

"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

+
+
+
+
+
    + +
  • + +
    + cup +
    +
    .icon-cup +
    +
  • + +
  • + +
    + race car +
    +
    .icon-car +
    +
  • + +
  • + +
    + sports_icon_racing car@2x +
    +
    .icon-driver +
    +
  • + +
  • + +
    + Home, homepage, menu +
    +
    .icon-home +
    +
  • + +
  • + +
    + icon_task_details_milestone +
    +
    .icon-milestone +
    +
  • + +
+
+

font-class 引用

+
+ +

font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。

+

与 Unicode 使用方式相比,具有如下特点:

+
    +
  • 相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。
  • +
  • 因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。
  • +
+

使用步骤如下:

+

第一步:引入项目下面生成的 fontclass 代码:

+
<link rel="stylesheet" href="./iconfont.css">
+
+

第二步:挑选相应图标并获取类名,应用于页面:

+
<span class="iconfont icon-xxx"></span>
+
+
+

" + iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

+
+
+
+
+
    + +
  • + +
    cup
    +
    #icon-cup
    +
  • + +
  • + +
    race car
    +
    #icon-car
    +
  • + +
  • + +
    sports_icon_racing car@2x
    +
    #icon-driver
    +
  • + +
  • + +
    Home, homepage, menu
    +
    #icon-home
    +
  • + +
  • + +
    icon_task_details_milestone
    +
    #icon-milestone
    +
  • + +
+
+

Symbol 引用

+
+ +

这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇文章 + 这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:

+
    +
  • 支持多色图标了,不再受单色限制。
  • +
  • 通过一些技巧,支持像字体那样,通过 font-size, color 来调整样式。
  • +
  • 兼容性较差,支持 IE9+,及现代浏览器。
  • +
  • 浏览器渲染 SVG 的性能一般,还不如 png。
  • +
+

使用步骤如下:

+

第一步:引入项目下面生成的 symbol 代码:

+
<script src="./iconfont.js"></script>
+
+

第二步:加入通用 CSS 代码(引入一次就行):

+
<style>
+.icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+</style>
+
+

第三步:挑选相应图标并获取类名,应用于页面:

+
<svg class="icon" aria-hidden="true">
+  <use xlink:href="#icon-xxx"></use>
+</svg>
+
+
+
+ +
+
+ + + diff --git a/frontend/src/assets/icon/font/iconfont.css b/frontend/src/assets/icon/font/iconfont.css new file mode 100755 index 0000000..6a10e9b --- /dev/null +++ b/frontend/src/assets/icon/font/iconfont.css @@ -0,0 +1,35 @@ +@font-face { + font-family: "iconfont"; /* Project id 5075080 */ + src: url('iconfont.woff2?t=1764608411765') format('woff2'), + url('iconfont.woff?t=1764608411765') format('woff'), + url('iconfont.ttf?t=1764608411765') format('truetype'); +} + +.iconfont { + font-family: "iconfont" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-cup:before { + content: "\e637"; +} + +.icon-car:before { + content: "\e600"; +} + +.icon-driver:before { + content: "\e67b"; +} + +.icon-home:before { + content: "\e9db"; +} + +.icon-milestone:before { + content: "\e6c4"; +} + diff --git a/frontend/src/assets/icon/font/iconfont.js b/frontend/src/assets/icon/font/iconfont.js new file mode 100755 index 0000000..5bdce75 --- /dev/null +++ b/frontend/src/assets/icon/font/iconfont.js @@ -0,0 +1 @@ +window._iconfont_svg_string_5075080='',(e=>{var t=(c=(c=document.getElementsByTagName("script"))[c.length-1]).getAttribute("data-injectcss"),c=c.getAttribute("data-disable-injectsvg");if(!c){var l,n,i,o,f,a=function(t,c){c.parentNode.insertBefore(t,c)};if(t&&!e.__iconfont__svg__cssinject__){e.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(t){console&&console.log(t)}}l=function(){var t,c=document.createElement("div");c.innerHTML=e._iconfont_svg_string_5075080,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(t=document.body).firstChild?a(c,t.firstChild):t.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(l,0):(n=function(){document.removeEventListener("DOMContentLoaded",n,!1),l()},document.addEventListener("DOMContentLoaded",n,!1)):document.attachEvent&&(i=l,o=e.document,f=!1,s(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,d())})}function d(){f||(f=!0,i())}function s(){try{o.documentElement.doScroll("left")}catch(t){return void setTimeout(s,50)}d()}})(window); \ No newline at end of file diff --git a/frontend/src/assets/icon/font/iconfont.json b/frontend/src/assets/icon/font/iconfont.json new file mode 100755 index 0000000..0ca6cba --- /dev/null +++ b/frontend/src/assets/icon/font/iconfont.json @@ -0,0 +1,44 @@ +{ + "id": "5075080", + "name": "f1", + "font_family": "iconfont", + "css_prefix_text": "icon-", + "description": "", + "glyphs": [ + { + "icon_id": "1206006", + "name": "cup", + "font_class": "cup", + "unicode": "e637", + "unicode_decimal": 58935 + }, + { + "icon_id": "3900649", + "name": "race car", + "font_class": "car", + "unicode": "e600", + "unicode_decimal": 58880 + }, + { + "icon_id": "9936992", + "name": "sports_icon_racing car@2x", + "font_class": "driver", + "unicode": "e67b", + "unicode_decimal": 59003 + }, + { + "icon_id": "11982742", + "name": "Home, homepage, menu", + "font_class": "home", + "unicode": "e9db", + "unicode_decimal": 59867 + }, + { + "icon_id": "13570233", + "name": "icon_task_details_milestone", + "font_class": "milestone", + "unicode": "e6c4", + "unicode_decimal": 59076 + } + ] +} diff --git a/frontend/src/assets/icon/font/iconfont.ttf b/frontend/src/assets/icon/font/iconfont.ttf new file mode 100755 index 0000000..6477e05 Binary files /dev/null and b/frontend/src/assets/icon/font/iconfont.ttf differ diff --git a/frontend/src/assets/icon/font/iconfont.woff b/frontend/src/assets/icon/font/iconfont.woff new file mode 100755 index 0000000..d76ec65 Binary files /dev/null and b/frontend/src/assets/icon/font/iconfont.woff differ diff --git a/frontend/src/assets/icon/font/iconfont.woff2 b/frontend/src/assets/icon/font/iconfont.woff2 new file mode 100755 index 0000000..32d52d1 Binary files /dev/null and b/frontend/src/assets/icon/font/iconfont.woff2 differ diff --git a/frontend/src/assets/logo.webp b/frontend/src/assets/logo.webp new file mode 100755 index 0000000..77ae03b Binary files /dev/null and b/frontend/src/assets/logo.webp differ diff --git a/frontend/src/assets/prix/Australia.avif b/frontend/src/assets/prix/Australia.avif new file mode 100755 index 0000000..86a069d Binary files /dev/null and b/frontend/src/assets/prix/Australia.avif differ diff --git a/frontend/src/assets/prix/Bahrain.avif b/frontend/src/assets/prix/Bahrain.avif new file mode 100755 index 0000000..13ba5ab Binary files /dev/null and b/frontend/src/assets/prix/Bahrain.avif differ diff --git a/frontend/src/assets/prix/China.avif b/frontend/src/assets/prix/China.avif new file mode 100755 index 0000000..771e89c Binary files /dev/null and b/frontend/src/assets/prix/China.avif differ diff --git a/frontend/src/assets/prix/Japan.avif b/frontend/src/assets/prix/Japan.avif new file mode 100755 index 0000000..9b257be Binary files /dev/null and b/frontend/src/assets/prix/Japan.avif differ diff --git a/frontend/src/assets/rb.jpg b/frontend/src/assets/rb.jpg new file mode 100755 index 0000000..041e7e2 Binary files /dev/null and b/frontend/src/assets/rb.jpg differ diff --git a/frontend/src/assets/source.ts b/frontend/src/assets/source.ts new file mode 100755 index 0000000..3b90a19 --- /dev/null +++ b/frontend/src/assets/source.ts @@ -0,0 +1,689 @@ +import piastri from '@/assets/drivers/piastri.avif' +import lando from '@/assets/drivers/lando.avif' +import russell from '@/assets/drivers/russell.avif' +import ant from '@/assets/drivers/ant.avif' +import max from '@/assets/drivers/max.avif' +import yuki from '@/assets/drivers/yuki.avif' +import ham from '@/assets/drivers/ham.avif' +import lec from '@/assets/drivers/lec.avif' +import alb from '@/assets/drivers/alb.avif' +import sai from '@/assets/drivers/sai.avif' +import law from '@/assets/drivers/law.avif' +import haj from '@/assets/drivers/haj.avif' +import str from '@/assets/drivers/str.avif' +import alo from '@/assets/drivers/alo.avif' +import hul from '@/assets/drivers/hul.avif' +import bor from '@/assets/drivers/bor.avif' +import oc from '@/assets/drivers/oc.avif' +import ber from '@/assets/drivers/ber.avif' +import gas from '@/assets/drivers/gas.avif' +import col from '@/assets/drivers/col.avif' + +import mclaren from '@/assets/teams/mclaren.avif' +import mercedes from '@/assets/teams/merc.avif' +import redbull from '@/assets/teams/rb.avif' +import ferrari from '@/assets/teams/fe.avif' +import williams from '@/assets/teams/will.avif' +import racingBulls from '@/assets/teams/srb.avif' +import astonMartin from '@/assets/teams/am.avif' +import haasF1Team from '@/assets/teams/hass.avif' +import kickSauber from '@/assets/teams/kick.avif' +import alpine from '@/assets/teams/alp.avif' + +import mclarenLogo from '@/assets/teams/mclogo.avif' +import mercedesLogo from '@/assets/teams/merclogo.avif' +import redbullLogo from '@/assets/teams/rblogo.avif' +import ferrariLogo from '@/assets/teams/felogo.avif' +import williamsLogo from '@/assets/teams/willogo.avif' +import racingBullsLogo from '@/assets/teams/srblogo.avif' +import astonMartinLogo from '@/assets/teams/amlogo.avif' +import haasF1TeamLogo from '@/assets/teams/hasslogo.avif' +import kickSauberLogo from '@/assets/teams/kicklogo.avif' +import alpineLogo from '@/assets/teams/alplogo.avif' + +import australia from '@/assets/prix/australia.avif' +import china from '@/assets/prix/china.avif' +import japan from '@/assets/prix/japan.avif' +import bahrain from '@/assets/prix/bahrain.avif' + +export const getColor = (team: string) : string => { + switch (team) { + case "McLaren": + return "#eb7100"; + case "Mercedes": + return "#00cfaf"; + case "Red Bull Racing": + return "#003282"; + case "Ferrari": + return "#710006"; + case "Williams": + return "#155dd1"; + case "Racing Bulls": + return "#2345ab"; + case "Aston Martin": + return "#00482c"; + case "Haas F1 Team": + return "#4d5052"; + case "Kick Sauber": + return "#006300"; + case "Alpine": + return "#005081"; + default: + return "#000"; + } +} + +export const getLogoColor = (team: string) : string => { + switch (team) { + case "McLaren": + return "#eb7100"; + case "Mercedes": + return "#00cfaf"; + case "Red Bull Racing": + return "#003282"; + case "Ferrari": + return "#710006"; + case "Williams": + return "#155dd1"; + case "Racing Bulls": + return "#2345ab"; + case "Aston Martin": + return "#00482c"; + case "Haas F1 Team": + return "#4d5052"; + case "Kick Sauber": + return "#006300"; + case "Alpine": + return "#005081"; + default: + return "#000"; + } +} + +export const getImage = (name: string) : string => { + switch (name) { + case "Oscar Piastri": + return piastri; + case "Lando Norris": + return lando; + case "George Russell": + return russell; + case "Kimi Antonelli": + return ant; + case "Max Verstappen": + return max; + case "Yuki Tsunoda": + return yuki; + case "Lewis Hamilton": + return ham; + case "Charles Leclerc": + return lec; + case "Alexander Albon": + return alb; + case "Carlos Sainz": + return sai; + case "Liam Lawson": + return law; + case "Isack Hadjar": + return haj; + case "Lance Stroll": + return str; + case "Fernando Alonso": + return alo; + case "Esteban Ocon": + return oc; + case "Oliver Bearman": + return ber; + case "Nico Hulkenberg": + return hul; + case "Gabriel Bortoleto": + return bor; + case "Pierre Gasly": + return gas; + case "Franco Colapinto": + return col; + default: + return ""; + } +} + +export const getCarImage = (team: string) : string => { + switch (team) { + case "McLaren": + return mclaren; + case "Mercedes": + return mercedes; + case "Red Bull Racing": + return redbull; + case "Ferrari": + return ferrari; + case "Williams": + return williams; + case "Racing Bulls": + return racingBulls; + case "Aston Martin": + return astonMartin; + case "Haas F1 Team": + return haasF1Team; + case "Kick Sauber": + return kickSauber; + case "Alpine": + return alpine; + default: + return ""; + } +} + +export const getLogo = (team: string) : string => { + switch (team) { + case "McLaren": + return mclarenLogo; + case "Mercedes": + return mercedesLogo; + case "Red Bull Racing": + return redbullLogo; + case "Ferrari": + return ferrariLogo; + case "Williams": + return williamsLogo; + case "Racing Bulls": + return racingBullsLogo; + case "Aston Martin": + return astonMartinLogo; + case "Haas F1 Team": + return haasF1TeamLogo; + case "Kick Sauber": + return kickSauberLogo; + case "Alpine": + return alpineLogo; + default: + return ""; + } +} + +export const prix = [ + { + id: 1, + name: "Australia", + image: australia, + }, + { + id: 2, + name: "China", + image: china, + }, + { + id: 3, + name: "Japan", + image: japan, + }, + { + id: 4, + name: "Bahrain", + image: bahrain, + } +] + +/* export const teams = [ + { + id: 1, + name: "McLaren", + nation: "United Kingdom", + image: mclaren, + color: "#eb7100", + driver1: "Oscar Piastri", + driver2: "Lando Norris", + logo: mclarenLogo + }, + { + id: 2, + name: "Mercedes", + nation: "Germany", + image: mercedes, + color: "#00d2be", + driver1: "George Russell", + driver2: "Kimi Antonell", + logo: mercedesLogo + }, + { + id: 3, + name: "Red Bull Racing", + nation: "United Kingdom", + image: redbull, + color: "#003282", + driver1: "Max Verstappen", + driver2: "yuki Tsunoda", + logo: redbullLogo + }, + { + id: 4, + name: "Ferrari", + nation: "Italy", + image: ferrari, + color: "#dc0000", + driver1: "Lewis Hamilton", + driver2: "Charles Leclerc", + logo: ferrariLogo + }, +] +*/ + +export const prixes = [ + { + id: 1, + name: "澳大利亚", + date: "16 Mar", + pos: 0, + pts: 0 + }, + { + id: 2, + name: "中国", + date: "23 Mar", + pos: 0, + pts: 0 + }, + { + id: 3, + name: "日本", + date: "06 Apr", + pos: 0, + pts: 0 + }, + { + id: 4, + name: "巴林", + date: "13 Apr", + pos: 0, + pts: 0 + }, + { + id: 5, + name: "沙特阿拉伯", + date: "20 Apr", + pos: 0, + pts: 0 + }, + { + id: 6, + name: "迈阿密", + date: "04 May", + pos: 0, + pts: 0 + }, + { + id: 7, + name: "伊莫拉", + date: "18 May", + pos: 0, + pts: 0 + }, + { + id: 8, + name: "摩纳哥", + date: "25 May", + pos: 0, + pts: 0 + }, + { + id: 9, + name: "西班牙", + date: "01 Jun", + pos: 0, + pts: 0 + }, + { + id: 10, + name: "意大利", + date: "15 Jun", + pos: 0, + pts: 0 + }, +] + +export const driver_career = [ + { + name: "Oscar Piastri", + races: 69, + points: 781, + hf: 1, + podiums: 25, + hg: 1, + polepositions: 6, + wc: 0, + dnfs: 4, + wins: 9 + }, + { + name: "Lando Norris", + races: 151, + points: 1415, + hf: 1, + podiums: 43, + hg: 1, + polepositions: 16, + wc: 0, + dnfs: 13, + wins: 11 + }, + { + name: "George Russell", + races: 151, + points: 1023, + hf: 1, + podiums: 24, + hg: 1, + polepositions: 7, + wc: 0, + dnfs: 19, + wins: 5 + }, + { + name: "Kimi Antonelli", + races: 23, + points: 150, + hf: 2, + podiums: 3, + hg: 2, + polepositions: 0, + wc: 0, + dnfs: 4, + wins: 0 + }, + { + name: "Max Verstappen", + races: 232, + points: 3419.5, + hf: 1, + podiums: 126, + hg: 1, + polepositions: 47, + wc: 4, + dnfs: 33, + wins: 71 + }, + { + name: "yuki Tsunoda", + races: 110, + points: 124, + hf: 4, + podiums: 0, + hg: 3, + polepositions: 0, + wc: 0, + dnfs: 15, + wins: 0 + }, + { + name: "Charles Leclerc", + races: 170, + points: 1660, + hf: 1, + podiums: 50, + hg: 1, + polepositions: 27, + wc: 0, + dnfs: 23, + wins: 8 + }, + { + name: "Lewis Hamilton", + races: 379, + points: 5014.5, + hf: 1, + podiums: 202, + hg: 1, + polepositions: 104, + wc: 7, + dnfs: 34, + wins: 105 + }, + { + name: "Alexander Albon", + races: 127, + points: 313, + hf: 3, + podiums: 2, + hg: 4, + polepositions: 0, + wc: 0, + dnfs: 22, + wins: 0 + }, + { + name: "Carlos Sainz", + races: 229, + points: 1336.5, + hf: 1, + podiums: 29, + hg: 1, + polepositions: 6, + wc: 0, + dnfs: 42, + wins: 4 + }, + { + name: "Liam Lawson", + races: 34, + points: 44, + hf: 5, + podiums: 0, + hg: 3, + polepositions: 0, + wc: 0, + dnfs: 6, + wins: 0 + }, + { + name: "Isack Hadjar", + races: 22, + points: 51, + hf: 3, + podiums: 1, + hg: 4, + polepositions: 0, + wc: 0, + dnfs: 2, + wins: 0 + }, + { + name: "Lance Stroll", + races: 189, + points: 324, + hf: 3, + podiums: 3, + hg: 1, + polepositions: 1, + wc: 0, + dnfs: 31, + wins: 0 + }, + { + name: "Fernando Alonso", + races: 426, + points: 2385, + hf: 1, + podiums: 106, + hg: 1, + polepositions: 22, + wc: 2, + dnfs: 83, + wins: 32 + }, + { + name: "Esteban Ocon", + races: 179, + points: 477, + hf: 1, + podiums: 4, + hg: 3, + polepositions: 0, + wc: 0, + dnfs: 25, + wins: 1 + }, + { + name: "Oliver Bearman", + races: 26, + points: 48, + hf: 4, + podiums: 0, + hg: 8, + polepositions: 0, + wc: 0, + dnfs: 3, + wins: 0 + }, + { + name: "Nico Hulkenberg", + races: 250, + points: 620, + hf: 3, + podiums: 1, + hg: 1, + polepositions: 1, + wc: 0, + dnfs: 44, + wins: 0 + }, + { + name: "Gabriel Bortoleto", + races: 23, + points: 19, + hf: 6, + podiums: 0, + hg: 7, + polepositions: 0, + wc: 0, + dnfs: 5, + wins: 0 + }, + { + name: "Pierre Gasly", + races: 176, + points: 458, + hf: 1, + podiums: 5, + hg: 2, + polepositions: 0, + wc: 0, + dnfs: 26, + wins: 1 + }, + { + name: "Franco Colapinto", + races: 26, + points: 5, + hf: 8, + podiums: 0, + hg: 8, + polepositions: 0, + wc: 0, + dnfs: 3, + wins: 0 + } +] + +export const team_career = [ + { + name: "Alpine", + races: 392, + points: 2000, + hf: 1, + podiums: 60, + hg: 1, + polepositions: 20, + wc: 2 + }, + { + name: "Aston Martin", + races: 152, + points: 863, + hf: 1, + podiums: 12, + hg: 1, + polepositions: 1, + wc: 0 + }, + { + name: "Ferrari", + races: 1123, + points: 10675, + hf: 1, + podiums: 639, + hg: 1, + polepositions: 254, + wc: 16 + }, + { + name: "Hass F1 Team", + races: 214, + points: 386, + hf: 4, + podiums: 0, + hg: 4, + polepositions: 1, + wc: 0 + }, + { + name: "Kick Sauber", + races: 615, + points: 1088, + hf: 1, + podiums: 27, + hg: 1, + polepositions: 1, + wc: 0 + }, + { + name: "McLaren", + races: 995, + points: 7783.5, + hf: 1, + podiums: 445, + hg: 1, + polepositions: 177, + wc: 10 + }, + { + name: "Mercedes", + races: 329, + points: 8159.5, + hf: 1, + podiums: 201, + hg: 1, + polepositions: 135, + wc: 8 + }, + { + name: "Racing Bulls", + races: 399, + points: 947, + hf: 1, + podiums: 6, + hg: 1, + polepositions: 1, + wc: 0 + }, + { + name: "Red Bull Racing", + races: 418, + points: 8288, + hf: 1, + podiums: 233, + hg: 1, + polepositions: 111, + wc: 6 + }, + { + name: "Williams", + races: 851, + points: 3768, + hf: 1, + podiums: 245, + hg: 1, + polepositions: 128, + wc: 9 + } +] + diff --git a/frontend/src/assets/style.css b/frontend/src/assets/style.css new file mode 100755 index 0000000..f621822 --- /dev/null +++ b/frontend/src/assets/style.css @@ -0,0 +1,4 @@ +body { + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/frontend/src/assets/teams/alp.avif b/frontend/src/assets/teams/alp.avif new file mode 100755 index 0000000..c25286c Binary files /dev/null and b/frontend/src/assets/teams/alp.avif differ diff --git a/frontend/src/assets/teams/alplogo.avif b/frontend/src/assets/teams/alplogo.avif new file mode 100755 index 0000000..0094906 Binary files /dev/null and b/frontend/src/assets/teams/alplogo.avif differ diff --git a/frontend/src/assets/teams/am.avif b/frontend/src/assets/teams/am.avif new file mode 100755 index 0000000..5a382cf Binary files /dev/null and b/frontend/src/assets/teams/am.avif differ diff --git a/frontend/src/assets/teams/amlogo.avif b/frontend/src/assets/teams/amlogo.avif new file mode 100755 index 0000000..d5b0c03 Binary files /dev/null and b/frontend/src/assets/teams/amlogo.avif differ diff --git a/frontend/src/assets/teams/fe.avif b/frontend/src/assets/teams/fe.avif new file mode 100755 index 0000000..c68d903 Binary files /dev/null and b/frontend/src/assets/teams/fe.avif differ diff --git a/frontend/src/assets/teams/felogo.avif b/frontend/src/assets/teams/felogo.avif new file mode 100755 index 0000000..9ea17fc Binary files /dev/null and b/frontend/src/assets/teams/felogo.avif differ diff --git a/frontend/src/assets/teams/hass.avif b/frontend/src/assets/teams/hass.avif new file mode 100755 index 0000000..2b18328 Binary files /dev/null and b/frontend/src/assets/teams/hass.avif differ diff --git a/frontend/src/assets/teams/hasslogo.avif b/frontend/src/assets/teams/hasslogo.avif new file mode 100755 index 0000000..a853666 Binary files /dev/null and b/frontend/src/assets/teams/hasslogo.avif differ diff --git a/frontend/src/assets/teams/kick.avif b/frontend/src/assets/teams/kick.avif new file mode 100755 index 0000000..fa36c21 Binary files /dev/null and b/frontend/src/assets/teams/kick.avif differ diff --git a/frontend/src/assets/teams/kicklogo.avif b/frontend/src/assets/teams/kicklogo.avif new file mode 100755 index 0000000..1a2e96f Binary files /dev/null and b/frontend/src/assets/teams/kicklogo.avif differ diff --git a/frontend/src/assets/teams/mclaren.avif b/frontend/src/assets/teams/mclaren.avif new file mode 100755 index 0000000..4ff1a9a Binary files /dev/null and b/frontend/src/assets/teams/mclaren.avif differ diff --git a/frontend/src/assets/teams/mclogo.avif b/frontend/src/assets/teams/mclogo.avif new file mode 100755 index 0000000..5b0ef39 Binary files /dev/null and b/frontend/src/assets/teams/mclogo.avif differ diff --git a/frontend/src/assets/teams/merc.avif b/frontend/src/assets/teams/merc.avif new file mode 100755 index 0000000..d4ede77 Binary files /dev/null and b/frontend/src/assets/teams/merc.avif differ diff --git a/frontend/src/assets/teams/merclogo.avif b/frontend/src/assets/teams/merclogo.avif new file mode 100755 index 0000000..50790b1 Binary files /dev/null and b/frontend/src/assets/teams/merclogo.avif differ diff --git a/frontend/src/assets/teams/rb.avif b/frontend/src/assets/teams/rb.avif new file mode 100755 index 0000000..ba1842e Binary files /dev/null and b/frontend/src/assets/teams/rb.avif differ diff --git a/frontend/src/assets/teams/rblogo.avif b/frontend/src/assets/teams/rblogo.avif new file mode 100755 index 0000000..f2376fc Binary files /dev/null and b/frontend/src/assets/teams/rblogo.avif differ diff --git a/frontend/src/assets/teams/srb.avif b/frontend/src/assets/teams/srb.avif new file mode 100755 index 0000000..ad3c592 Binary files /dev/null and b/frontend/src/assets/teams/srb.avif differ diff --git a/frontend/src/assets/teams/srblogo.avif b/frontend/src/assets/teams/srblogo.avif new file mode 100755 index 0000000..5d8f473 Binary files /dev/null and b/frontend/src/assets/teams/srblogo.avif differ diff --git a/frontend/src/assets/teams/will.avif b/frontend/src/assets/teams/will.avif new file mode 100755 index 0000000..6232f21 Binary files /dev/null and b/frontend/src/assets/teams/will.avif differ diff --git a/frontend/src/assets/teams/willogo.avif b/frontend/src/assets/teams/willogo.avif new file mode 100755 index 0000000..9bfb5c4 Binary files /dev/null and b/frontend/src/assets/teams/willogo.avif differ diff --git a/frontend/src/assets/ver.gif b/frontend/src/assets/ver.gif new file mode 100755 index 0000000..e9d5c1a Binary files /dev/null and b/frontend/src/assets/ver.gif differ diff --git a/frontend/src/components/.DS_Store b/frontend/src/components/.DS_Store new file mode 100755 index 0000000..7960ede Binary files /dev/null and b/frontend/src/components/.DS_Store differ diff --git a/frontend/src/components/DriverCard.vue b/frontend/src/components/DriverCard.vue new file mode 100755 index 0000000..c2bbe68 --- /dev/null +++ b/frontend/src/components/DriverCard.vue @@ -0,0 +1,71 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/DriverResult.vue b/frontend/src/components/DriverResult.vue new file mode 100755 index 0000000..759a1a1 --- /dev/null +++ b/frontend/src/components/DriverResult.vue @@ -0,0 +1,93 @@ + + + + diff --git a/frontend/src/components/QualifyingResult.vue b/frontend/src/components/QualifyingResult.vue new file mode 100755 index 0000000..e81937a --- /dev/null +++ b/frontend/src/components/QualifyingResult.vue @@ -0,0 +1,89 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/RaceResult.vue b/frontend/src/components/RaceResult.vue new file mode 100755 index 0000000..99054cc --- /dev/null +++ b/frontend/src/components/RaceResult.vue @@ -0,0 +1,85 @@ + + + diff --git a/frontend/src/components/TeamCard.vue b/frontend/src/components/TeamCard.vue new file mode 100755 index 0000000..09d075c --- /dev/null +++ b/frontend/src/components/TeamCard.vue @@ -0,0 +1,74 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/TeamResult.vue b/frontend/src/components/TeamResult.vue new file mode 100755 index 0000000..9c33e3a --- /dev/null +++ b/frontend/src/components/TeamResult.vue @@ -0,0 +1,89 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100755 index 0000000..39495f3 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,23 @@ +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import '@/assets/icon/font/iconfont.js' +import './assets/style.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' + +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' +import router from './router' + +const pinia = createPinia() + +const app = createApp(App) +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} +app.use(ElementPlus) +app.use(router) +app.use(pinia) + + +app.mount('#app') diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100755 index 0000000..0adfb30 --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,106 @@ +import { createRouter, createWebHistory } from 'vue-router' +import Home from '@/views/Home.vue' +import Seasons from '@/views/Seasons.vue' +import Teams from '@/views/Teams.vue' +import Drivers from '@/views/Drivers.vue' +import SeasonDetail from '@/views/SeasonDetail.vue' +import DriverDetail from '@/views/DriverDetail.vue' +import TeamDetail from '@/views/TeamDetail.vue' +import Result from '@/views/Result.vue' +import RacePage from '@/views/RacePage.vue' +import RaceResult from '@/views/RaceResult.vue' +import Login from '@/views/Login.vue' +import Register from '@/views/Register.vue' +import CommentDetail from '@/views/CommentDetail.vue' +import { isAuthenticated } from '@/utils/auth' + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + name: 'home', + component: Home, + }, + { + path: '/login', + name: 'login', + component: Login, + }, + { + path: '/register', + name: 'register', + component: Register, + }, + { + path: '/seasons', + name: 'seasons', + component: Seasons, + }, + { + path: '/seasons/:year', + name: 'season-detail', + component: SeasonDetail, + }, + { + path: '/seasons/:year/races/:id', + name: 'race-page', + component: RacePage, + }, + { + path: '/seasons/:year/races/:id/:type', + name: 'race-result', + component: RaceResult, + }, + { + path: '/teams', + name: 'teams', + component: Teams, + }, + { + path: '/teams/:id', + name: 'team-detail', + component: TeamDetail, + }, + { + path: '/drivers', + name: 'drivers', + component: Drivers, + }, + { + path: '/drivers/:id', + name: 'driver-detail', + component: DriverDetail, + }, + { + path: '/drivers/:id/results', + name: 'driver-result', + component: Result, + }, + { + path: '/teams/:id/results', + name: 'team-result', + component: Result, + } + , + { + path: '/comments/:id', + name: 'comment-detail', + component: CommentDetail, + } + ], +}) + +router.beforeEach((to, from, next) => { + if (to.name === 'login' || to.name === 'register') { + next() + return + } + if (!isAuthenticated()) { + next({ name: 'login' }) + } else { + next() + } +}) + +export default router diff --git a/frontend/src/store/SeasonDrivers.ts b/frontend/src/store/SeasonDrivers.ts new file mode 100755 index 0000000..0c0f424 --- /dev/null +++ b/frontend/src/store/SeasonDrivers.ts @@ -0,0 +1,46 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const useDriversStore = defineStore('drivers', () => { + const driversList = ref([]) + const hasFetched = ref(false) + const isLoading = ref(false) + + const fetchDrivers = async (forceRefresh = false) => { + if (hasFetched.value && !forceRefresh) { + return driversList.value + } + + isLoading.value = true + try { + const response = await fetch('/api/season-drivers?season=2025') + const data = await response.json() + + driversList.value = Array.isArray(data) ? data : (data?.data ?? []) + + hasFetched.value = true // 标记为已获取 + return driversList.value + } catch (error) { + console.error('获取drivers失败:', error) + throw error + } finally { + isLoading.value = false + } + } + + const ensureDriversLoaded = async () => { + if (!hasFetched.value) { + await fetchDrivers() + } + } + + fetchDrivers(true).catch(() => {}) + + return { + driversList, + isLoading, + hasFetched, + fetchDrivers, + ensureDriversLoaded + } +}) diff --git a/frontend/src/store/UserInfo.ts b/frontend/src/store/UserInfo.ts new file mode 100755 index 0000000..349fc56 --- /dev/null +++ b/frontend/src/store/UserInfo.ts @@ -0,0 +1,54 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const userInfoStore = defineStore('userInfo', () => { + const userInfo = ref({}) + const hasFetched = ref(false) + const isLoading = ref(false) + + const fetchUserInfo = async (forceRefresh = false) => { + if (hasFetched.value && !forceRefresh) { + return userInfo.value + } + isLoading.value = true + try { + const response = await fetch('/api/user') + const json = await response.json() + if (json?.data) { + userInfo.value = json.data + } else { + userInfo.value = json + } + hasFetched.value = true // 标记为已获取 + return userInfo.value + } catch (error) { + console.error('获取userInfo失败:', error) + throw error + } finally { + isLoading.value = false + } + } + + const ensureUserInfoLoaded = async () => { + if (!hasFetched.value) { + await fetchUserInfo() + } + } + + const clearUserInfo = () => { + userInfo.value = {} + hasFetched.value = false + isLoading.value = false + } + + fetchUserInfo(true).catch(() => {}) + + return { + userInfo, + isLoading, + hasFetched, + fetchUserInfo, + ensureUserInfoLoaded, + clearUserInfo + } +}) diff --git a/frontend/src/utils/auth.ts b/frontend/src/utils/auth.ts new file mode 100755 index 0000000..75e3778 --- /dev/null +++ b/frontend/src/utils/auth.ts @@ -0,0 +1,11 @@ +export const TOKEN_KEY = 'auth_token' +export const SESSION_KEY = 'auth_session' + +export const getToken = () => localStorage.getItem(TOKEN_KEY) || '' +export const setToken = (token: string) => localStorage.setItem(TOKEN_KEY, token) +export const clearToken = () => localStorage.removeItem(TOKEN_KEY) + +export const setSessionAuth = (v: boolean) => localStorage.setItem(SESSION_KEY, v ? '1' : '') +export const hasSessionAuth = () => localStorage.getItem(SESSION_KEY) === '1' + +export const isAuthenticated = () => !!getToken() || hasSessionAuth() diff --git a/frontend/src/views/.DS_Store b/frontend/src/views/.DS_Store new file mode 100755 index 0000000..6d6bfb8 Binary files /dev/null and b/frontend/src/views/.DS_Store differ diff --git a/frontend/src/views/CommentDetail.vue b/frontend/src/views/CommentDetail.vue new file mode 100755 index 0000000..3da07e1 --- /dev/null +++ b/frontend/src/views/CommentDetail.vue @@ -0,0 +1,249 @@ + + + + + diff --git a/frontend/src/views/DriverDetail.vue b/frontend/src/views/DriverDetail.vue new file mode 100755 index 0000000..3d72a9b --- /dev/null +++ b/frontend/src/views/DriverDetail.vue @@ -0,0 +1,341 @@ + + + + + diff --git a/frontend/src/views/Drivers.vue b/frontend/src/views/Drivers.vue new file mode 100755 index 0000000..1bb5d2c --- /dev/null +++ b/frontend/src/views/Drivers.vue @@ -0,0 +1,78 @@ + + + + diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue new file mode 100755 index 0000000..14dec7d --- /dev/null +++ b/frontend/src/views/Home.vue @@ -0,0 +1,348 @@ + + + + diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100755 index 0000000..dc5ccd6 --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/frontend/src/views/RacePage.vue b/frontend/src/views/RacePage.vue new file mode 100755 index 0000000..36bc354 --- /dev/null +++ b/frontend/src/views/RacePage.vue @@ -0,0 +1,237 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/RaceResult.vue b/frontend/src/views/RaceResult.vue new file mode 100755 index 0000000..1c6c063 --- /dev/null +++ b/frontend/src/views/RaceResult.vue @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/Register.vue b/frontend/src/views/Register.vue new file mode 100755 index 0000000..7b6cdf6 --- /dev/null +++ b/frontend/src/views/Register.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/frontend/src/views/Result.vue b/frontend/src/views/Result.vue new file mode 100755 index 0000000..b80169d --- /dev/null +++ b/frontend/src/views/Result.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/frontend/src/views/SeasonDetail.vue b/frontend/src/views/SeasonDetail.vue new file mode 100755 index 0000000..b57c188 --- /dev/null +++ b/frontend/src/views/SeasonDetail.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/frontend/src/views/Seasons.vue b/frontend/src/views/Seasons.vue new file mode 100755 index 0000000..1c263e1 --- /dev/null +++ b/frontend/src/views/Seasons.vue @@ -0,0 +1,89 @@ + + + + diff --git a/frontend/src/views/TeamDetail.vue b/frontend/src/views/TeamDetail.vue new file mode 100755 index 0000000..ebd9767 --- /dev/null +++ b/frontend/src/views/TeamDetail.vue @@ -0,0 +1,410 @@ + + + + + diff --git a/frontend/src/views/Teams.vue b/frontend/src/views/Teams.vue new file mode 100755 index 0000000..5fecbdc --- /dev/null +++ b/frontend/src/views/Teams.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100755 index 0000000..d23833f --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,24 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import vueDevTools from 'vite-plugin-vue-devtools' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + vueDevTools(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + }, + }, + server: { + proxy: { + '/api': 'http://localhost:8080', + // '/api': 'http://10.128.50.6:8080', + }, + }, +})