来点前后端 ok,最近要搞前端项目练手,来点spring boot
1.0.后端:Spring Boot?这是什么? 找到springboot的教程 ,开始部署开发环境。
不过我觉得直接从IDEA开局貌似更好?
直接在github上找到Spring的开源项目[点我去看看 ] 简单瞅一眼,发现有这个框架在IDEA中部署开发的教程,让我看看 。
Steps: Within your locally cloned spring-framework working directory:
Precompile spring-oxm with ./gradlew :spring-oxm:compileTestJava Import into IntelliJ (File -> New -> Project from Existing Sources -> Navigate to directory -> Select build.gradle) When prompted exclude the spring-aspects module (or after the import via File-> Project Structure -> Modules)
OK,gradle的部署方式,那在IDEA中从已有的远程项目中创建。
欸?好像启动不了一个可运行的示例,我再找找…原来是可以使用Spring Initializr 直接创建一个项目啊 ,看来之前安装的Spring-framework项目如果要启动要使用集成测试,属于是新手误入Boss房了。
好,Spring Initializr 生成出来的项目小很多,使用IDEA启动并完成构建之后,我们就有了一个简易的Spring Boot项目了。
1.1.将SpringBoot连接到我的Mysql Step1.数据库准备 Mysql部分:使用Navicat轻松创建表格定义数据类型,这里我创建了一个test数据库后在库中定义一个user表方便我们后续的类定义以及其他的功能。
**user表格(用户)**:
1 2 3 4 5 6 7 8 9 10 11 12 CREATE TABLE user ( userid INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR (50 ) NOT NULL UNIQUE , password VARCHAR (255 ) NOT NULL , gender ENUM('male' , 'female' , 'other' ) NOT NULL , birthdate DATE , address VARCHAR (255 ), peopleid VARCHAR (20 ) NOT NULL , peoplename VARCHAR (20 ) NOT NULL , created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP , updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );
**medical_records表格(病历)**: 这个表一个外键,使其与users表中的userid相关联
1 2 3 4 5 6 7 8 9 10 CREATE TABLE medical_records ( record_id INT AUTO_INCREMENT PRIMARY KEY, userid INT NOT NULL , diagnosis_date DATE NOT NULL , doctor VARCHAR (255 ) NOT NULL , diagnosis TEXT NOT NULL , remark TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP , FOREIGN KEY (userid) REFERENCES user (userid) ON DELETE CASCADE );
在IDEA中,点击窗口右侧的数据库图标,在里面添加我们的数据库,笔者这里用的Mysql,其他厂商的数据库你在下拉列表里找到就行了。 点击后填写我们的数据库信息,数据库方面不是本文重点,这里不过多笔墨了。
Step2.配置项目依赖并重新构建 SpringBoot部分:我使用的是gradle构建,因此我在build.gradle中配置必要的依赖项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 plugins { id 'java' id 'org.springframework.boot' version '3.4.3' id 'io.spring.dependency-management' version '1.1.7' } group = 'com.example' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(23 ) } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-actuator' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' runtimeOnly 'mysql:mysql-connector-java:8.0.33' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation('org.springframework.boot:spring-boot-starter-test' ) { exclude group : 'org.junit.vintage' , module: 'junit-vintage-engine' } testImplementation 'org.assertj:assertj-core:3.24.2' } tasks.named('test' ) { useJUnitPlatform() }
如果飘红,就按照IDEA的引导引入对应的依赖库就行了。
mysql-connector的版本和Mysql版本有关,我的Mysql版本是8.0,所以这里使用8.0的Connector,具体的Connector版本和Mysql版本的对照可以到Mysql官网查看
添加完成依赖之后,运行build.gradle完成构建
Step3.配置数据库连接 在application.properties中添加数据库连接配置 ,当然,如果我们的spring版本比较旧,可能是application.yml之类的文件,这里我贴出我的application.properties,yml格式的文件不支持 直接粘贴进去。
1 2 3 4 5 6 7 spring.datasource.url =jdbc:mysql://localhost:3306/your_database_name?useSSL=false&serverTimezone=UTC spring.datasource.username =your_username spring.datasource.password =your_password spring.jpa.hibernate.ddl-auto =update
Step4.创建用户实体类 根据我们刚刚新建的User表创建JPA实体类
然后,进入User类,IDEA生成的JPA代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 package com.example.demo.entity;import jakarta.persistence.*;import org.hibernate.annotations.ColumnDefault;import java.time.Instant;import java.time.LocalDate;@Entity @Table(name = "user") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "userid", nullable = false) private Integer id; @Column(name = "username", nullable = false, length = 50) private String username; @Column(name = "password", nullable = false) private String password; @Lob @Column(name = "gender", nullable = false) private String gender; @Column(name = "birthdate") private LocalDate birthdate; @Column(name = "address") private String address; @Column(name = "pepoleid", nullable = false, length = 20) private String pepoleid; @Column(name = "pepolename", nullable = false, length = 20) private String pepolename; @ColumnDefault("CURRENT_TIMESTAMP") @Column(name = "created_at") private Instant createdAt; @ColumnDefault("CURRENT_TIMESTAMP") @Column(name = "updated_at") private Instant updatedAt; public Integer getId () { return id; } public void setId (Integer 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 getGender () { return gender; } public void setGender (String gender) { this .gender = gender; } public LocalDate getBirthdate () { return birthdate; } public void setBirthdate (LocalDate birthdate) { this .birthdate = birthdate; } public String getAddress () { return address; } public void setAddress (String address) { this .address = address; } public String getPepoleid () { return pepoleid; } public void setPepoleid (String pepoleid) { this .pepoleid = (String) pepoleid; } public String getPepolename () { return pepolename; } public void setPepolename (String pepolename) { this .pepolename = pepolename; } public Instant getCreatedAt () { return createdAt; } public void setCreatedAt (Instant createdAt) { this .createdAt = createdAt; } public Instant getUpdatedAt () { return updatedAt; } public void setUpdatedAt (Instant updatedAt) { this .updatedAt = updatedAt; } }
然后你会看到@Table(name = "user")这一行中有警告,根据IDEA的提示给他重新分配数据源到我们刚刚创建的user数据库中
完成之后你Ctrl+鼠标左键点击这个"user"应该是能触发你IDEA弹出数据库的弹窗的,这样就对了。
另外,你会发现在User类在com.example.demo中,为了方便管理,我们在com.example.demo下创建一个entity包,并将User类移动到这个包中。
做完这些之后的项目结构:
对病历表实体进行同样的操作,将MedicalRecord类移动到com.example.demo.entity包中。MedicalRecord:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 package com.example.demo.entity;import jakarta.persistence.*;import org.hibernate.annotations.ColumnDefault;import org.hibernate.annotations.OnDelete;import org.hibernate.annotations.OnDeleteAction;import java.time.Instant;import java.time.LocalDate;@Entity @Table(name = "medical_records") public class MedicalRecord { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "record_id", nullable = false) private Integer id; @ManyToOne(fetch = FetchType.LAZY, optional = false) @OnDelete(action = OnDeleteAction.CASCADE) @JoinColumn(name = "userid", nullable = false) private User userid; @Column(name = "diagnosis_date", nullable = false) private LocalDate diagnosisDate; @Column(name = "doctor", nullable = false) private String doctor; @Lob @Column(name = "diagnosis", nullable = false) private String diagnosis; @Lob @Column(name = "remark") private String remark; @ColumnDefault("CURRENT_TIMESTAMP") @Column(name = "created_at") private Instant createdAt; public Integer getId () { return id; } public void setId (Integer id) { this .id = id; } public User getUserid () { return userid; } public void setUserid (User userid) { this .userid = userid; } public LocalDate getDiagnosisDate () { return diagnosisDate; } public void setDiagnosisDate (LocalDate diagnosisDate) { this .diagnosisDate = diagnosisDate; } public String getDoctor () { return doctor; } public void setDoctor (String doctor) { this .doctor = doctor; } public String getDiagnosis () { return diagnosis; } public void setDiagnosis (String diagnosis) { this .diagnosis = diagnosis; } public String getRemark () { return remark; } public void setRemark (String remark) { this .remark = remark; } public Instant getCreatedAt () { return createdAt; } public void setCreatedAt (Instant createdAt) { this .createdAt = createdAt; } }
Step5.编写数据访问层Repository 在项目中src/main/java/com.example.demo/下创建一个repository包,并在其中创建UserRepository和MedicalRecordRepository接口 ,代码如下:
UserRepository:
1 2 3 4 5 6 7 8 9 10 package com.example.demo.repository;import com.example.demo.entity.User;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.stereotype.Repository;@Repository public interface UserRepository extends JpaRepository <User, Integer> { User findByUsername (String username) ; }
MedicalRecordRepository:
1 2 3 4 5 6 7 8 9 10 11 12 package com.example.demo.repository;import com.example.demo.entity.MedicalRecord;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.stereotype.Repository;import java.util.List;@Repository public interface MedicalRecordRepository extends JpaRepository <MedicalRecord, Integer> { List<MedicalRecord> findByUserid (Integer userid) ; }
注意UserRepository和MedicalRecordRepository是Interface而不是Class:
Step7.编写测试方法,检测连通性 转到src/test/java/com.example.demo/,注意是test 中,创建一个repository包,添加UserRepositoryTest类用来测试,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 package com.example.demo.repository;import com.example.demo.entity.User;import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;import org.springframework.test.annotation.Rollback;import java.time.LocalDate;import static org.assertj.core.api.Assertions.assertThat;@DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) public class UserRepositoryTest { @Autowired private UserRepository userRepository; @Autowired private TestEntityManager entityManager; private User testUser; @BeforeEach void setUp () { System.out.println("Setting up test environment..." ); testUser = new User (); testUser.setUsername("zzb" ); testUser.setPassword("password" ); testUser.setGender("male" ); testUser.setBirthdate(LocalDate.of(1998 , 3 , 3 )); testUser.setAddress("Example Address" ); testUser.setPepolename("zzb" ); testUser.setPepoleid("123456789123456789" ); entityManager.persist(testUser); System.out.println("Inserted user ID: " + testUser.getId()); } @Test @Rollback(false) public void whenFindByUsername_thenReturnUser () { System.out.println("Running whenFindByUsername_thenReturnUser test..." ); User foundUser = userRepository.findByUsername(testUser.getUsername()); assertThat(foundUser).isNotNull().usingRecursiveComparison().isEqualTo(testUser); if (foundUser == null ) { System.out.println("User not found by username." ); } else { System.out.println("Found user with ID: " + foundUser.getId()); } } @Test public void testSaveUser () { User newUser = new User (); newUser.setUsername("zyy" ); newUser.setPassword("newpassword" ); newUser.setGender("female" ); newUser.setBirthdate(LocalDate.of(1993 , 3 , 3 )); newUser.setAddress("New Example Address" ); newUser.setPepolename("zyy" ); newUser.setPepoleid("12345678912345678X" ); User savedUser = userRepository.save(newUser); assertThat(savedUser.getId()).isNotNull(); User foundUser = userRepository.findByUsername("zyy" ); assertThat(foundUser).isNotNull().usingRecursiveComparison().isEqualTo(savedUser); if (savedUser == null || foundUser == null ) { System.out.println("Failed to save or find the new user." ); } else { System.out.println("Saved and found user with ID: " + savedUser.getId()); } } }
做完这些之后我们的项目结构 你可能会发现AutoConfigureTestDatabase和localDate这两个类没有导入,导入一下就完事了。
搞定,重新构建一下,没有报错就可以跑一下测试了
测试完成,没有报错,刷新表之后在Navicat中看到我们测试用例创建的新用户 搞定~
Step8:给前端提供功能 对于用户相关的操作,我们提供注册、登录等功能。 其中,能操作所有数据的管理员功能我们写到controller内 而普通用户需要用到的操作,我们写到service内
首先我们创建一个service包,然后创建UserService和MedicalRecordService接口
UserService:
1 2 3 4 5 6 7 8 9 10 package com.example.demo.service;import com.example.demo.entity.User;public interface UserService { User validateUser (String username, String password) ; User registerUser (User user) throws IllegalArgumentException; User updateUserInfo (Integer userId, User userDetails) throws IllegalArgumentException; void updatePassword (Integer userId, String oldPassword, String newPassword) throws IllegalArgumentException; }
MedicalRecordService:
1 2 3 4 5 6 7 8 9 10 package com.example.demo.service;import com.example.demo.entity.MedicalRecord;import java.util.List;public interface MedicalRecordService { List<MedicalRecord> getMedicalRecordsByUserId (Integer userId) ; }
然后,我们用再在service内创建实现其功能的包impl,在service.impl内创建实现这个接口的类UserServiceImpl和MedicalRecordServiceImpl
UserServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 package com.example.demo.service.impl;import com.example.demo.entity.User;import com.example.demo.repository.UserRepository;import com.example.demo.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.util.Optional;@Service public class UserServiceImpl implements UserService { @Autowired private UserRepository userRepository; @Override public User validateUser (String username, String password) { User user = userRepository.findByUsername(username); if (user != null && user.getPassword().equals(password)) { return user; } return null ; } @Override public User registerUser (User user) throws IllegalArgumentException { if (userRepository.findByUsername(user.getUsername()) != null ) { throw new IllegalArgumentException ("用户名已存在" ); } if (user.getPassword().length() < 6 || user.getPassword().length() > 255 ) { throw new IllegalArgumentException ("密码长度必须在 6 到 255 个字符之间" ); } return userRepository.save(user); } @Override public User updateUserInfo (Integer userId, User userDetails) throws IllegalArgumentException { Optional<User> optionalUser = userRepository.findById(userId); if (optionalUser.isPresent()) { User user = optionalUser.get(); user.setUsername(userDetails.getUsername()); user.setGender(userDetails.getGender()); user.setBirthdate(userDetails.getBirthdate()); user.setAddress(userDetails.getAddress()); user.setPepoleid(userDetails.getPepoleid()); user.setPepolename(userDetails.getPepolename()); return userRepository.save(user); } else { throw new IllegalArgumentException ("用户不存在" ); } } @Override public void updatePassword (Integer userId, String oldPassword, String newPassword) throws IllegalArgumentException { Optional<User> optionalUser = userRepository.findById(userId); if (optionalUser.isPresent()) { User user = optionalUser.get(); if (!user.getPassword().equals(oldPassword)) { throw new IllegalArgumentException ("旧密码错误" ); } user.setPassword(newPassword); userRepository.save(user); } else { throw new IllegalArgumentException ("用户不存在" ); } } }
MedicalRecordServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.example.demo.service.impl;import com.example.demo.entity.MedicalRecord;import com.example.demo.repository.MedicalRecordRepository;import com.example.demo.service.MedicalRecordService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.util.List;@Service public class MedicalRecordServiceImpl implements MedicalRecordService { @Autowired private MedicalRecordRepository medicalRecordRepository; @Override public List<MedicalRecord> getMedicalRecordsByUserId (Integer userId) { return medicalRecordRepository.findByUserid(userId); } }
对于查询所有账户的信息,对账户信息的增删改查等管理员权限的功能,我们写在controller包的UserController类中,当然,为了调用方便,我们也在这个类中应用UserService的方法。
UserController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 package com.example.demo.controller;import com.example.demo.entity.User;import com.example.demo.repository.UserRepository;import com.example.demo.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Optional;@RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @PostMapping("/login") public ResponseEntity<?> login(@RequestBody User loginRequest) { User user = userService.validateUser(loginRequest.getUsername(), loginRequest.getPassword()); if (user != null ) { return ResponseEntity.ok(user); } else { return ResponseEntity.badRequest().body("用户名或密码错误" ); } } @Autowired private UserRepository userRepository; @GetMapping public List<User> getAllUsers () { return userRepository.findAll(); } @GetMapping("/{id}") public ResponseEntity<User> getUserById (@PathVariable(value = "id") Integer userId) { Optional<User> user = userRepository.findById(userId); if (user.isPresent()) { return ResponseEntity.ok().body(user.get()); } else { return ResponseEntity.notFound().build(); } } @PostMapping public User createUser (@RequestBody User newUser) { return userRepository.save(newUser); } @PutMapping("/{id}") public ResponseEntity<User> updateUser (@PathVariable(value = "id") Integer userId, @RequestBody User userDetails) { Optional<User> optionalUser = userRepository.findById(userId); if (optionalUser.isPresent()) { User user = optionalUser.get(); user.setUsername(userDetails.getUsername()); user.setPassword(userDetails.getPassword()); user.setGender(userDetails.getGender()); user.setBirthdate(userDetails.getBirthdate()); user.setAddress(userDetails.getAddress()); user.setPepoleid(userDetails.getPepoleid()); user.setPepolename(userDetails.getPepolename()); final User updatedUser = userRepository.save(user); return ResponseEntity.ok(updatedUser); } else { return ResponseEntity.notFound().build(); } } @DeleteMapping("/{id}") public ResponseEntity<Void> deleteUser (@PathVariable(value = "id") Integer userId) { Optional<User> user = userRepository.findById(userId); if (user.isPresent()) { userRepository.delete(user.get()); return ResponseEntity.ok().build(); } else { return ResponseEntity.notFound().build(); } } }
注意添加完成后重新构建一下。
2.0.前端:VUE?这是什么? 好吧,找一下发现VUE 是JavaScript的框架,那还是要Node.js 的运行环境,装就完事了。node.js的安装还是很简单的,笔者是Windows11环境,使用pnpm安装就完事了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 winget install Schniz.fnm fnm install 22 node -v corepack enable pnpm pnpm -v
ok,安装完成,node -v能出现版本号了。
Vite:强大的Vue构建工具 OK,导师说装个Vite 更方便,问一下AI这玩意干啥的。
Generated from Qwen: Vite 是一个现代化的前端构建工具,旨在显著提升前端开发体验。它主要由两大部分组成:一个是开发服务器,另一个是用于生产环境的构建指令。
装就完事了
1 npm install -g create-vite
OK装好之后就可以用create-vite来方便地创建一个Demo了。
创建并进入一个我们打算存放Demo的地方,然后运行
1 2 3 4 5 6 7 8 9 10 create-vite vue_demo --template vue Scaffolding project in vue_demo... Done. Now run: cd vue_demo npm install npm run dev
OK,接下来按照指引运行一下这三个命令,就能运行我们的Demo了。
安装和配置必要的组件库 当然,此时的Demo还是空的,我们还需要安装一些必要的组件库,比如axios、vue-router、Element Plus等,才能满足我们对页面的各种各样的需求。
axios 安装 Axios: Axios用于发送Http请求给后端API。
配置 Axios: 我们还需要创建一个文件来封装它,以便于管理和维护。
创建一个新的文件 src/api/index.js:
1 2 3 4 5 6 7 8 9 import axios from 'axios' ;const instance = axios.create ({ baseURL : 'http://localhost:8080/api/' , timeout : 5000 , headers : {'Content-Type' : 'application/json' } }); export default instance;
创建 axios 实例
在src目录下创建一个utils文件夹,并在其中创建一个http.js文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import axios from 'axios' ;const http = axios.create ({ baseURL : 'https://api.example.com' , timeout : 5000 , }); http.interceptors .request .use ( (config ) => { return config; }, (error ) => { return Promise .reject (error); } ); http.interceptors .response .use ( (response ) => { return response.data ; }, (error ) => { return Promise .reject (error); } ); export default http;
在需要调用 API 的地方引入并使用这个实例:
1 import http from '@/utils/http' ;
vue-router 安装 Vue Router: Vue Router用于管理页面路由,也就是我们在网页中经常用到的页面跳转功能。
配置 Vue Router: 接下来,我们为 Vue 应用程序设置路由管理。
创建路由配置文件 src/router/index.js:
这里病历页面使用/medical-records/:userId的方式进行访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import { createRouter, createWebHistory } from 'vue-router' ;const routes = [ { path : '/' , name : 'Home' , component : () => import ('@/views/Home.vue' ), }, { path : '/login' , name : 'Login' , component : () => import ('@/views/Login.vue' ), }, { path : '/register' , name : 'Register' , component : () => import ('@/views/Register.vue' ), }, { path : '/medical-records/:userId' , name : 'MedicalRecords' , component : () => import ('@/views/MedicalRecords.vue' ), } ]; const router = createRouter ({ history : createWebHistory (), routes, }); export default router;
在 main.js 或 main.ts 中引入并使用路由:
1 2 3 4 5 6 7 8 9 10 import { createApp } from 'vue' ;import App from './App.vue' ;import router from './router' ;import ElementPlus from 'element-plus' ;import 'element-plus/dist/index.css' ;const app = createApp (App );app.use (router); app.use (ElementPlus ); app.mount ('#app' );
Element Plus 安装 Element Plus: 最后安装Element Plus用于快速开发UI界面。 顺便再装一个自动引入的插件,这样我们就可以直接使用Element Plus的组件而无需手动导入每个组件。
1 2 npm install element-plus npm install unplugin-auto-import unplugin-vue-components -D
配置 Element Plus: 对于 Element Plus,我们可以选择完整引入或按需引入。这里推荐使用按需引入以减少打包体积。
修改 vite.config.js 文件:
1 2 3 4 5 6 7 8 9 10 11 12 import { defineConfig } from 'vite' ;import vue from '@vitejs/plugin-vue' ;import path from 'path' ; export default defineConfig ({ plugins : [vue ()], resolve : { alias : { '@' : path.resolve (__dirname, './src' ), }, }, });
这样,你就可以在我们的组件中直接使用 Element Plus 的组件而无需手动导入每个组件。
通过以上步骤,你应该能够在我们的 Vite + Vue 项目中成功配置和使用 axios、vue-router 和 Element Plus。记得根据实际需求调整配置和代码逻辑。
页面编写 好了,接下来就可以开始写登录界面的代码了。 我们在之前配置 Vue Router的时候已经指定了Home、Login和Register三个文件的路径,他们是在views目录下的,所以我们要先在根目录src下创建views文件夹,然后在里面创建Home.vue、Login.vue和Register.vue,分别对应首页、登录页和注册页,MedicalRecords.vue则是病历的动态页面。
之后,我们的项目目录应该看起来像这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 src/ ├── assets/ ├── components/ ├── router/ │ └── index.js ├── utils/ │ └── http.js ├── views/ │ ├── Home.vue │ ├── Login.vue │ ├── MedicalRecords.vue │ └── Register.vue ├── App.vue └── main.js
Home.vue: 随便写点东西在Home(其实一开始是想在这个界面显示数据库中最近创建的16个用户,不过写完是一坨bug,所以就删掉了,不完整但是无伤大雅)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <template > <el-container > <el-main > <h1 > Welcome to Home Page</h1 > <el-button type ="primary" @click ="fetchData" > Fetch Data</el-button > <el-table :data ="tableData" v-loading ="loading" style ="margin-top: 20px" > <el-table-column prop ="id" label ="ID" width ="100" /> <el-table-column prop ="title" label ="Title" /> </el-table > </el-main > </el-container > </template > <script > import http from '@/utils/http' ;export default { name : 'Home' , data ( ) { return { tableData : [], loading : false , }; }, methods : { async fetchData ( ) { this .loading = true ; try { const response = await http.get ('/posts' ); this .tableData = response; } catch (error) { this .$message .error ('Failed to fetch data' ); console .error (error); } finally { this .loading = false ; } }, }, }; </script > <style scoped > h1 { color : #409EFF ; } .el-button { margin-bottom : 20px ; } </style >
Login.vue: 登录成功之后使用router将UserId传给之后的页面,用于传递用户信息,当然使用会话也很好,有验证机制,这里先用URL保存用户ID
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 <template > <el-container class ="login-container" > <el-main > <el-card class ="login-card" > <h2 > 用户登录</h2 > <el-form :model ="loginForm" :rules ="loginRules" ref ="loginFormRef" label-width ="80px" > <el-form-item label ="用户名" prop ="username" > <el-input v-model ="loginForm.username" placeholder ="请输入用户名" /> </el-form-item > <el-form-item label ="密码" prop ="password" > <el-input v-model ="loginForm.password" type ="password" placeholder ="请输入密码" show-password /> </el-form-item > <el-form-item > <el-button type ="primary" @click ="handleLogin" > 登录</el-button > <el-button @click ="resetForm" > 重置</el-button > </el-form-item > </el-form > </el-card > </el-main > </el-container > </template > <script > import http from '@/utils/http' ; import { ElMessage } from 'element-plus' ; export default { name : 'Login' , data ( ) { return { loginForm : { username : '' , password : '' , }, loginRules : { username : [ { required : true , message : '请输入用户名' , trigger : 'blur' }, { min : 3 , max : 50 , message : '用户名长度在 3 到 50 个字符' , trigger : 'blur' }, ], password : [ { required : true , message : '请输入密码' , trigger : 'blur' }, { min : 6 , max : 255 , message : '密码长度在 6 到 255 个字符' , trigger : 'blur' }, ], }, }; }, methods : { async handleLogin ( ) { try { await this .$refs .loginFormRef .validate (); const response = await http.post ('/login' , this .loginForm ); if (response.code === 200 ) { ElMessage .success ('登录成功' ); const userId = response.data .userId ; localStorage .setItem ('user' , JSON .stringify (response.data )); this .$router .push ({ name : 'MedicalRecords' , params : { userId } }); } else { ElMessage .error (response.message || '登录失败' ); } } catch (error) { ElMessage .error ('登录失败,请检查用户名和密码' ); console .error (error); } }, resetForm ( ) { this .$refs .loginFormRef .resetFields (); }, }, }; </script > <style scoped > .login-container { display : flex; justify-content : center; align-items : center; height : 100vh ; background-color : #f5f5f5 ; } .login-card { width : 400px ; padding : 20px ; } h2 { text-align : center; margin-bottom : 20px ; color : #409EFF ; } </style >
效果是这样
Register.vue: 给每个输入都做了判定和单独的提示,如果输入不合法,则不会发送请求,并且会有提示。
其中,比较有创新的地方是
按照出生日期在前端实时刷新年龄,当前时间减去出生日期,并将计算结果显示在前端。
限制了出生日期只能选择在今天以前的日期。
对身份证号有特殊检查,只能是18位,并且前17位必须是数字,最后一位可以是数字或者X。
其他数据的检查稀松平常,看代码吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 <template > <el-container class ="register-container" > <el-main > <el-card class ="register-card" > <h2 > 用户注册</h2 > <el-form :model ="registerForm" :rules ="registerRules" ref ="registerFormRef" label-width ="100px" > <el-form-item label ="用户名" prop ="username" > <el-input v-model ="registerForm.username" placeholder ="请输入用户名" /> </el-form-item > <el-form-item label ="密码" prop ="password" > <el-input v-model ="registerForm.password" type ="password" placeholder ="请输入密码" show-password /> </el-form-item > <el-form-item label ="确认密码" prop ="confirmPassword" > <el-input v-model ="registerForm.confirmPassword" type ="password" placeholder ="请再次输入密码" show-password /> </el-form-item > <el-form-item label ="性别" prop ="gender" > <el-radio-group v-model ="registerForm.gender" > <el-radio label ="male" > 男</el-radio > <el-radio label ="female" > 女</el-radio > <el-radio label ="other" > 其他</el-radio > </el-radio-group > </el-form-item > <el-row > <el-col :span ="12" > <el-form-item label ="出生日期" prop ="birthdate" > <el-date-picker v-model ="registerForm.birthdate" type ="date" placeholder ="选择日期" @change ="calculateAge" :disabled-date ="disabledDate" /> </el-form-item > </el-col > <el-col :span ="12" > <el-form-item label ="年龄" > <el-input :value ="age" disabled > </el-input > </el-form-item > </el-col > </el-row > <el-form-item label ="地址" prop ="address" > <el-input v-model ="registerForm.address" placeholder ="请输入地址" /> </el-form-item > <el-form-item label ="姓名" prop ="realName" > <el-input v-model ="registerForm.realName" placeholder ="请输入您的真实姓名" /> </el-form-item > <el-form-item label ="身份证号" prop ="idNumber" > <el-input v-model ="registerForm.idNumber" placeholder ="请输入您的身份证号码" /> </el-form-item > <el-form-item > <el-button type ="primary" @click ="handleRegister" > 注册</el-button > <el-button @click ="resetForm" > 重置</el-button > </el-form-item > </el-form > </el-card > </el-main > </el-container > </template > <script > import http from '@/utils/http' ; import { ElMessage } from 'element-plus' ; export default { name : 'Register' , data ( ) { const validateConfirmPassword = (rule, value, callback ) => { if (value !== this .registerForm .password ) { callback (new Error ('两次输入的密码不一致' )); } else { callback (); } }; return { registerForm : { username : '' , password : '' , confirmPassword : '' , gender : 'male' , birthdate : null , address : '' }, age : null , registerRules : { username : [ { required : true , message : '请输入用户名' , trigger : 'blur' }, { min : 3 , max : 50 , message : '用户名长度在 3 到 50 个字符' , trigger : 'blur' }, ], password : [ { required : true , message : '请输入密码' , trigger : 'blur' }, { min : 6 , max : 255 , message : '密码长度在 6 到 255 个字符' , trigger : 'blur' }, ], confirmPassword : [ { required : true , message : '请再次输入密码' , trigger : 'blur' }, { validator : validateConfirmPassword, trigger : 'blur' }, ], gender : [ { required : true , message : '请选择性别' , trigger : 'change' }, ], birthdate : [ { required : true , message : '请选择出生日期' , trigger : 'change' }, ], address : [ { required : true , message : '请输入地址' , trigger : 'blur' }, ], realName : [ { required : true , message : '请输入您的真实姓名' , trigger : 'blur' }, ], idNumber : [ { required : true , message : '请输入您的身份证号码' , trigger : 'blur' }, { min : 18 , max : 18 , message : '身份证号码必须是18位' , trigger : 'blur' }, { pattern : /^[0-9]{17}[0-9X]$/ , message : '身份证号码格式不正确,只能包含数字和最后一位可能是大写字母X' , trigger : 'blur' } ], }, }; }, methods : { disabledDate (time ) { return time.getTime () > Date .now (); }, calculateAge ( ) { if (this .registerForm .birthdate ) { const today = new Date (); const birthDate = new Date (this .registerForm .birthdate ); let years = today.getFullYear () - birthDate.getFullYear (); const monthDiff = today.getMonth () - birthDate.getMonth (); if (monthDiff < 0 || (monthDiff === 0 && today.getDate () < birthDate.getDate ())) { years--; } this .age = years; } else { this .age = null ; } }, async handleRegister ( ) { try { await this .$refs .registerFormRef .validate (); const response = await http.post ('/api/users/register' , this .registerForm ); if (response.code === 200 ) { ElMessage .success ('注册成功' ); this .$router .push ('/login' ); } else { ElMessage .error (response.message || '注册失败' ); } } catch (error) { ElMessage .error (error.response ?.data ?.message || '注册失败,请检查输入' ); console .error (error); } }, resetForm ( ) { this .$refs .registerFormRef .resetFields (); this .age = null ; }, }, }; </script > <style scoped > .register-container { display : flex; justify-content : center; align-items : center; height : 100vh ; background-color : #f5f5f5 ; } .register-card { width : 500px ; padding : 20px ; } h2 { text-align : center; margin-bottom : 20px ; color : #409EFF ; } </style >
效果截图贴这,我主要技术栈是单片机方向的,所以页面主打一个能用就行,不太美观。
在酒吧点个炒饭看看 好,全防住了
MedicalRecords.vue 通过URL传递的用户ID,对后端进行访问,查询到用户的病历数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 <template > <div class ="medical-records" > <el-table :data ="medicalRecords" style ="width: 100%" > <el-table-column prop ="diagnosisDate" label ="诊断日期" width ="180" > </el-table-column > <el-table-column prop ="doctor" label ="医生" width ="180" > </el-table-column > <el-table-column prop ="diagnosis" label ="诊断" > </el-table-column > <el-table-column prop ="remark" label ="备注" > </el-table-column > <el-table-column label ="操作" > <template slot-scope ="scope" > <el-button @click ="handleDetail(scope.row)" type ="text" size ="small" > 查看详情</el-button > </template > </el-table-column > </el-table > <el-dialog title ="病历详情" :visible.sync ="dialogVisible" > <p > 诊断日期: {{ currentRecord.diagnosisDate }}</p > <p > 医生: {{ currentRecord.doctor }}</p > <p > 诊断: {{ currentRecord.diagnosis }}</p > <p > 备注: {{ currentRecord.remark }}</p > <span slot ="footer" class ="dialog-footer" > <el-button type ="primary" @click ="dialogVisible = false" > 关闭</el-button > </span > </el-dialog > </div > </template > <script > import { useRoute } from 'vue-router' ; export default { data ( ) { return { userId : null , medicalRecords : [], dialogVisible : false , currentRecord : {} }; }, created ( ) { const route = useRoute (); this .userId = route.params .userId ; this .fetchMedicalRecords (); }, methods : { fetchMedicalRecords ( ) { }, handleDetail (record ) { this .currentRecord = record; this .dialogVisible = true ; } } }; </script > <style scoped > .medical-records { padding : 20px ; } </style >
效果图
系统漏洞分析
后端没有编写对发送过来的表单进行二次审查的功能,所以很容易被攻击输入非法数据甚至是SQL注入,安全性几乎是0。
用户输入的所有数据都是透明传输,所以用户输入的数据很容易被窃取。
虽然这些系统上的问题都可以通过增加功能进行防御,我都想写,但是时间上来不及了,手上也没有现成的开源模块可以直接塞进去(手上能用的是Python的代码),不过还是在这里简单总结一下,谨留以后效。
结语 至此,我们完成了对用户注册和登录功能的开发,并添加了表单验证和错误处理。现在,用户可以注册他们的账户,并且系统会根据输入的数据进行验证,确保数据的完整性和准确性现在我们了解了怎么在IDEA中用SpringBoot进行数据库方面的开发,另外学了一些前端的知识,也算是前端成功入门了,技能+++++……
本文使用过的AI工具: Qwen DeepSeek
在遇到一些技术问题,比如在测试中怎么取消测试用例的回滚,以及怎么防止测试用例访问自己的H2数据库而不是我的Mysql这些问题上,感谢这些AI帮我解决了这些关键问题,顺便还了解了JPA和EntityManager的知识,爽!AI技术进步和开源真好,在AI工具的帮助下,我学东西都快了不少。