feat(security): 添加 JWT 认证功能
- 在后端添加 JWT 认证过滤器 JwtAuthenticationTokenFilter - 创建 JwtTokenUtil 工具类用于生成和验证 JWT token - 在 application.yml 中配置 JWT 相关参数 - 更新前端 HomePage 组件,增加用户认证相关逻辑
This commit is contained in:
@@ -9,14 +9,6 @@
|
||||
"alwaysAllow": [
|
||||
"sequentialthinking"
|
||||
]
|
||||
},
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-filesystem",
|
||||
"C:/AIFA/2 "
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,30 @@
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT Library -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 测试依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -81,17 +105,7 @@
|
||||
<artifactId>java-uuid-generator</artifactId>
|
||||
<version>4.0.1</version>
|
||||
</dependency>
|
||||
<!-- 文件上传支持 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<!-- 密码加密-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-core</artifactId>
|
||||
<version>5.7.1</version> <!-- 使用最新稳定版 -->
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- SHA256计算工具 -->
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.test.bijihoudaun.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import com.test.bijihoudaun.interceptor.JwtAuthenticationTokenFilter;
|
||||
import com.test.bijihoudaun.util.JwtTokenUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Autowired
|
||||
private UserDetailsService userDetailsService;
|
||||
|
||||
@Autowired
|
||||
private JwtTokenUtil jwtTokenUtil;
|
||||
|
||||
@Value("${jwt.header}")
|
||||
private String tokenHeader;
|
||||
|
||||
@Value("${jwt.tokenHead}")
|
||||
private String tokenHead;
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter = new JwtAuthenticationTokenFilter(userDetailsService, jwtTokenUtil, tokenHeader, tokenHead);
|
||||
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(authz -> authz
|
||||
.requestMatchers("/doc.html", "/webjars/**", "/v3/api-docs/**", "/api/user/login", "/api/user/register").permitAll()
|
||||
.requestMatchers(org.springframework.http.HttpMethod.GET).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
);
|
||||
|
||||
// 在这里添加JWT过滤器
|
||||
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,10 @@ import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "用户接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/user")
|
||||
@@ -36,8 +40,11 @@ public class UserController {
|
||||
@Parameter(name = "password", description = "密码",required = true)
|
||||
})
|
||||
@PostMapping("/login")
|
||||
public R<User> login(String username, String password){
|
||||
return R.success(userService.login(username,password));
|
||||
public R<Map<String, String>> login(String username, String password){
|
||||
String token = userService.login(username, password);
|
||||
Map<String, String> tokenMap = new HashMap<>();
|
||||
tokenMap.put("token", token);
|
||||
return R.success(tokenMap);
|
||||
}
|
||||
|
||||
@Operation(summary = "用户删除")
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.test.bijihoudaun.interceptor;
|
||||
|
||||
import com.test.bijihoudaun.util.JwtTokenUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
|
||||
|
||||
private final UserDetailsService userDetailsService;
|
||||
private final JwtTokenUtil jwtTokenUtil;
|
||||
private final String tokenHeader;
|
||||
private final String tokenHead;
|
||||
|
||||
public JwtAuthenticationTokenFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, String tokenHeader, String tokenHead) {
|
||||
this.userDetailsService = userDetailsService;
|
||||
this.jwtTokenUtil = jwtTokenUtil;
|
||||
this.tokenHeader = tokenHeader;
|
||||
this.tokenHead = tokenHead;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
|
||||
String authHeader = request.getHeader(this.tokenHeader);
|
||||
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
|
||||
final String authToken = authHeader.substring(this.tokenHead.length());
|
||||
String username = jwtTokenUtil.getUsernameFromToken(authToken);
|
||||
|
||||
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
|
||||
|
||||
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
|
||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
}
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,11 @@ package com.test.bijihoudaun.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.bijihoudaun.entity.User;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface UserMapper extends BaseMapper<User> {
|
||||
// 自定义查询方法示例
|
||||
// @Select("SELECT * FROM user WHERE username = #{username}")
|
||||
// User findByUsername(String username);
|
||||
@Select("SELECT * FROM user WHERE username = #{username}")
|
||||
User findByUsername(String username);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public interface UserService extends IService<User> {
|
||||
* @param password 密码
|
||||
* @return 登录成功的用户
|
||||
*/
|
||||
User login(String username, String password);
|
||||
String login(String username, String password);
|
||||
|
||||
/**
|
||||
* 用户删除
|
||||
|
||||
@@ -6,21 +6,34 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.test.bijihoudaun.entity.User;
|
||||
import com.test.bijihoudaun.mapper.UserMapper;
|
||||
import com.test.bijihoudaun.service.UserService;
|
||||
import com.test.bijihoudaun.util.JwtTokenUtil;
|
||||
import com.test.bijihoudaun.util.PasswordUtils;
|
||||
import com.test.bijihoudaun.util.UuidV7;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
|
||||
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService, UserDetailsService {
|
||||
|
||||
@Autowired
|
||||
private UserMapper userMapper;
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
User user = userMapper.findByUsername(username);
|
||||
if (user == null) {
|
||||
throw new UsernameNotFoundException("User not found with username: " + username);
|
||||
}
|
||||
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), new ArrayList<>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public User register(String username, String password, String email) {
|
||||
String encrypt = PasswordUtils.encrypt(password);
|
||||
@@ -43,23 +56,16 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
return user;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private JwtTokenUtil jwtTokenUtil;
|
||||
|
||||
@Override
|
||||
public User login(String username, String password) {
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(User::getUsername, username);
|
||||
User user = userMapper.selectOne(queryWrapper);
|
||||
boolean verify = PasswordUtils.verify(password, user.getPassword());
|
||||
if (!verify) {
|
||||
public String login(String username, String password) {
|
||||
UserDetails userDetails = loadUserByUsername(username);
|
||||
if (!PasswordUtils.verify(password, userDetails.getPassword())) {
|
||||
throw new RuntimeException("密码错误");
|
||||
}
|
||||
user.setToken(UuidV7.uuidNoHyphen());
|
||||
// 过期时间:当前时间+3天的时间
|
||||
// 修改时间计算方式
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.add(Calendar.DAY_OF_MONTH, 3); // 增加3天
|
||||
user.setTokenEnddata(calendar.getTime());
|
||||
userMapper.updateById(user);
|
||||
return user;
|
||||
return jwtTokenUtil.generateToken(userDetails);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.test.bijihoudaun.util;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
@Component
|
||||
public class JwtTokenUtil {
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.expiration}")
|
||||
private Long expiration;
|
||||
|
||||
// 从token中获取用户名
|
||||
public String getUsernameFromToken(String token) {
|
||||
return getClaimFromToken(token, Claims::getSubject);
|
||||
}
|
||||
|
||||
// 从token中获取过期时间
|
||||
public Date getExpirationDateFromToken(String token) {
|
||||
return getClaimFromToken(token, Claims::getExpiration);
|
||||
}
|
||||
|
||||
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
|
||||
final Claims claims = getAllClaimsFromToken(token);
|
||||
return claimsResolver.apply(claims);
|
||||
}
|
||||
|
||||
// 为了从token中获取任何信息,我们都需要密钥
|
||||
private Claims getAllClaimsFromToken(String token) {
|
||||
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
|
||||
}
|
||||
|
||||
// 检查token是否过期
|
||||
private Boolean isTokenExpired(String token) {
|
||||
final Date expiration = getExpirationDateFromToken(token);
|
||||
return expiration.before(new Date());
|
||||
}
|
||||
|
||||
// 为用户生成token
|
||||
public String generateToken(UserDetails userDetails) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
return doGenerateToken(claims, userDetails.getUsername());
|
||||
}
|
||||
|
||||
// 创建token
|
||||
private String doGenerateToken(Map<String, Object> claims, String subject) {
|
||||
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
|
||||
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
|
||||
.signWith(SignatureAlgorithm.HS512, secret).compact();
|
||||
}
|
||||
|
||||
// 验证token
|
||||
public Boolean validateToken(String token, UserDetails userDetails) {
|
||||
final String username = getUsernameFromToken(token);
|
||||
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
|
||||
}
|
||||
}
|
||||
@@ -29,3 +29,10 @@ mybatis-plus:
|
||||
mapper-locations: classpath:mapper/*.xml
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
|
||||
# JWT 配置
|
||||
jwt:
|
||||
secret: mysecretkeymysecretkeymysecretkeymysecretkeymysecretkey # 至少256位的密钥
|
||||
expiration: 86400 # token有效期,单位秒,这里是24小时
|
||||
header: Authorization # JWT存储的请求头
|
||||
tokenHead: "Bearer " # JWT负载中拿到开头
|
||||
|
||||
244
biji-qianduan/package-lock.json
generated
244
biji-qianduan/package-lock.json
generated
@@ -10,7 +10,9 @@
|
||||
"dependencies": {
|
||||
"@kangc/v-md-editor": "^2.2.4",
|
||||
"codemirror": "^6.0.1",
|
||||
"element-plus": "^2.10.4",
|
||||
"highlight.js": "^11.11.1",
|
||||
"vditor": "^3.11.1",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -160,6 +162,24 @@
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@ctrl/tinycolor": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
|
||||
"integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@element-plus/icons-vue": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz",
|
||||
"integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
|
||||
@@ -602,6 +622,31 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz",
|
||||
"integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz",
|
||||
"integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.2",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
||||
@@ -702,6 +747,17 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"name": "@sxzz/popperjs-es",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
|
||||
"integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz",
|
||||
@@ -1029,6 +1085,21 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash-es": {
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdast": {
|
||||
"version": "3.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz",
|
||||
@@ -1065,6 +1136,12 @@
|
||||
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
|
||||
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vant/icons": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@vant/icons/-/icons-1.8.0.tgz",
|
||||
@@ -1263,6 +1340,94 @@
|
||||
"upath": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "9.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz",
|
||||
"integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.16",
|
||||
"@vueuse/metadata": "9.13.0",
|
||||
"@vueuse/shared": "9.13.0",
|
||||
"vue-demi": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "9.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz",
|
||||
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "9.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
|
||||
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vue-demi": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
@@ -1350,6 +1515,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async-validator": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
||||
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/atob": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||
@@ -2227,6 +2398,12 @@
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/diff-match-patch": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
|
||||
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz",
|
||||
@@ -2245,6 +2422,32 @@
|
||||
"integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)"
|
||||
},
|
||||
"node_modules/element-plus": {
|
||||
"version": "2.10.4",
|
||||
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.10.4.tgz",
|
||||
"integrity": "sha512-UD4elWHrCnp1xlPhbXmVcaKFLCRaRAY6WWRwemGfGW3ceIjXm9fSYc9RNH3AiOEA6Ds1p9ZvhCs76CR9J8Vd+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^3.4.1",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@floating-ui/dom": "^1.0.1",
|
||||
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@vueuse/core": "^9.1.0",
|
||||
"async-validator": "^4.2.5",
|
||||
"dayjs": "^1.11.13",
|
||||
"escape-html": "^1.0.3",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash-unified": "^1.0.2",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-wheel-es": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/elkjs": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz",
|
||||
@@ -3060,12 +3263,29 @@
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-unified": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
|
||||
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/lodash-es": "*",
|
||||
"lodash": "*",
|
||||
"lodash-es": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||
@@ -3227,6 +3447,12 @@
|
||||
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@@ -3832,6 +4058,12 @@
|
||||
"integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-wheel-es": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
|
||||
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/object-copy": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
|
||||
@@ -4780,6 +5012,18 @@
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vditor": {
|
||||
"version": "3.11.1",
|
||||
"resolved": "https://registry.npmjs.org/vditor/-/vditor-3.11.1.tgz",
|
||||
"integrity": "sha512-7rjNSXYVyZG0mVZpUG2tfxwnoNtkcRCnwdSju+Zvpjf/r72iQa6kLpeThFMIKPuQ5CRnQQv6gnR3eNU6UGbC2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"diff-match-patch": "^1.0.5"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ld246.com/sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
"dependencies": {
|
||||
"@kangc/v-md-editor": "^2.2.4",
|
||||
"codemirror": "^6.0.1",
|
||||
"element-plus": "^2.10.4",
|
||||
"highlight.js": "^11.11.1",
|
||||
"vditor": "^3.11.1",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<div class="container">
|
||||
<el-container class="home-page">
|
||||
<!-- 左侧菜单区域 -->
|
||||
<div v-if="!isCollapsed" class="sidebar">
|
||||
<el-aside :width="isCollapsed ? '64px' : '250px'" class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<span v-if="!isCollapsed" style="margin-right: 15px">笔记分类</span>
|
||||
<el-button v-if="!isCollapsed" type="primary" size="small" @click="showCreateGroupDialog = true">
|
||||
新建分类
|
||||
<span v-if="!isCollapsed" style="margin-right: 15px; font-weight: bold;">笔记分类</span>
|
||||
<el-button v-if="!isCollapsed" type="primary" size="small" @click="showCreateGroupDialog = true" circle>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-button>
|
||||
|
||||
<el-button v-if="!isCollapsed" @click="isCollapsed=!isCollapsed" type="primary" size="small">
|
||||
收起
|
||||
<el-button @click="isCollapsed = !isCollapsed" type="primary" size="small" circle>
|
||||
<el-icon>
|
||||
<Fold v-if="!isCollapsed" />
|
||||
<Expand v-else />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
@@ -19,58 +20,48 @@
|
||||
class="el-menu-vertical-demo"
|
||||
:collapse="isCollapsed"
|
||||
popper-effect="light"
|
||||
collapse-transition
|
||||
:collapse-transition="false"
|
||||
>
|
||||
|
||||
<!-- 分组分类 -->
|
||||
<el-sub-menu v-for="group in groupings" :key="group.id" :index="`group-${group.id}`">
|
||||
<template #title>
|
||||
<el-icon><Folder /></el-icon>
|
||||
<span>{{ group.grouping }}</span>
|
||||
</template>
|
||||
<el-menu-item v-for="sub in jb22.filter(j => +j.parentId === +group.id)"
|
||||
<el-menu-item
|
||||
v-for="sub in jb22.filter(j => +j.parentId === +group.id)"
|
||||
:key="sub.id"
|
||||
:index="`sub-${sub.id}`"
|
||||
@click="selectFile(sub); selectedFile = null"
|
||||
>{{ sub.grouping }}</el-menu-item>
|
||||
>
|
||||
<el-icon><Document /></el-icon>
|
||||
{{ sub.grouping }}
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</div>
|
||||
|
||||
<el-icon :size="25" style="margin-top: 20px;margin-left: 10px" v-if="isCollapsed" @click="isCollapsed=!isCollapsed" ><DArrowRight /></el-icon>
|
||||
</el-aside>
|
||||
|
||||
<!-- 右侧内容区域 -->
|
||||
<div class="content">
|
||||
<el-container>
|
||||
<el-main class="content">
|
||||
<div v-if="selectedFile" class="file-preview">
|
||||
<div class="preview-header">
|
||||
<el-header class="preview-header">
|
||||
<h2>{{ selectedFile.title }}</h2>
|
||||
<div class="actions">
|
||||
<el-button v-if="!showEditor" type="primary" @click="selectedFile = null">清空</el-button>
|
||||
<el-button v-if="!showEditor" type="primary" @click="editNote(selectedFile); isCollapsed = true">编辑</el-button>
|
||||
<el-button v-if="!showEditor" type="danger" @click="deleteNote(selectedFile)">删除</el-button>
|
||||
<el-button v-if="showEditor" type="primary" @click="showEditor = !showEditor; previewFile(editData)">返回</el-button>
|
||||
<el-button v-if="showEditor" type="success" @click="handleSave(editData.content)">保存</el-button>
|
||||
<el-button v-if="showEditor" type="success" @click="handleSave(vditor.getValue())">保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<v-md-preview
|
||||
v-if="!showEditor"
|
||||
:text="selectedFile.content"
|
||||
class="markdown-preview"
|
||||
@copy-code-success="handleCopyCodeSuccess"
|
||||
></v-md-preview>
|
||||
<!-- Markdown编辑器 -->
|
||||
<v-md-editor
|
||||
v-if="showEditor"
|
||||
v-model="editData.content"
|
||||
height="500px"
|
||||
@upload-image="handleImageUpload"
|
||||
@save="handleSave"
|
||||
:disabled-menus="[]"
|
||||
@copy-code-success="handleCopyCodeSuccess"
|
||||
></v-md-editor>
|
||||
</el-header>
|
||||
<div v-if="!showEditor" v-html="previewHtml" class="markdown-preview"></div>
|
||||
<!-- Vditor 编辑器 -->
|
||||
<div v-show="showEditor" id="vditor" class="vditor" />
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="header">
|
||||
<el-header class="header">
|
||||
<h1>我的笔记</h1>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="showCreateNoteDialog = true">新建笔记</el-button>
|
||||
@@ -84,29 +75,36 @@
|
||||
<el-button type="success">上传Markdown</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<div v-if="groupMarkdownFiles.length > 0" class="file-list">
|
||||
<div v-for="file in groupMarkdownFiles" :key="file.id" class="file-item">
|
||||
<el-card v-for="file in groupMarkdownFiles" :key="file.id" shadow="hover" class="file-item">
|
||||
<div @click="previewFile(file)" class="file-title">{{ file.title }}</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-tip">暂无笔记,请创建或上传</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="暂无笔记,请创建或上传" />
|
||||
</div>
|
||||
|
||||
<!-- 分类创建对话框 -->
|
||||
<el-dialog v-model="showCreateGroupDialog" title="新建分类" width="30%">
|
||||
<el-form :model="newGroupForm" label-width="80px">
|
||||
<el-switch v-model="isGroup1" active-text="一级分类" inactive-text="二级分类" style="margin-bottom: 20px;margin-left:30%" />
|
||||
<el-form-item label="一级名称">
|
||||
<el-input v-model="newGroupForm.name" autocomplete="off"></el-input>
|
||||
<el-dialog v-model="showCreateGroupDialog" title="新建分类" width="400px" @close="resetGroupForm">
|
||||
<el-form :model="newGroupForm" :rules="groupFormRules" ref="groupFormRef" label-width="80px">
|
||||
<el-form-item label="分类级别">
|
||||
<el-radio-group v-model="isGroup1">
|
||||
<el-radio :label="true">一级分类</el-radio>
|
||||
<el-radio :label="false">二级分类</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="一级名称">
|
||||
<el-input v-model="newGroupForm.name" autocomplete="off"></el-input>
|
||||
<el-form-item v-if="!isGroup1" label="父级分类" prop="parentId">
|
||||
<el-select v-model="newGroupForm.parentId" placeholder="请选择父级分类">
|
||||
<el-option
|
||||
v-for="group in groupings"
|
||||
:key="group.id"
|
||||
:label="group.grouping"
|
||||
:value="group.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!isGroup1" label="二级名称">
|
||||
<el-form-item label="分类名称" prop="name">
|
||||
<el-input v-model="newGroupForm.name" autocomplete="off"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -117,13 +115,13 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 笔记创建对话框 -->
|
||||
<el-dialog v-model="showCreateNoteDialog" title="新建笔记" width="30%">
|
||||
<el-form :model="newNoteForm" label-width="80px">
|
||||
<el-form-item label="笔记标题">
|
||||
<el-dialog v-model="showCreateNoteDialog" title="新建笔记" width="400px" @close="resetNoteForm">
|
||||
<el-form :model="newNoteForm" :rules="noteFormRules" ref="noteFormRef" label-width="80px">
|
||||
<el-form-item label="笔记标题" prop="title">
|
||||
<el-input v-model="newNoteForm.title" autocomplete="off"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="选择大分类">
|
||||
<el-select v-model="fenlei1" :change="getjb2()" placeholder="请选择">
|
||||
<el-form-item label="选择大分类" prop="parentId">
|
||||
<el-select v-model="fenlei1" placeholder="请选择" @change="getjb2">
|
||||
<el-option
|
||||
v-for="item in groupings"
|
||||
:key="item.id"
|
||||
@@ -132,7 +130,7 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="选择分类">
|
||||
<el-form-item label="选择分类" prop="groupingId">
|
||||
<el-select v-model="newNoteForm.groupingId" placeholder="请选择">
|
||||
<el-option
|
||||
v-for="group in fenlei2"
|
||||
@@ -148,14 +146,16 @@
|
||||
<el-button type="primary" @click="createNote">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from 'vue';
|
||||
import {onMounted, ref, nextTick} from 'vue';
|
||||
import {ElMessage} from 'element-plus';
|
||||
import '@kangc/v-md-editor/lib/style/preview.css';
|
||||
import '@kangc/v-md-editor/lib/theme/style/github.css';
|
||||
import Vditor from 'vditor';
|
||||
import 'vditor/dist/index.css';
|
||||
import {
|
||||
addGroupings,
|
||||
deleteImages, deleteMarkdown,
|
||||
@@ -165,7 +165,7 @@ import {
|
||||
Preview,
|
||||
updateMarkdown, uploadImage
|
||||
} from '@/api/CommonApi.js'
|
||||
import {DArrowRight} from "@element-plus/icons-vue";
|
||||
import { DArrowRight, Plus, Fold, Expand, Folder, Document } from "@element-plus/icons-vue";
|
||||
|
||||
const isGroup1=ref(true)
|
||||
// 创建新文件中大分类的信息
|
||||
@@ -182,14 +182,29 @@ const activeMenu = ref('all');
|
||||
const isCollapsed = ref(false);
|
||||
const showCreateGroupDialog = ref(false);
|
||||
const showCreateNoteDialog = ref(false);
|
||||
const newGroupForm = ref({ name: '' });
|
||||
|
||||
const groupFormRef = ref(null);
|
||||
const newGroupForm = ref({ name: '', parentId: null });
|
||||
const groupFormRules = ref({
|
||||
name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }],
|
||||
parentId: [{ required: true, message: '请选择父级分类', trigger: 'change' }],
|
||||
});
|
||||
|
||||
const noteFormRef = ref(null);
|
||||
const newNoteForm = ref({
|
||||
id: null,
|
||||
title: '',
|
||||
groupingId: null,
|
||||
parentId: null,
|
||||
fileName: '',
|
||||
content: ''
|
||||
});
|
||||
const noteFormRules = ref({
|
||||
title: [{ required: true, message: '请输入笔记标题', trigger: 'blur' }],
|
||||
parentId: [{ required: true, message: '请选择大分类', trigger: 'change' }],
|
||||
groupingId: [{ required: true, message: '请选择二级分类', trigger: 'change' }],
|
||||
});
|
||||
|
||||
// 创建新笔记的多级菜单
|
||||
const options=ref([])
|
||||
// 编辑笔记的数据
|
||||
@@ -201,6 +216,28 @@ const originalImages = ref([]);
|
||||
// 分类为二级的数据
|
||||
const jb22=ref([])
|
||||
|
||||
// Vditor 实例
|
||||
const vditor = ref(null);
|
||||
const previewHtml = ref('');
|
||||
|
||||
const initVditor = () => {
|
||||
vditor.value = new Vditor('vditor', {
|
||||
height: 'calc(100vh - 120px)',
|
||||
mode: 'ir', // 即时渲染模式
|
||||
after: () => {
|
||||
if (editData.value) {
|
||||
vditor.value.setValue(editData.value.content);
|
||||
}
|
||||
},
|
||||
upload: {
|
||||
accept: 'image/*',
|
||||
handler(files) {
|
||||
handleImageUpload(files);
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 创建md文件时通过大分类获取二级分类
|
||||
const getjb2 = async () => {
|
||||
if (fenlei1.value != null) {
|
||||
@@ -246,11 +283,6 @@ const selectFile = async (data) => {
|
||||
groupMarkdownFiles.value=promise.data
|
||||
};
|
||||
|
||||
// 代码块复制成功回调
|
||||
const handleCopyCodeSuccess = () => {
|
||||
ElMessage.success('代码已复制到剪贴板');
|
||||
};
|
||||
|
||||
// 获取所有Markdown文件(确保ID为字符串)
|
||||
const fetchMarkdownFiles = async () => {
|
||||
try {
|
||||
@@ -267,30 +299,58 @@ const fetchMarkdownFiles = async () => {
|
||||
|
||||
// 创建新分类
|
||||
const createGrouping = async () => {
|
||||
// TODO 添加分类创建逻辑
|
||||
if (!groupFormRef.value) return;
|
||||
await groupFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const response = await addGroupings(newGroupForm.value.name)
|
||||
const response = await addGroupings(newGroupForm.value)
|
||||
ElMessage.success('分类创建成功');
|
||||
showCreateGroupDialog.value = false;
|
||||
newGroupForm.value.name = '';
|
||||
await fetchGroupings();
|
||||
} catch (error) {
|
||||
ElMessage.error('创建分类失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 重置新建分类表单
|
||||
const resetGroupForm = () => {
|
||||
newGroupForm.value = { name: '', parentId: null };
|
||||
if (groupFormRef.value) {
|
||||
groupFormRef.value.resetFields();
|
||||
}
|
||||
};
|
||||
|
||||
// 创建新笔记
|
||||
const createNote = async () => {
|
||||
if (!noteFormRef.value) return;
|
||||
await noteFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
newNoteForm.value.fileName = newNoteForm.value.title+'.md'
|
||||
editData.value=newNoteForm.value
|
||||
console.log(editData.value)
|
||||
showCreateNoteDialog.value = false
|
||||
showEditor.value = true;
|
||||
selectedFile.value=editData.value
|
||||
await nextTick(() => {
|
||||
initVditor();
|
||||
});
|
||||
} catch (error) {
|
||||
ElMessage.error('创建笔记失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 重置新建笔记表单
|
||||
const resetNoteForm = () => {
|
||||
newNoteForm.value = { id: null, title: '', groupingId: null, parentId: null, fileName: '', content: '' };
|
||||
fenlei1.value = null;
|
||||
fenlei2.value = null;
|
||||
if (noteFormRef.value) {
|
||||
noteFormRef.value.resetFields();
|
||||
}
|
||||
};
|
||||
|
||||
// 选择文件预览
|
||||
@@ -318,22 +378,25 @@ const previewFile = async (file) => {
|
||||
...file,
|
||||
content: content
|
||||
};
|
||||
Vditor.preview(document.querySelector('.markdown-preview'), content);
|
||||
} catch (error) {
|
||||
ElMessage.error('获取笔记内容失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑笔记
|
||||
const editNote = (file) => {
|
||||
const editNote = async (file) => {
|
||||
editData.value = file
|
||||
originalImages.value = extractImageUrls(file.content);
|
||||
showEditor.value = true;
|
||||
await nextTick(() => {
|
||||
initVditor();
|
||||
});
|
||||
};
|
||||
|
||||
// 图片上传
|
||||
const handleImageUpload=async (event, insertImage, files) => {
|
||||
console.log(files)
|
||||
const promise = await uploadImage(files[0]);
|
||||
const handleImageUpload=async (files) => {
|
||||
const promise = await uploadImage(files);
|
||||
if (promise.code !== 200) {
|
||||
ElMessage.error(promise.msg);
|
||||
return;
|
||||
@@ -342,13 +405,8 @@ const handleImageUpload=async (event, insertImage, files) => {
|
||||
const imageUrl = promise.data.url.startsWith('/')
|
||||
? `http://127.0.0.1:8084${promise.data.url}`
|
||||
: promise.data.url;
|
||||
// 插入图片
|
||||
insertImage({
|
||||
// 图片地址
|
||||
url: imageUrl
|
||||
// 图片描述
|
||||
// desc: '七龙珠',
|
||||
});
|
||||
|
||||
vditor.value.insertValue(``);
|
||||
}
|
||||
|
||||
// 在编辑页面,按Ctrl+S保存笔记,或者点击保存,对数据进行保存
|
||||
@@ -387,21 +445,21 @@ const extractImageUrls = (data) => {
|
||||
const mdRegex = /!\[.*?\]\((.*?)\)/g;
|
||||
let mdMatch;
|
||||
while ((mdMatch = mdRegex.exec(content)) !== null) {
|
||||
urls.push(getPathFromUrl(mdMatch[1]))
|
||||
urls.push(getPathFromUrl(mdMatch))
|
||||
}
|
||||
|
||||
// 匹配HTML img标签
|
||||
const htmlRegex = /<img[^>]+src="([^">]+)"/g;
|
||||
let htmlMatch;
|
||||
while ((htmlMatch = htmlRegex.exec(content)) !== null) {
|
||||
urls.push(getPathFromUrl(htmlMatch[1]));
|
||||
urls.push(getPathFromUrl(htmlMatch));
|
||||
}
|
||||
|
||||
// 匹配base64图片
|
||||
const base64Regex = /<img[^>]+src="(data:image\/[^;]+;base64[^">]+)"/g;
|
||||
let base64Match;
|
||||
while ((base64Match = base64Regex.exec(content)) !== null) {
|
||||
urls.push(base64Match[1]);
|
||||
urls.push(base64Match);
|
||||
}
|
||||
|
||||
// 过滤和去重
|
||||
@@ -473,7 +531,11 @@ const handleMarkdownUpload = (file) => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
chushihua()
|
||||
chushihua();
|
||||
// 根据屏幕宽度初始化侧边栏状态
|
||||
if (window.innerWidth < 768) {
|
||||
isCollapsed.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
const chushihua = async () => {
|
||||
@@ -488,16 +550,12 @@ const chushihua = async () => {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: #fff;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
@@ -509,9 +567,8 @@ const chushihua = async () => {
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
height: 100vh;
|
||||
margin-left: 20px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -538,16 +595,12 @@ const chushihua = async () => {
|
||||
}
|
||||
|
||||
.file-item {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.file-title {
|
||||
@@ -555,18 +608,8 @@ const chushihua = async () => {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
height: 100vh;
|
||||
padding: 20px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -580,17 +623,33 @@ const chushihua = async () => {
|
||||
|
||||
.markdown-preview {
|
||||
flex: 1;
|
||||
border: 1px solid #aa9898;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.el-menu-vertical-demo {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.el-menu-vertical-demo:not(.el-menu--collapse) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,11 +2,29 @@
|
||||
|
||||
本计划旨在为您的笔记项目提供一个清晰、分步走的开发与美化路线图。
|
||||
|
||||
## 第一阶段:基础功能完善与安全加固
|
||||
## 第一阶段:核心体验提升 - UI美化与交互优化
|
||||
|
||||
这个阶段的目标是全面提升应用的颜值和易用性。
|
||||
|
||||
- **任务1.1:引入Element Plus UI库**
|
||||
- [ ] **前端**: 安装并配置 Element Plus。
|
||||
- [ ] 运行 `npm install element-plus --save`。
|
||||
- [ ] 在 `main.js` 中全局引入 Element Plus。
|
||||
- [ ] 使用 Element Plus 的组件重构现有页面,如按钮、表单、布局等。
|
||||
- **任务1.2:响应式布局**
|
||||
- [ ] **前端**: 使用 Element Plus 的栅格系统或 CSS Flexbox/Grid 来实现响应式布局,确保在不同设备上都能良好显示。
|
||||
- **任务1.3:升级Markdown编辑器**
|
||||
- [ ] **前端**: 调研并集成一个功能更强大的Markdown编辑器,例如 `vditor` 或 `cherry-markdown`。
|
||||
- [ ] 替换现有的 `MarkdownEditor.vue` 组件。
|
||||
- [ ] 确保新编辑器与应用的集成,包括内容的双向绑定和图片的上传。
|
||||
- **(可选任务)任务:重构前端页面**
|
||||
- [ ] **前端**: 重新对整个页面进行重构,使其更简洁、易用。
|
||||
|
||||
## 第二阶段:基础功能完善与安全加固
|
||||
|
||||
这个阶段的目标是补齐核心功能,并确保应用的安全性。
|
||||
|
||||
- **任务1.1:实现用户认证与授权**
|
||||
- **任务2.1:实现用户认证与授权**
|
||||
- [ ] **后端**: JWT。
|
||||
- [ ] 实现 UserDetailsService 来加载用户信息。
|
||||
- [ ] 创建 JWT 工具类,用于生成和验证 Token。
|
||||
@@ -18,24 +36,6 @@
|
||||
- [ ] 封装 `axios`,在请求头中自动添加 `Authorization` 字段。
|
||||
- [ ] 实现路由守卫,未登录用户访问受保护页面时跳转到登录页。
|
||||
- [ ] 实现登出功能。
|
||||
|
||||
## 第二阶段:核心体验提升 - UI美化与交互优化
|
||||
|
||||
这个阶段的目标是全面提升应用的颜值和易用性。
|
||||
|
||||
- **任务2.1:引入Element Plus UI库**
|
||||
- [ ] **前端**: 安装并配置 Element Plus。
|
||||
- [ ] 运行 `npm install element-plus --save`。
|
||||
- [ ] 在 `main.js` 中全局引入 Element Plus。
|
||||
- [ ] 使用 Element Plus 的组件重构现有页面,如按钮、表单、布局等。
|
||||
- **任务2.2:响应式布局**
|
||||
- [ ] **前端**: 使用 Element Plus 的栅格系统或 CSS Flexbox/Grid 来实现响应式布局,确保在不同设备上都能良好显示。
|
||||
- **任务2.3:升级Markdown编辑器**
|
||||
- [ ] **前端**: 调研并集成一个功能更强大的Markdown编辑器,例如 `vditor` 或 `cherry-markdown`。
|
||||
- [ ] 替换现有的 `MarkdownEditor.vue` 组件。
|
||||
- [ ] 确保新编辑器与应用的集成,包括内容的双向绑定和图片的上传。
|
||||
- **(可选任务)任务:重构前端页面**
|
||||
- [ ] **前端**: 重新对整个页面进行重构,使其更简洁、易用。
|
||||
## 第三阶段:高级功能拓展
|
||||
|
||||
这个阶段我们将为应用增加更多有价值的功能。
|
||||
|
||||
Reference in New Issue
Block a user