前端页面作品集页面以及后端对应代码实现1.0

实现点赞,标签,悬浮通知
This commit is contained in:
xiaowang 2025-07-12 01:59:09 +08:00
parent 475e0461e2
commit 1f379b9c21
30 changed files with 2872 additions and 299 deletions

View File

@ -30,38 +30,113 @@
<java.version>21</java.version> <java.version>21</java.version>
</properties> </properties>
<dependencies> <dependencies>
<!-- Spring Boot JDBC数据访问启动器 -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId> <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency> </dependency>
<!-- Spring Boot Redis数据访问启动器 -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId> <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency> </dependency>
<!-- Spring Boot JDBC启动器 -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId> <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency> </dependency>
<!-- Spring Boot Web启动器 -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<!-- MySQL连接器 -->
<dependency> <dependency>
<groupId>com.mysql</groupId> <groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId> <artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<!-- Lombok注解处理器 -->
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<!-- Spring Boot测试启动器 -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- 阿里云OSS对象存储依赖 -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
<!-- FastJSON JSON处理库 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.31</version>
</dependency>
<!-- MyBatis-Plus启动器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.7</version>
</dependency>
<!-- Druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.18</version>
</dependency>
<!-- Redis客户端依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- AOP依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 日志框架 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<!-- JSON处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- IP地址解析 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
<!-- IP地址库 -->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.6.4</version>
</dependency>
<!-- Spring Boot 3 需要使用 jakarta.servlet -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -1,11 +1,12 @@
package com.nailart; package com.nailart;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication @SpringBootApplication
@MapperScan("com.nailart.mapper")
public class NaCoreApplication { public class NaCoreApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(NaCoreApplication.class, args); SpringApplication.run(NaCoreApplication.class, args);
} }

View File

@ -0,0 +1,91 @@
package com.nailart.aop;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nailart.dto.RequestLogDTO;
import com.nailart.utils.IpAddressUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
/**
* @Description 实现日志记录
* @Classname RequestLogDTOAspect
* @Date 2025/7/11 0:05
* @Created by 21616
*/
@Aspect
@Component
public class RequestLogAspect {
@Autowired
private IpAddressUtil ipAddressUtil;
private final ObjectMapper objectMapper = new ObjectMapper();
private final String logFilePath = "request_logs.json"; // 日志文件路径
@Pointcut("execution(* com.nailart.controller..*(..))")
public void requestPointcut() {}
@Around("requestPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return joinPoint.proceed();
}
HttpServletRequest request = attributes.getRequest();
RequestLogDTO log = new RequestLogDTO();
log.setRequestTime(new Date());
log.setIp(getClientIp(request));
log.setCity(ipAddressUtil.getCityInfo(log.getIp()));
log.setUrl(request.getRequestURL().toString());
log.setHttpMethod(request.getMethod());
log.setMethod(joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
try {
return joinPoint.proceed();
} finally {
log.setResponseTime(System.currentTimeMillis() - startTime);
// 直接将日志写入文件不使用日志框架
writeLogToFile(log);
}
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
private void writeLogToFile(RequestLogDTO log) {
try (PrintWriter writer = new PrintWriter(new FileWriter(logFilePath, true))) {
// 将日志对象序列化为JSON并写入文件
writer.println(objectMapper.writeValueAsString(log));
} catch (IOException e) {
// 处理写入异常可以选择记录到标准输出或忽略
System.err.println("Failed to write request log: " + e.getMessage());
}
}
}

View File

@ -0,0 +1,31 @@
package com.nailart.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @Description 跨域处理
* @Classname CorsConfig
* @Date 2025/7/11 0:01
* @Created by 21616
*/
// 案例
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
//是否发送Cookie
.allowCredentials(false)
//放行哪些原始域
.allowedOrigins("*")
.allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"})
.allowedHeaders("*")
.exposedHeaders("*");
}
}

View File

@ -0,0 +1,29 @@
package com.nailart.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Classname my
* @Description TODO
* @Date 2025/7/10 18:50
* @Created by 21616
*/
@Configuration
public class MyBatisPlusConfig {
/**
* 分页插件配置
*
* @return MybatisPlusInterceptor
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 向MyBatis-Plus的过滤器链中添加分页拦截器需要设置数据库类型主要用于分页方言
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

View File

@ -0,0 +1,37 @@
package com.nailart.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @Description redis配置类
* @Classname RedisConfig
* @Date 2025/7/10 22:23
* @Created by 21616
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 设置键的序列化方式
template.setKeySerializer(new StringRedisSerializer());
// 设置值的序列化方式
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// 设置哈希键的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
// 设置哈希值的序列化方式
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}

View File

@ -0,0 +1,131 @@
package com.nailart.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.nailart.dataobj.PortfolioDO;
import com.nailart.enums.RespCodeEnum;
import com.nailart.service.PortfolioService;
import com.nailart.vo.RespVO;
import org.apache.ibatis.annotations.Param;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @Classname PortfolioController
* @Description 作品集控制类
* @Date 2025/7/10 18:48
* @Created by 21616
*/
@RestController
@RequestMapping("/portfolio")
public class PortfolioController {
private final PortfolioService portfolioService;
public PortfolioController(PortfolioService portfolioService) {
this.portfolioService = portfolioService;
}
@GetMapping("/list")
public RespVO getPortfolioList(@RequestParam("page") int page, @RequestParam("size") int size) {
RespVO respVO = new RespVO();
if (page <= 0 || size <= 0) {
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
respVO.setMsg(RespCodeEnum.BAD_REQUEST.getMessage());
return respVO;
}
try {
IPage<PortfolioDO> portfolioDOIPage = portfolioService.listByPage(page, size);
respVO.setCode(RespCodeEnum.SUCCESS.getCode());
respVO.setMsg(RespCodeEnum.SUCCESS.getMessage());
respVO.setData(portfolioDOIPage);
} catch (Exception e) {
respVO.setCode(RespCodeEnum.INTERNAL_SERVER_ERROR.getCode());
respVO.setMsg(RespCodeEnum.INTERNAL_SERVER_ERROR.getMessage());
}
return respVO;
}
@PostMapping("/add")
public RespVO addPortfolio(PortfolioDO portfolioDO) {
RespVO respVO = new RespVO();
if (portfolioDO == null) {
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
respVO.setMsg(RespCodeEnum.BAD_REQUEST.getMessage());
return respVO;
}
try {
Integer i = portfolioService.addPortfolio(portfolioDO);
respVO.setCode(RespCodeEnum.SUCCESS.getCode());
respVO.setMsg(RespCodeEnum.SUCCESS.getMessage());
respVO.setData(i);
} catch (Exception e) {
respVO.setCode(RespCodeEnum.INTERNAL_SERVER_ERROR.getCode());
respVO.setMsg(RespCodeEnum.INTERNAL_SERVER_ERROR.getMessage());
}
return respVO;
}
@PostMapping("/update")
public RespVO updatePortfolio(PortfolioDO portfolioDO) {
RespVO respVO = new RespVO();
if (portfolioDO == null) {
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
respVO.setMsg(RespCodeEnum.BAD_REQUEST.getMessage());
return respVO;
}
try {
Integer i = portfolioService.updatePortfolio(portfolioDO);
respVO.setCode(RespCodeEnum.SUCCESS.getCode());
respVO.setMsg(RespCodeEnum.SUCCESS.getMessage());
respVO.setData(i);
} catch (Exception e) {
respVO.setCode(RespCodeEnum.INTERNAL_SERVER_ERROR.getCode());
respVO.setMsg(RespCodeEnum.INTERNAL_SERVER_ERROR.getMessage());
}
return respVO;
}
@PostMapping("/delete")
public RespVO deletePortfolio(@RequestParam("id") Integer id) {
RespVO respVO = new RespVO();
if (id == null) {
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
respVO.setMsg(RespCodeEnum.BAD_REQUEST.getMessage());
return respVO;
}
try {
Integer i = portfolioService.deletePortfolio(id);
respVO.setCode(RespCodeEnum.SUCCESS.getCode());
respVO.setMsg(RespCodeEnum.SUCCESS.getMessage());
respVO.setData(i);
} catch (Exception e) {
respVO.setCode(RespCodeEnum.INTERNAL_SERVER_ERROR.getCode());
respVO.setMsg(RespCodeEnum.INTERNAL_SERVER_ERROR.getMessage());
}
return respVO;
}
@PostMapping("/addLoves")
public RespVO addLoves(@RequestParam("id") Integer id,@RequestParam Integer changes) {
RespVO respVO = new RespVO();
if (id == null) {
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
respVO.setMsg(RespCodeEnum.BAD_REQUEST.getMessage());
return respVO;
}
try {
Integer i = portfolioService.addLoves(id, changes);
respVO.setCode(RespCodeEnum.SUCCESS.getCode());
respVO.setMsg(RespCodeEnum.SUCCESS.getMessage());
respVO.setData(i);
} catch (Exception e) {
respVO.setCode(RespCodeEnum.INTERNAL_SERVER_ERROR.getCode());
respVO.setMsg(RespCodeEnum.INTERNAL_SERVER_ERROR.getMessage());
}
return respVO;
}
}

View File

@ -0,0 +1,54 @@
package com.nailart.dataobj;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.data.relational.core.mapping.Table;
@Data
@TableName("portfolio_info")
public class PortfolioDO {
/**
* id
*/
private Long id;
/**
* 标题
*/
private String title;
/**
* 描述
*/
private String description;
/**
* 点赞数
*/
private Integer loves;
/**
* 图片地址
*/
private String imgUrl;
/**
* 图片文件名
*/
private String imgFilename;
/**
* 标签
*/
private String tag;
/**
* 标签样式
*/
private String tagStyle;
/**
* 创建时间
*/
private String createTime;
/**
* 状态
*/
private Integer status;
/**
* 价格
*/
private Integer amt;
}

View File

@ -0,0 +1,33 @@
package com.nailart.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
* @Description 日志实体类
* @Classname RequestLogDTO
* @Date 2025/7/11 0:04
* @Created by 21616
*/
@Data
public class RequestLogDTO {
// 请求IP
private String ip;
// IP对应的城市
private String city;
// 请求时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS", timezone = "GMT+8")
private Date requestTime;
// 请求方法类名+方法名
private String method;
// 请求URL
private String url;
// HTTP方法GET/POST等
private String httpMethod;
// 响应时间(ms)
private long responseTime;
}

View File

@ -0,0 +1,80 @@
package com.nailart.enums;
import lombok.Getter;
/**
* @Description 统一响应状态码枚举类
* @Classname ResptCodeEnum
* @Date 2025/7/10 22:34
* @Created by 21616
*/
@Getter
public enum RespCodeEnum {
// ===================== 成功状态码 =====================
/** 成功 */
SUCCESS(200, "操作成功"),
/** 创建成功 */
CREATED(201, "资源创建成功"),
/** 无内容(适用于删除成功等场景) */
NO_CONTENT(204, "操作成功,无返回内容"),
// ===================== 客户端错误 =====================
/** 请求参数错误 */
BAD_REQUEST(400, "请求参数格式错误或不完整"),
/** 未授权(未登录) */
UNAUTHORIZED(401, "请先登录"),
/** 权限不足 */
FORBIDDEN(403, "没有权限执行该操作"),
/** 资源不存在 */
NOT_FOUND(404, "请求的资源不存在"),
/** 请求方法不支持 */
METHOD_NOT_ALLOWED(405, "不支持的请求方法如用GET访问POST接口"),
/** 请求频率过高 */
TOO_MANY_REQUESTS(429, "请求过于频繁,请稍后再试"),
// ===================== 服务器错误 =====================
/** 服务器内部错误 */
INTERNAL_SERVER_ERROR(500, "服务器内部错误,请联系管理员"),
/** 服务暂不可用 */
SERVICE_UNAVAILABLE(503, "服务暂时不可用,请稍后再试"),
/** 数据库错误 */
DATABASE_ERROR(5001, "数据库操作失败"),
/** 缓存错误 */
CACHE_ERROR(5002, "缓存操作失败"),
// ===================== 业务自定义错误 =====================
/** 数据重复(如新增已存在的记录) */
DATA_DUPLICATE(601, "数据已存在,请勿重复添加"),
/** 数据验证失败(如字段格式不符合要求) */
DATA_VALIDATION_FAILED(602, "数据验证失败,请检查输入"),
/** 第三方接口调用失败 */
THIRD_PARTY_SERVICE_ERROR(603, "第三方服务调用失败");
/** 状态码 */
private final int code;
/** 描述信息 */
private final String message;
// 构造方法
RespCodeEnum(int code, String message) {
this.code = code;
this.message = message;
}
/**
* 根据状态码获取枚举实例可选工具方法
*/
public static RespCodeEnum getByCode(int code) {
for (RespCodeEnum resultCode : values()) {
if (resultCode.code == code) {
return resultCode;
}
}
return null;
}
}

View File

@ -0,0 +1,9 @@
package com.nailart.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nailart.dataobj.PortfolioDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PortfolioMapper extends BaseMapper<PortfolioDO> {
}

View File

@ -0,0 +1,17 @@
package com.nailart.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.nailart.dataobj.PortfolioDO;
public interface PortfolioService extends IService<PortfolioDO> {
IPage<PortfolioDO> listByPage(int page, int size);
Integer addPortfolio(PortfolioDO portfolioDO);
Integer updatePortfolio(PortfolioDO portfolioDO);
Integer deletePortfolio(Integer id);
Integer addLoves(Integer id, Integer changes);
}

View File

@ -0,0 +1,61 @@
package com.nailart.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nailart.dataobj.PortfolioDO;
import com.nailart.mapper.PortfolioMapper;
import com.nailart.service.PortfolioService;
import org.springframework.stereotype.Service;
/**
*
* @Author: xiaowang
* @Date: 2021/5/10 14:48
*/
@Service
public class PortfolioServiceImpl extends ServiceImpl<PortfolioMapper, PortfolioDO> implements PortfolioService {
private final PortfolioMapper portfolioMapper;
public PortfolioServiceImpl(PortfolioMapper portfolioMapper) {
this.portfolioMapper = portfolioMapper;
}
/**
*
* @param page
* @param size
* @return
*/
@Override
public IPage<PortfolioDO> listByPage(int page, int size) {
Page<PortfolioDO> objectPage = new Page<>(page, size);
return portfolioMapper.selectPage(objectPage, null);
}
@Override
public Integer addPortfolio(PortfolioDO portfolioDO) {
return portfolioMapper.insert(portfolioDO);
}
@Override
public Integer updatePortfolio(PortfolioDO portfolioDO) {
return portfolioMapper.updateById(portfolioDO);
}
@Override
public Integer deletePortfolio(Integer id) {
return portfolioMapper.deleteById(id);
}
@Override
public Integer addLoves(Integer id, Integer changes) {
PortfolioDO portfolioDO = portfolioMapper.selectById(id);
if (portfolioDO == null) {
return 0;
}
portfolioDO.setLoves(portfolioDO.getLoves() + changes);
return portfolioMapper.updateById(portfolioDO);
}
}

View File

@ -0,0 +1,313 @@
package com.nailart.utils;
/**
* @Classname AliyunOSSUtils
* @Description 阿里云对象存储工具类
* @Date 2025/7/10 19:11
* @Created by 21616
*/
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.net.URL;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
* 阿里云OSS操作工具类
*/
@Component
public class AliyunOSSUtils {
private static final Logger logger = LoggerFactory.getLogger(AliyunOSSUtils.class);
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.access-key-id}")
private String accessKeyId;
@Value("${aliyun.oss.access-key-secret}")
private String accessKeySecret;
@Value("${aliyun.oss.bucket-name}")
private String bucketName;
@Value("${aliyun.oss.file-host}")
private String fileHost;
/**
* 上传文件到OSS
*
* @param file 上传的文件
* @param directory 存储目录
* @return 文件访问URL
*/
public String uploadFile(MultipartFile file, String directory) {
OSS ossClient = null;
try {
// 生成文件名
String originalFilename = file.getOriginalFilename();
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = UUID.randomUUID().toString().replace("-", "") + fileExtension;
// 构建完整路径
if (directory != null && !directory.isEmpty()) {
if (!directory.endsWith("/")) {
directory = directory + "/";
}
fileName = directory + fileName;
}
// 创建OSSClient实例
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 上传文件
ossClient.putObject(bucketName, fileName, new ByteArrayInputStream(file.getBytes()));
// 返回访问URL
return fileHost + "/" + fileName;
} catch (Exception e) {
logger.error("上传文件失败: {}", e.getMessage(), e);
return null;
} finally {
// 关闭OSSClient
if (ossClient != null) {
ossClient.shutdown();
}
}
}
/**
* 上传本地文件到OSS
*
* @param filePath 文件路径
* @param targetFileName 目标文件名
* @return 文件访问URL
*/
public String uploadLocalFile(String filePath, String targetFileName) {
OSS ossClient = null;
try {
// 创建OSSClient实例
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 上传文件
ossClient.putObject(bucketName, targetFileName, new File(filePath));
// 返回访问URL
return fileHost + "/" + targetFileName;
} catch (Exception e) {
logger.error("上传本地文件失败: {}", e.getMessage(), e);
return null;
} finally {
// 关闭OSSClient
if (ossClient != null) {
ossClient.shutdown();
}
}
}
/**
* 下载文件
*
* @param objectName 对象名称
* @param localFilePath 本地文件路径
* @return 是否下载成功
*/
public boolean downloadFile(String objectName, String localFilePath) {
OSS ossClient = null;
try {
// 创建OSSClient实例
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 下载OSS文件到本地文件
ossClient.getObject(new GetObjectRequest(bucketName, objectName), new File(localFilePath));
return true;
} catch (Exception e) {
logger.error("下载文件失败: {}", e.getMessage(), e);
return false;
} finally {
// 关闭OSSClient
if (ossClient != null) {
ossClient.shutdown();
}
}
}
/**
* 删除文件
*
* @param objectName 对象名称
* @return 是否删除成功
*/
public boolean deleteFile(String objectName) {
OSS ossClient = null;
try {
// 创建OSSClient实例
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 删除文件
ossClient.deleteObject(bucketName, objectName);
return true;
} catch (Exception e) {
logger.error("删除文件失败: {}", e.getMessage(), e);
return false;
} finally {
// 关闭OSSClient
if (ossClient != null) {
ossClient.shutdown();
}
}
}
/**
* 批量删除文件
*
* @param objectNames 对象名称列表
* @return 删除结果
*/
public DeleteObjectsResult deleteFiles(List<String> objectNames) {
OSS ossClient = null;
try {
// 创建OSSClient实例
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 批量删除文件
DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(bucketName)
.withKeys(objectNames);
return ossClient.deleteObjects(deleteObjectsRequest);
} catch (Exception e) {
logger.error("批量删除文件失败: {}", e.getMessage(), e);
return null;
} finally {
// 关闭OSSClient
if (ossClient != null) {
ossClient.shutdown();
}
}
}
/**
* 获取文件URL带签名用于临时访问
*
* @param objectName 对象名称
* @param expires 过期时间
* @return 带签名的URL
*/
public String getSignedUrl(String objectName, long expires) {
OSS ossClient = null;
try {
// 创建OSSClient实例
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 设置URL过期时间
Date expiration = new Date(System.currentTimeMillis() + expires * 1000);
// 生成签名URL
URL url = ossClient.generatePresignedUrl(bucketName, objectName, expiration);
return url.toString();
} catch (Exception e) {
logger.error("获取签名URL失败: {}", e.getMessage(), e);
return null;
} finally {
// 关闭OSSClient
if (ossClient != null) {
ossClient.shutdown();
}
}
}
/**
* 判断文件是否存在
*
* @param objectName 对象名称
* @return 是否存在
*/
public boolean doesObjectExist(String objectName) {
OSS ossClient = null;
try {
// 创建OSSClient实例
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 判断文件是否存在
return ossClient.doesObjectExist(bucketName, objectName);
} catch (Exception e) {
logger.error("检查文件是否存在失败: {}", e.getMessage(), e);
return false;
} finally {
// 关闭OSSClient
if (ossClient != null) {
ossClient.shutdown();
}
}
}
/**
* 获取文件元信息
*
* @param objectName 对象名称
* @return 文件元信息
*/
public ObjectMetadata getObjectMetadata(String objectName) {
OSS ossClient = null;
try {
// 创建OSSClient实例
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 获取文件元信息
return ossClient.getObjectMetadata(bucketName, objectName);
} catch (Exception e) {
logger.error("获取文件元信息失败: {}", e.getMessage(), e);
return null;
} finally {
// 关闭OSSClient
if (ossClient != null) {
ossClient.shutdown();
}
}
}
/**
* 列出指定目录下的文件
*
* @param prefix 前缀目录
* @param maxKeys 最大返回数量
* @return 文件列表
*/
public ListObjectsV2Result listObjects(String prefix, int maxKeys) {
OSS ossClient = null;
try {
// 创建OSSClient实例
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 构造ListObjects请求
ListObjectsV2Request listObjectsV2Request = new ListObjectsV2Request();
listObjectsV2Request.setBucketName(bucketName);
listObjectsV2Request.setPrefix(prefix);
listObjectsV2Request.setMaxKeys(maxKeys);
// 列出文件
return ossClient.listObjectsV2(listObjectsV2Request);
} catch (Exception e) {
logger.error("列出文件失败: {}", e.getMessage(), e);
return null;
} finally {
// 关闭OSSClient
if (ossClient != null) {
ossClient.shutdown();
}
}
}
}

View File

@ -0,0 +1,199 @@
package com.nailart.utils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Date;
/**
* 日期处理工具类基于Java 8+的java.time包
*/
public class DateUtils {
// 默认日期格式
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
// 默认日期时间格式
public static final String DEFAULT_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
// 默认时间格式
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
// 日期格式化器
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT);
// 日期时间格式化器
private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_DATETIME_FORMAT);
// 时间格式化器
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT);
/**
* 获取当前日期yyyy-MM-dd
* @return 当前日期字符串
*/
public static String getCurrentDate() {
return LocalDate.now().format(DATE_FORMATTER);
}
/**
* 获取当前日期时间yyyy-MM-dd HH:mm:ss
* @return 当前日期时间字符串
*/
public static String getCurrentDateTime() {
return LocalDateTime.now().format(DATETIME_FORMATTER);
}
/**
* 获取当前时间HH:mm:ss
* @return 当前时间字符串
*/
public static String getCurrentTime() {
return LocalTime.now().format(TIME_FORMATTER);
}
/**
* 日期转字符串
* @param date 日期
* @param pattern 格式模式
* @return 格式化后的日期字符串
*/
public static String formatDate(LocalDate date, String pattern) {
return date.format(DateTimeFormatter.ofPattern(pattern));
}
/**
* 日期时间转字符串
* @param dateTime 日期时间
* @param pattern 格式模式
* @return 格式化后的日期时间字符串
*/
public static String formatDateTime(LocalDateTime dateTime, String pattern) {
return dateTime.format(DateTimeFormatter.ofPattern(pattern));
}
/**
* 字符串转日期
* @param dateStr 日期字符串
* @param pattern 格式模式
* @return 解析后的LocalDate
*/
public static LocalDate parseDate(String dateStr, String pattern) {
return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(pattern));
}
/**
* 字符串转日期时间
* @param dateTimeStr 日期时间字符串
* @param pattern 格式模式
* @return 解析后的LocalDateTime
*/
public static LocalDateTime parseDateTime(String dateTimeStr, String pattern) {
return LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ofPattern(pattern));
}
/**
* 计算两个日期之间的天数差
* @param startDate 开始日期
* @param endDate 结束日期
* @return 天数差正数表示结束日期晚于开始日期
*/
public static long daysBetween(LocalDate startDate, LocalDate endDate) {
return ChronoUnit.DAYS.between(startDate, endDate);
}
/**
* 计算两个日期时间之间的小时差
* @param startDateTime 开始日期时间
* @param endDateTime 结束日期时间
* @return 小时差
*/
public static long hoursBetween(LocalDateTime startDateTime, LocalDateTime endDateTime) {
return ChronoUnit.HOURS.between(startDateTime, endDateTime);
}
/**
* 计算两个日期时间之间的分钟差
* @param startDateTime 开始日期时间
* @param endDateTime 结束日期时间
* @return 分钟差
*/
public static long minutesBetween(LocalDateTime startDateTime, LocalDateTime endDateTime) {
return ChronoUnit.MINUTES.between(startDateTime, endDateTime);
}
/**
* 在指定日期上增加/减少天数
* @param date 基准日期
* @param days 增加(正数)/减少(负数)的天数
* @return 计算后的日期
*/
public static LocalDate plusDays(LocalDate date, int days) {
return date.plusDays(days);
}
/**
* 在指定日期时间上增加/减少小时
* @param dateTime 基准日期时间
* @param hours 增加(正数)/减少(负数)的小时
* @return 计算后的日期时间
*/
public static LocalDateTime plusHours(LocalDateTime dateTime, int hours) {
return dateTime.plusHours(hours);
}
/**
* 在指定日期时间上增加/减少分钟
* @param dateTime 基准日期时间
* @param minutes 增加(正数)/减少(负数)的分钟
* @return 计算后的日期时间
*/
public static LocalDateTime plusMinutes(LocalDateTime dateTime, int minutes) {
return dateTime.plusMinutes(minutes);
}
/**
* Date转LocalDateTime
* @param date Date对象
* @return LocalDateTime对象
*/
public static LocalDateTime toLocalDateTime(Date date) {
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
}
/**
* LocalDateTime转Date
* @param dateTime LocalDateTime对象
* @return Date对象
*/
public static Date toDate(LocalDateTime dateTime) {
return Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
}
/**
* 获取一天的开始时间00:00:00
* @param date 日期
* @return 当天开始时间的LocalDateTime
*/
public static LocalDateTime getStartOfDay(LocalDate date) {
return date.atStartOfDay();
}
/**
* 获取一天的结束时间23:59:59
* @param date 日期
* @return 当天结束时间的LocalDateTime
*/
public static LocalDateTime getEndOfDay(LocalDate date) {
return LocalDateTime.of(date, LocalTime.MAX);
}
/**
* 判断日期是否在范围内
* @param targetDate 目标日期
* @param startDate 开始日期
* @param endDate 结束日期
* @return true-在范围内false-不在范围内
*/
public static boolean isDateInRange(LocalDate targetDate, LocalDate startDate, LocalDate endDate) {
return !targetDate.isBefore(startDate) && !targetDate.isAfter(endDate);
}
}

View File

@ -0,0 +1,59 @@
package com.nailart.utils;
import cn.hutool.core.io.resource.ResourceUtil;
import org.lionsoul.ip2region.xdb.Searcher;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
/**
* @Description IP 解析工具类,使用 ip2region 实现 IP 地址解析城市功能
* @Classname IpAddressUtil
* @Date 2025/7/11 0:04
* @Created by 21616
*/
@Component
public class IpAddressUtil {
private static Searcher searcher;
// 初始化IP数据库
static {
try {
// 从资源文件中读取ip2region.xdb
InputStream inputStream = ResourceUtil.getStream("ip2region.xdb");
byte[] dbContent = new byte[inputStream.available()];
inputStream.read(dbContent);
searcher = Searcher.newWithBuffer(dbContent);
} catch (Exception e) {
throw new RuntimeException("初始化IP地址解析器失败", e);
}
}
/**
* 根据IP获取城市信息
*
* @param ip IP地址
* @return 城市信息格式国家|区域|省份|城市|ISP
*/
public String getCityInfo(String ip) {
if (ip == null || ip.isEmpty()) {
return "未知IP";
}
// 本地IP处理
if ("127.0.0.1".equals(ip) || "localhost".equals(ip)) {
return "本地主机";
}
try {
String result = searcher.search(ip);
return result != null ? result : "未知地址";
} catch (IOException e) {
return "解析失败";
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,665 @@
package com.nailart.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* Redis工具类封装常用的Redis操作
*/
@Component
public class RedisUtils {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
*
* @param key
* @param time 时间()
* @return true成功 false失败
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key获取过期时间
*
* @param key 不能为null
* @return 时间() 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key
* @return true存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(Arrays.asList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
*
* @param key
* @return
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key
* @param value
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key
* @param value
* @param time 时间() time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key
* @param delta 要增加几(大于0)
* @return 增加后的值
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key
* @param delta 要减少几(大于0)
* @return 减少后的值
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
*
* @param key 不能为null
* @param item 不能为null
* @return
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key
* @param map 对应多个键值
* @param time 时间()
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key
* @param item
* @param value
* @return true 成功 false 失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key
* @param item
* @param value
* @param time 时间() 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false 失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 不能为null
* @param item 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 不能为null
* @param item 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key
* @param item
* @param by 要增加几(大于0)
* @return 增加后的值
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key
* @param item
* @param by 要减少记(大于0)
* @return 减少后的值
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key
* @return Set集合
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key
* @param value
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key
* @param values 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key
* @param time 时间()
* @param values 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key
* @return 长度数值
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key
* @param values 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key
* @param start 开始
* @param end 结束 0 -1代表所有值
* @return List集合
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key
* @return 长度数值
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key
* @param index 索引 index>=0时 0 表头1 第二个元素依次类推index<0时-1表尾-2倒数第二个元素依次类推
* @return 值对象
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key
* @param value
* @return 操作结果
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key
* @param value
* @param time 时间()
* @return 操作结果
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key
* @param value 值列表
* @return 操作结果
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key
* @param value 值列表
* @param time 时间()
* @return 操作结果
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key
* @param index 索引
* @param value
* @return 操作结果
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key
* @param count 移除多少个
* @param value
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ============================HyperLogLog=============================
/**
* 添加元素到HyperLogLog
*
* @param key
* @param values
* @return 添加结果
*/
public long pfAdd(String key, String... values) {
return redisTemplate.opsForHyperLogLog().add(key, (Object[]) values);
}
/**
* 获取HyperLogLog的基数估算值
*
* @param key
* @return 基数估算值
*/
public long pfCount(String key) {
return redisTemplate.opsForHyperLogLog().size(key);
}
/**
* 合并多个HyperLogLog
*
* @param destKey 目标键
* @param sourceKeys 源键
* @return 合并结果
*/
public long pfMerge(String destKey, String... sourceKeys) {
return redisTemplate.opsForHyperLogLog().union(destKey, sourceKeys);
}
// ============================BitMap=============================
/**
* 设置BitMap的位值
*
* @param key
* @param offset 偏移量
* @param value
* @return 设置结果
*/
public boolean setBit(String key, long offset, boolean value) {
return redisTemplate.opsForValue().setBit(key, offset, value);
}
/**
* 获取BitMap的位值
*
* @param key
* @param offset 偏移量
* @return 位值
*/
public boolean getBit(String key, long offset) {
return redisTemplate.opsForValue().getBit(key, offset);
}
/**
* 统计BitMap中为1的位的数量
*
* @param key
* @return 为1的位的数量
*/
public long bitCount(String key) {
return stringRedisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes()));
}
/**
* 统计BitMap中指定范围内为1的位的数量
*
* @param key
* @param start 起始字节
* @param end 结束字节
* @return 为1的位的数量
*/
public long bitCount(String key, long start, long end) {
return stringRedisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes(), start, end));
}
/**
* 对多个BitMap执行位运算
*
* @param op 运算类型
* @param destKey 目标键
* @param keys 源键
* @return 运算结果的长度
*/
public long bitOp(RedisStringCommands.BitOperation op, String destKey, String... keys) {
byte[][] bytes = new byte[keys.length][];
for (int i = 0; i < keys.length; i++) {
bytes[i] = keys[i].getBytes();
}
return stringRedisTemplate.execute((RedisCallback<Long>) con ->
con.bitOp(op, destKey.getBytes(), bytes));
}
}

View File

@ -0,0 +1,16 @@
package com.nailart.vo;
import lombok.Data;
/**
* @Description 所有的请求返回封装体
* @Classname RespVO
* @Date 2025/7/10 22:32
* @Created by 21616
*/
@Data
public class RespVO {
private Integer code;
private String msg;
private Object data;
}

View File

@ -1 +0,0 @@
spring.application.name=na-core

View File

@ -0,0 +1,76 @@
server:
port: 8090
servlet:
context-path: /api # 应用访问路径前缀
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 使用Druid连接池
driver-class-name: com.mysql.cj.jdbc.Driver # MySQL驱动类
url: jdbc:mysql://wcy111.top:3306/nailart?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC # 数据库连接地址
username: root # 数据库用户名
password: 122503wcy # 数据库密码
druid: # Druid连接池配置
initial-size: 5 # 初始化连接数
min-idle: 5 # 最小空闲连接数
max-active: 20 # 最大活跃连接数
max-wait: 60000 # 获取连接时的最大等待时间(毫秒)
time-between-eviction-runs-millis: 60000 # 间隔多久进行一次检测,检测需要关闭的空闲连接(毫秒)
min-evictable-idle-time-millis: 300000 # 一个连接在池中最小生存的时间(毫秒)
validation-query: SELECT 1 FROM DUAL # 验证连接是否有效的SQL语句
test-while-idle: true # 空闲时是否检测连接有效性
test-on-borrow: false # 借出连接时是否检测有效性
test-on-return: false # 归还连接时是否检测有效性
pool-prepared-statements: true # 是否缓存PreparedStatement
max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的PreparedStatement数量
filters: stat,wall,log4j # 配置监控统计拦截的filters
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 连接属性配置(合并SQL、慢SQL阈值)
# Redis配置
redis:
host: wcy111.top # Redis服务器地址
port: 6379 # Redis服务器端口
password: 122503wcy # Redis密码
timeout: 10000ms # 连接超时时间
database: 0 # 数据库索引
lettuce: # Lettuce连接池配置
pool:
max-active: 8 # 最大连接数
max-wait: -1ms # 最大阻塞等待时间(负数表示无限制)
max-idle: 8 # 最大空闲连接
min-idle: 0 # 最小空闲连接
# MyBatis-Plus配置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml # Mapper XML文件存放路径
type-aliases-package: com.nailart.do # 实体类别名包路径
global-config:
db-config:
id-type: auto # 主键生成策略(AUTO=数据库ID自增)
field-strategy: not_null # 字段验证策略(NOT_NULL=非NULL判断)
table-underline: false # 表名是否使用下划线命名
logic-delete-field: deleted # 全局逻辑删除字段名
logic-not-delete-value: 0 # 逻辑未删除值
logic-delete-value: 1 # 逻辑已删除值
configuration:
map-underscore-to-camel-case: false # 是否开启下划线转驼峰命名
cache-enabled: false # 是否开启二级缓存
call-setters-on-nulls: true # 查询结果是否返回null字段
jdbc-type-for-null: 'null' # 空值的JDBC类型映射
# 阿里云OSS配置
aliyun:
oss:
endpoint: oss-cn-chengdu.aliyuncs.com # OSS服务端点
access-key-id: LTAI5tLbzuS6P8rGQ4Rw9rN9 # 访问密钥ID
access-key-secret: R3zbklnTmEMks4HEiKRaxlXwBJvY8T # 访问密钥Secret
bucket-name: yuminailart # 存储桶名称
file-host: yuminailart.oss-cn-chengdu.aliyuncs.com # 文件主机地址
# 日志配置
logging:
level:
root: info # 根日志级别
com.example.mapper: debug # Mapper接口日志级别(调试用)
file:
name: logs/application.log # 日志文件存储路径

Binary file not shown.

View File

@ -8,6 +8,7 @@
"name": "na-frontend", "name": "na-frontend",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"axios": "^1.10.0",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"element-ui": "^2.15.14", "element-ui": "^2.15.14",
"less-loader": "^12.3.0", "less-loader": "^12.3.0",
@ -3442,6 +3443,12 @@
"babel-runtime": "6.x" "babel-runtime": "6.x"
} }
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/at-least-node": { "node_modules/at-least-node": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", "resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz",
@ -3490,6 +3497,17 @@
"postcss": "^8.1.0" "postcss": "^8.1.0"
} }
}, },
"node_modules/axios": {
"version": "1.10.0",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.10.0.tgz",
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-helper-vue-jsx-merge-props": { "node_modules/babel-helper-vue-jsx-merge-props": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmmirror.com/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz", "resolved": "https://registry.npmmirror.com/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz",
@ -3903,7 +3921,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@ -4238,6 +4255,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": { "node_modules/commander": {
"version": "8.3.0", "version": "8.3.0",
"resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz",
@ -5075,6 +5104,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
@ -5253,7 +5291,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.1", "call-bind-apply-helpers": "^1.0.1",
@ -5428,7 +5465,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -5438,7 +5474,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -5455,7 +5490,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0" "es-errors": "^1.3.0"
@ -5464,6 +5498,21 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escalade": { "node_modules/escalade": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
@ -6385,7 +6434,6 @@
"version": "1.15.9", "version": "1.15.9",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz", "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@ -6402,6 +6450,22 @@
} }
} }
}, },
"node_modules/form-data": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.3.tgz",
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
@ -6485,7 +6549,6 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@ -6522,7 +6585,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2", "call-bind-apply-helpers": "^1.0.2",
@ -6547,7 +6609,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"dunder-proto": "^1.0.1", "dunder-proto": "^1.0.1",
@ -6666,7 +6727,6 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -6732,7 +6792,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -6741,6 +6800,21 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hash-sum": { "node_modules/hash-sum": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-2.0.0.tgz", "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-2.0.0.tgz",
@ -6752,7 +6826,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@ -8024,7 +8097,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -8138,7 +8210,6 @@
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@ -8148,7 +8219,6 @@
"version": "2.1.35", "version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"mime-db": "1.52.0" "mime-db": "1.52.0"
@ -9804,6 +9874,12 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/prr": { "node_modules/prr": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz",

View File

@ -8,6 +8,7 @@
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"axios": "^1.10.0",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"element-ui": "^2.15.14", "element-ui": "^2.15.14",
"less-loader": "^12.3.0", "less-loader": "^12.3.0",

View File

@ -6,9 +6,9 @@
<!-- 中间内容区包裹路由视图控制动画范围 --> <!-- 中间内容区包裹路由视图控制动画范围 -->
<div class="content-container"> <div class="content-container">
<!-- 根据路由方向动态切换过渡类 --> <!-- 根据路由方向动态切换过渡类 -->
<transition :name="transitionName" mode="out-in"> <!-- <transition name="van-slide-left"> -->
<router-view></router-view> <router-view></router-view>
</transition> <!-- </transition> -->
</div> </div>
<!-- 固定底部TabBar --> <!-- 固定底部TabBar -->
@ -48,22 +48,12 @@ export default {
/* 固定顶部导航栏 */ /* 固定顶部导航栏 */
.nav-bar { .nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
height: 45px; height: 45px;
background: #fff; background: #fff;
} }
/* 固定底部TabBar */ /* 固定底部TabBar */
.tab-bar { .tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
height: 50px; height: 50px;
background: #fff; background: #fff;
} }
@ -71,9 +61,7 @@ export default {
/* 中间内容区避开上下Bar的位置 */ /* 中间内容区避开上下Bar的位置 */
.content-container { .content-container {
position: relative; position: relative;
min-height: 100vh; height: calc(100vh - 110px);
padding-top: 50px;
padding-bottom: 60px;
overflow: hidden; overflow: hidden;
/* 防止内容溢出 */ /* 防止内容溢出 */
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="nav-bar"> <div class="nav-bar">
<van-nav-bar title="屿" left-text="" :placeholder="true" :safe-area-inset-top="true" z-index="99"> <van-nav-bar title="屿" left-text="" :placeholder="true" :safe-area-inset-top="true" z-index="50" :fixed="true">
<template #right> <template #right>
<van-icon name="search" size="18" /> <van-icon name="search" size="18" />
</template> </template>

View File

@ -2,8 +2,7 @@ import Vue from "vue";
import Vuex from "vuex"; import Vuex from "vuex";
import App from "./App.vue"; import App from "./App.vue";
import router from "./router"; import router from "./router";
import axios from './utils/request'
Vue.config.productionTip = false;
import { Tabbar, TabbarItem } from "vant"; import { Tabbar, TabbarItem } from "vant";
import { NavBar } from "vant"; import { NavBar } from "vant";
import { Toast } from "vant"; import { Toast } from "vant";
@ -14,7 +13,16 @@ import { Image as VanImage } from "vant";
import { Lazyload } from "vant"; import { Lazyload } from "vant";
import { Card } from "vant"; import { Card } from "vant";
import { Tag } from "vant"; import { Tag } from "vant";
import { Swipe, SwipeItem } from 'vant';
import { Loading } from 'vant';
import { NoticeBar } from 'vant';
import { Sticky } from 'vant';
Vue.use(Sticky);
Vue.use(NoticeBar);
Vue.use(Loading);
Vue.use(Swipe);
Vue.use(SwipeItem);
Vue.use(Tag); Vue.use(Tag);
Vue.use(Card); Vue.use(Card);
Vue.use(Lazyload); Vue.use(Lazyload);
@ -27,7 +35,10 @@ Vue.use(NavBar);
Vue.use(Tabbar); Vue.use(Tabbar);
Vue.use(TabbarItem); Vue.use(TabbarItem);
Vue.use(Vuex); Vue.use(Vuex);
Vue.config.productionTip = false;
// 全局挂载 axios
Vue.prototype.$axios = axios
new Vue({ new Vue({
router, router,
render: (h) => h(App), render: (h) => h(App),

View File

@ -1,284 +1,443 @@
<template> <template>
<div class="container"> <div class="page-container">
<div class="card-grid"> <!-- 使用原生 sticky 定位的公告栏 -->
<!-- 卡片列表 --> <div class="sticky-notice">
<div <van-notice-bar left-icon="volume-o" :scrollable="true" text="所有标价均为款式价格,如需延长需补差价喔~~~半贴¥90,浅贴¥130" />
v-for="(card, index) in cards"
:key="index"
class="card"
:class="{ 'card-active': activeCardIndex === index }"
@touchstart="handleTouchStart(index)"
@touchend="handleTouchEnd(index)"
@click="handleCardClick(card)"
>
<div class="card-image-container">
<img
:src="card.image"
:alt="card.title"
class="card-image"
>
<div
v-if="card.tag"
class="card-tag"
:style="card.tagStyle"
>
{{ card.tag }}
</div>
</div> </div>
<div class="card-content"> <!-- 内容容器 -->
<h2 class="card-title">{{ card.title }}</h2> <div class="cards-container">
<p class="card-description">{{ card.description }}</p> <div class="card-grid">
<!-- 卡片列表 -->
<div class="card-footer"> <div v-for="(card, index) in cards" :key="index" class="card"
<span class="card-meta"> :class="{ 'card-active': activeCardIndex === index }" @touchstart="handleTouchStart(index)"
<i class="fa fa-calendar-o mr-1"></i> {{ card.duration }} @touchend="handleTouchEnd(index)" @click="handleCardClick(card)">
</span>
<button <div class="card-image-container">
class="card-button" <van-swipe :autoplay="3000">
@click.stop="handleButtonClick(card)" <van-swipe-item v-for="(image, imgIndex) in card.images" :key="imgIndex">
> <van-image lazy-load fit="contain" :src="image">
了解更多 <template v-slot:loading>
</button> <van-loading type="spinner" size="20" />
</div> </template>
</van-image>
</van-swipe-item>
</van-swipe>
</div>
<div class="card-content">
<h2 class="card-title">{{ card.title }}</h2>
<p class="card-description">{{ card.description }}</p>
<div v-if="card.tag" class="card-tag">
<van-tag round type="primary">{{ card.tag }}</van-tag>
</div>
<div class="card-footer">
<span class="card-meta">
<span>款式价格:</span><i class="fa fa-calendar-o mr-1"></i> {{ card.amt }}
</span>
<div class="heart" @click.stop>
<label class="action-btn heart-label">
<input type="checkbox" class="heart-checkbox" v-model="card.checked"
@change="handleClickLove(card)">
<span class="heart-btn">
<span class="heart-icon"></span>
</span>
</label>
<span class="like-count">{{ card.loves }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- <div class="loading" v-show="loading">
<van-loading type="spinner" color="#1989fa" />
</div> -->
</div> </div>
</div>
</div> </div>
</div>
</template> </template>
<script> <script>
export default { export default {
name: 'PortfolioPage', name: 'PortfolioPage',
data() { data() {
return { return {
cards: [ cards: [],
{ activeCardIndex: -1,
title: "探索自然之美", page: 1,
description: "深入原始森林,感受大自然的鬼斧神工。这里有清澈的溪流、茂密的树木和各种珍稀野生动物。", pageSize: 2,
image: "https://picsum.photos/seed/card1/600/800", loading: false,
duration: "3天2晚", changes: [],
tag: "热门", totalPages: 0
tagStyle: { };
backgroundColor: "#F97316", },
color: "#fff" methods: {
} async getCarts(pageNum, size) {
try {
this.loading = true;
let res = await this.$axios.get('/portfolio/list', {
params: {
page: pageNum,
size: size
}
});
if (res.code !== 200 && res.data.code !== 200) {
const errorMsg = res.msg || res.data.msg || '获取数据失败';
this.$toast.fail(errorMsg);
return;
}
const records = res.data?.data?.records || [];
if (records.length === 0) {
this.$toast('没有更多数据了');
return;
}
const formattedCards = records.map(item => {
const images = item.imgFilename
? item.imgFilename.split('&&').map(filename => `${item.imgUrl}/${filename}`)
: [];
const tagStyle = item.tagStyle
? { backgroundColor: item.tagStyle.split('&&')[0], color: item.tagStyle.split('&&')[1] }
: { backgroundColor: "#eeeeee", color: "#fff" };
return {
id: item.id,
title: item.title,
description: item.description,
images,
duration: '',
tag: item.tag || '热门',
tagStyle,
loves: item.loves || 0,
changes: 0,
checked: false,
amt: item.amt || 0,
};
});
this.cards = [...this.cards, ...formattedCards];
this.loading = false;
this.totalPages = res.data.data.pages;
} catch (error) {
console.error('获取卡片数据失败:', error);
this.loading = false;
this.$toast.fail('网络错误,请稍后重试');
}
}, },
{ handleTouchStart(index) {
title: "城市探险之旅", this.activeCardIndex = index;
description: "穿梭于现代都市与历史古迹之间,体验城市独特的文化氛围和美食特色。",
image: "https://picsum.photos/seed/card2/600/800",
duration: "2天1晚",
tag: "热门",
tagStyle: {
backgroundColor: "#F97316",
color: "#fff"
}
}, },
{ handleTouchEnd() {
title: "美食文化体验", setTimeout(() => {
description: "跟随当地厨师学习传统美食的制作方法,品尝地道佳肴,感受美食背后的文化故事。", this.activeCardIndex = -1;
image: "https://picsum.photos/seed/card3/600/800", }, 300);
duration: "1天",
tag: "新品",
tagStyle: {
backgroundColor: "#22c55e",
color: "#fff"
}
}, },
{ handleCardClick(card) {
title: "海洋生态探索", console.log('点击了卡片:', card.title);
description: "潜入蔚蓝深海,探索神秘的海底世界,与海龟、珊瑚和热带鱼共舞。", },
image: "https://picsum.photos/seed/card4/600/800", handleClickLove(card) {
duration: "4天3晚", if (card.checked) {
tag: "推荐", card.loves++;
tagStyle: { card.changes++;
backgroundColor: "#3b82f6", } else {
color: "#fff" if (card.loves > 0) {
} card.loves--;
card.changes--;
}
}
this.changes.push({
id: card.id,
changes: card.changes
});
},
async savaLoveChanes() {
for (const change of this.changes) {
try {
await this.$axios.post('/portfolio/addLoves', null, {
params: {
id: change.id,
changes: change.changes
}
});
} catch (error) {
console.error('保存点赞变化失败:', error);
this.$toast.fail('保存点赞状态失败,请稍后重试');
}
}
},
handleScroll() {
const container = document.querySelector('.cards-container');
if (!container) return;
const scrollTop = container.scrollTop;
const clientHeight = container.clientHeight;
const scrollHeight = container.scrollHeight;
if (scrollTop + clientHeight >= scrollHeight - 50) {
if (!this.loading && this.page < this.totalPages) {
this.page++;
this.getCarts(this.page, this.pageSize || 2);
console.log('加载更多数据,当前页:', this.page);
}
}
} }
],
activeCardIndex: -1
};
},
methods: {
handleTouchStart(index) {
this.activeCardIndex = index;
}, },
mounted() {
handleTouchEnd() { this.getCarts(this.page, this.pageSize || 2);
setTimeout(() => { this.scrollContainer = document.querySelector('.cards-container');
this.activeCardIndex = -1; if (this.scrollContainer) {
}, 300); this.scrollHandler = () => this.handleScroll();
this.scrollContainer.addEventListener('scroll', this.scrollHandler);
}
}, },
beforeDestroy() {
handleCardClick(card) { if (this.scrollContainer && this.scrollHandler) {
console.log('点击了卡片:', card.title); this.scrollContainer.removeEventListener('scroll', this.scrollHandler);
}
}, },
beforeRouteLeave(to, from, next) {
handleButtonClick(card) { next();
console.log('点击了了解更多:', card.title); this.savaLoveChanes();
} }
},
mounted() {
//
const buttons = document.querySelectorAll('.card-button');
buttons.forEach(button => {
button.addEventListener('touchstart', () => {
button.classList.add('button-active');
});
button.addEventListener('touchend', () => {
setTimeout(() => {
button.classList.remove('button-active');
}, 200);
});
});
}
}; };
</script> </script>
<style scoped> <style lang="less" scoped>
/* 基础样式 */ //
* { @base-bg: #f9fafb;
margin: 0; @card-bg: white;
padding: 0; @text-primary: #1f2937;
box-sizing: border-box; @text-secondary: #6b7280;
font-family: 'Inter', system-ui, sans-serif; @accent-color: #3b82f6;
@accent-dark: #2563eb;
@danger-color: #ef4444;
@gray-light: #d1d5db;
@shadow-base: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
@shadow-active: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
//
.page-container {
display: flex;
flex-direction: column;
height: 100%;
max-width: 800px;
margin: 0 auto;
padding: 0 20px;
//
.sticky-notice {
position: sticky;
top: 0;
z-index: 100;
margin-bottom: 10px;
}
//
.cards-container {
overflow: auto;
padding-bottom: 20px;
//
.card-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
//
@media (max-width: 600px) {
grid-template-columns: 1fr;
}
//
.card {
background-color: @card-bg;
border-radius: 16px;
overflow: hidden;
box-shadow: @shadow-base;
transition: all 0.3s ease;
cursor: pointer;
//
&:hover {
transform: translateY(-5px);
box-shadow: @shadow-active;
}
&.card-active {
transform: translateY(-5px);
box-shadow: @shadow-active;
}
//
.card-image-container {
position: relative;
//
.card-tag {
padding: 4px 12px;
border-radius: 9999px;
font-size: 14px;
font-weight: 500;
}
}
//
.card-content {
padding: 20px;
position: relative;
//
.card-title {
font-size: 18px;
font-weight: bold;
color: @text-primary;
margin-bottom: 8px;
}
//
.card-description {
color: @text-secondary;
text-wrap: balance;
margin-bottom: 12px;
line-height: 1.5;
font-size: 14px;
}
//
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12px;
position: relative;
bottom: 0px;
//
.card-meta {
color: #9932CC;
font-weight: 500;
font-size: 20px;
background-color: #D8BFD8;
border-radius: 8px;
padding: 0 5px 0 5px;
//
.fa {
margin-right: 1px;
}
}
//
.heart {
display: flex;
align-items: center;
gap: 4px;
//
.like-count {
font-size: 13px;
color: @text-secondary;
}
//
.action-btn {
position: relative;
cursor: pointer;
}
//
.heart-checkbox {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
//
.heart-btn {
position: relative;
width: 10px;
height: 10px;
transition: transform 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
&:hover {
transform: scale(1.1);
}
}
//
.heart-icon {
position: absolute;
top: 0;
left: -5px;
width: 100%;
height: 100%;
transition: color 0.3s;
display: flex;
align-items: center;
justify-content: center;
&::before {
content: '❤';
font-size: 1.25rem;
color: @gray-light;
transition: color 0.3s, transform 0.3s;
position: relative;
line-height: 1;
vertical-align: middle;
transform-origin: center;
}
}
//
.heart-checkbox:checked~.heart-btn .heart-icon::before {
color: @danger-color;
animation: heartBeat 0.5s cubic-bezier(0.17, 0.89, 0.32, 1.49);
}
}
}
}
}
}
//
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
}
}
} }
body { //
background-color: #f9fafb; @keyframes heartBeat {
min-height: 100vh; 0% {
padding: 20px 0; transform: scale(1);
} }
/* 容器样式 - 限制最大宽度为800px双列布局的合理宽度 */ 25% {
.container { transform: scale(1.3);
max-width: 800px; /* 双列布局的最佳最大宽度 */ }
margin: 0 auto;
padding: 0 20px;
}
.page-title { 50% {
font-size: clamp(1.5rem, 3vw, 2rem); transform: scale(1);
font-weight: bold; }
color: #1f2937;
margin-bottom: 32px;
text-align: center;
}
/* 卡片网格布局 - 固定双列 */ 75% {
.card-grid { transform: scale(1.2);
display: grid; }
grid-template-columns: repeat(2, 1fr); /* 固定双列布局 */
gap: 24px; /* 卡片间距 */
}
/* 移动端适配 - 屏幕小于600px时改为单列 */ 100% {
@media (max-width: 600px) { transform: scale(1);
.card-grid { }
grid-template-columns: 1fr;
}
}
/* 卡片样式 */
.card {
background-color: white;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
cursor: pointer;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.card-active {
transform: translateY(-5px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
/* 卡片图片 */
.card-image-container {
position: relative;
}
.card-image {
width: 100%;
display: block;
/* 3:4 比例 */
aspect-ratio: 3 / 4;
object-fit: cover;
}
/* 卡片标签 */
.card-tag {
position: absolute;
top: 16px;
right: 16px;
padding: 4px 12px;
border-radius: 9999px;
font-size: 14px;
font-weight: 500;
}
/* 卡片内容 */
.card-content {
padding: 20px;
}
.card-title {
font-size: 18px;
font-weight: bold;
color: #1f2937;
margin-bottom: 8px;
}
.card-description {
color: #6b7280;
text-wrap: balance;
margin-bottom: 12px;
line-height: 1.5;
font-size: 14px;
}
/* 卡片底部 */
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12px;
}
.card-meta {
color: #3b82f6;
font-weight: 500;
font-size: 13px;
}
/* 按钮样式 */
.card-button {
background-color: #3b82f6;
color: white;
border: none;
padding: 6px 12px;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
font-size: 13px;
}
.card-button:hover {
background-color: #2563eb;
transform: scale(1.05);
}
.button-active {
transform: scale(0.95);
} }
</style> </style>

View File

@ -1,6 +1,41 @@
<template> <template>
<div class="sample-collection-page"> <div class="sample-collection-page">
<h1>Sample Collection Page</h1> <div class="card-grid">
<!-- 卡片列表 -->
<div v-for="(card, index) in cards" :key="index" class="card"
:class="{ 'card-active': activeCardIndex === index }" @touchstart="handleTouchStart(index)"
@touchend="handleTouchEnd(index)" @click="handleCardClick(card)">
<div class="card-image-container">
<van-swipe :autoplay="3000">
<van-swipe-item v-for="(image, imgIndex) in card.images" :key="imgIndex">
<van-image :src="image" lazy-load>
<template v-slot:loading>
<van-loading type="spinner" size="20" />
</template>
</van-image>
</van-swipe-item>
</van-swipe>
<div v-if="card.tag" class="card-tag" :style="card.tagStyle">
{{ card.tag }}
</div>
</div>
<div class="card-content">
<h2 class="card-title">{{ card.title }}</h2>
<p class="card-description">{{ card.description }}</p>
<div class="card-footer">
<div class="employee">
<van-tag round type="primary" v-for="employee in card.employee.split('&&')"
:key="employee">{{ employee
}}</van-tag>
</div>
</div>
</div>
</div>
</div>
</div> </div>
</template> </template>
@ -10,16 +45,277 @@ export default {
components: {}, components: {},
props: {}, props: {},
data() { data() {
return { return {
cards: [
{
title: "探索自然之美",
description: "深入原始森林,感受大自然的鬼斧神工。这里有清澈的溪流、茂密的树木和各种珍稀野生动物。",
images: ["https://picsum.photos/seed/card1/600/800", "https://picsum.photos/seed/card2/600/800"],
tag: "热门",
tagStyle: {
backgroundColor: "#F97316",
color: "#fff"
},
status: 0,
employee: "小吕&&寒冰射手",
neddTime: 2.0
}
],
activeCardIndex: -1
}; };
}, },
watch: {}, watch: {},
computed: { computed: {
},
methods: {
handleTouchStart(index) {
this.activeCardIndex = index;
},
handleTouchEnd() {
setTimeout(() => {
this.activeCardIndex = -1;
}, 300);
},
handleCardClick(card) {
console.log('点击了卡片:', card.title);
},
handleButtonClick(card) {
console.log('点击了了解更多:', card.title);
},
},
mounted() {
//
const buttons = document.querySelectorAll('.card-button');
buttons.forEach(button => {
button.addEventListener('touchstart', () => {
button.classList.add('button-active');
});
button.addEventListener('touchend', () => {
setTimeout(() => {
button.classList.remove('button-active');
}, 200);
});
});
}, },
methods: {},
created() { }, created() { },
mounted() { }
}; };
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped>
//
@base-bg: #f9fafb;
@card-bg: white;
@text-primary: #1f2937;
@text-secondary: #6b7280;
@accent-color: #3b82f6;
@accent-dark: #2563eb;
@danger-color: #ef4444;
@gray-light: #d1d5db;
@shadow-base: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
@shadow-active: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
//
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Inter', system-ui, sans-serif;
}
body {
background-color: @base-bg;
min-height: 100vh;
padding: 20px 0;
}
//
.sample-collection-page {
max-width: 800px;
margin: 0 auto;
padding: 0 20px;
.page-title {
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: bold;
color: @text-primary;
margin-bottom: 32px;
text-align: center;
}
//
.card-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
//
@media (max-width: 600px) {
grid-template-columns: 1fr;
}
}
//
.card {
background-color: @card-bg;
border-radius: 16px;
overflow: hidden;
box-shadow: @shadow-base;
transition: all 0.3s ease;
cursor: pointer;
&:hover {
transform: translateY(-5px);
box-shadow: @shadow-active;
}
&.card-active {
transform: translateY(-5px);
box-shadow: @shadow-active;
}
//
.card-image-container {
position: relative;
//
.card-tag {
position: absolute;
top: 16px;
right: 16px;
padding: 4px 12px;
border-radius: 9999px;
font-size: 14px;
font-weight: 500;
}
}
//
.card-content {
padding: 20px;
.card-title {
font-size: 18px;
font-weight: bold;
color: @text-primary;
margin-bottom: 8px;
}
.card-description {
color: @text-secondary;
text-wrap: balance;
margin-bottom: 12px;
line-height: 1.5;
font-size: 14px;
}
//
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12px;
.card-meta {
color: @accent-color;
font-weight: 500;
font-size: 13px;
.fa {
margin-right: 1px;
}
}
//
.heart {
display: flex;
align-items: center;
gap: 4px;
.like-count {
font-size: 13px;
color: @text-secondary;
}
.action-btn {
position: relative;
cursor: pointer;
}
.heart-checkbox {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.heart-btn {
position: relative;
width: 10px;
height: 10px;
transition: transform 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
&:hover {
transform: scale(1.1);
}
}
.heart-icon {
position: absolute;
top: 0;
left: -5px;
width: 100%;
height: 100%;
transition: color 0.3s;
display: flex;
align-items: center;
justify-content: center;
&::before {
content: '❤';
font-size: 1.25rem;
color: @gray-light;
transition: color 0.3s, transform 0.3s;
position: relative;
line-height: 1;
vertical-align: middle;
transform-origin: center;
}
}
//
.heart-checkbox:checked~.heart-btn .heart-icon::before {
color: @danger-color;
animation: heartBeat 0.5s cubic-bezier(0.17, 0.89, 0.32, 1.49);
}
}
}
}
.employee {
display: flex;
flex-wrap: wrap;
gap: 8px;
.van-tag {
font-size: 12px;
padding: 4px 8px;
border-radius: 9999px;
background-color: #7B68EE;
color: white;
}
}
}
}
</style>

View File

@ -0,0 +1,66 @@
import axios from 'axios'
// 创建 axios 实例
const service = axios.create({
baseURL: "http://127.0.0.1:8090/api",
timeout: 10000
})
// 存储请求的防抖定时器key: 请求标识, value: 定时器ID
const debounceTimers = new Map();
// 防抖时间(毫秒,可根据需求调整)
const DEBOUNCE_DELAY = 500;
// 生成请求唯一标识URL + 参数 + 方法)
const getRequestKey = (config) => {
const { url, method, params, data } = config;
// 序列化参数确保相同参数生成相同key
const paramsStr = params ? JSON.stringify(params) : '';
const dataStr = data ? JSON.stringify(data) : '';
return `${method}:${url}:${paramsStr}:${dataStr}`;
};
// 请求拦截器:添加防抖逻辑
service.interceptors.request.use(
config => {
// 1. 检查POST请求体和参数是否都为空
if (config.method === 'post') {
// 检查请求体是否为空
const bodyEmpty =
config.data === undefined ||
config.data === null ||
(typeof config.data === 'object' && Object.keys(config.data).length === 0) ||
(typeof config.data === 'string' && config.data.trim().length === 0);
// 检查URL参数是否为空
const paramsEmpty =
config.params === undefined ||
config.params === null ||
(typeof config.params === 'object' && Object.keys(config.params).length === 0);
// 如果请求体和参数都为空,则打印提示
if (bodyEmpty && paramsEmpty) {
console.log('提示POST请求的请求体和参数均为空');
}
}
// 2. 防抖逻辑
return new Promise((resolve) => {
const requestKey = getRequestKey(config);
// 清除之前的定时器
if (debounceTimers.has(requestKey)) {
clearTimeout(debounceTimers.get(requestKey));
}
// 设置新定时器
const timer = setTimeout(() => {
debounceTimers.delete(requestKey);
resolve(config); // 延迟后执行请求
}, DEBOUNCE_DELAY);
debounceTimers.set(requestKey, timer);
});
},
error => {
console.error('请求拦截器错误:', error);
return Promise.reject(error);
}
);
export default service;

View File

@ -2,7 +2,7 @@ const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({ module.exports = defineConfig({
transpileDependencies: true, transpileDependencies: true,
devServer: { devServer: {
host: 'localhost', host: '0.0.0.0',
port: 8080, port: 8080,
historyApiFallback: true, historyApiFallback: true,
allowedHosts: "all" allowedHosts: "all"