初版提交

This commit is contained in:
xiaowang 2025-07-29 14:51:28 +08:00
parent 1f379b9c21
commit fd043bb9f2
44 changed files with 5376 additions and 367 deletions

View File

@ -137,6 +137,12 @@
<version>6.0.0</version> <version>6.0.0</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- MinIO 客户端 -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.6</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -0,0 +1,47 @@
package com.nailart.config;
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Description MinIO配置类用于创建MinIO客户端实例
* @Classname MinioConfig
* @Date 2025/7/16 10:13
* @Created by 21616
*/
@Configuration
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
@Value("${minio.region:}")
private String region;
@Value("${minio.connectTimeout:30}")
private long connectTimeout;
@Value("${minio.writeTimeout:60}")
private long writeTimeout;
@Value("${minio.readTimeout:60}")
private long readTimeout;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}

View File

@ -0,0 +1,170 @@
package com.nailart.controller;
import com.nailart.service.FileStorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* @Description 文件服务控制类
* @Classname FileStorageController
* @Date 2025/7/19 15:47
* @Created by 21616
*/
@RestController
@RequestMapping("/file")
public class FileStorageController {
private final FileStorageService fileStorageService;
@Autowired
public FileStorageController(FileStorageService fileStorageService) {
this.fileStorageService = fileStorageService;
}
/**
* 上传文件
* @param bucketName 存储桶名称
* @param filePath 文件路径
* @param file 文件内容
* @return 上传成功的文件路径
*/
@PostMapping("/upload")
public ResponseEntity<String> uploadFile(
@RequestParam("bucketName") String bucketName,
@RequestParam("filePath") String filePath,
@RequestParam("file") MultipartFile file) {
String uploadedFilePath = fileStorageService.uploadFile(bucketName, filePath, file);
return ResponseEntity.ok(uploadedFilePath);
}
/**
* 下载文件
* @param bucketName 存储桶名称
* @param filePath 文件路径
* @return 文件内容流
*/
@GetMapping("/download")
public ResponseEntity<InputStream> downloadFile(
@RequestParam("bucketName") String bucketName,
@RequestParam("filePath") String filePath) {
try {
InputStream fileStream = fileStorageService.downloadFile(bucketName, filePath);
// 设置响应头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
// 处理中文文件名编码
String encodedFileName = URLEncoder.encode(filePath.substring(filePath.lastIndexOf("/") + 1),
StandardCharsets.UTF_8).replaceAll("\\+", "%20");
headers.setContentDispositionFormData("attachment", encodedFileName);
return new ResponseEntity<>(fileStream, headers, HttpStatus.OK);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
}
/**
* 删除文件
* @param bucketName 存储桶名称
* @param filePath 文件路径
* @return 操作结果
*/
@DeleteMapping("/delete")
public ResponseEntity<Void> deleteFile(
@RequestParam("bucketName") String bucketName,
@RequestParam("filePath") String filePath) {
fileStorageService.deleteFile(bucketName, filePath);
return ResponseEntity.noContent().build();
}
/**
* 检查文件是否存在
* @param bucketName 存储桶名称
* @param filePath 文件路径
* @return 文件是否存在
*/
@GetMapping("/exists")
public ResponseEntity<Boolean> fileExists(
@RequestParam("bucketName") String bucketName,
@RequestParam("filePath") String filePath) {
boolean exists = fileStorageService.fileExists(bucketName, filePath);
return ResponseEntity.ok(exists);
}
/**
* 获取文件的预签名URL
* @param bucketName 存储桶名称
* @param filePath 文件路径
* @param expirySeconds 过期时间
* @return 预签名URL
*/
@GetMapping("/url")
public ResponseEntity<String> getFileUrl(
@RequestParam("bucketName") String bucketName,
@RequestParam("filePath") String filePath,
@RequestParam(value = "expirySeconds", defaultValue = "3600") int expirySeconds) {
String url = fileStorageService.getFileUrl(bucketName, filePath, expirySeconds);
return ResponseEntity.ok(url);
}
/**
* 获取所有存储桶列表
* @return 存储桶名称列表
*/
@GetMapping("/buckets")
public ResponseEntity<List<String>> listBuckets() {
List<String> buckets = fileStorageService.listBuckets();
return ResponseEntity.ok(buckets);
}
/**
* 创建存储桶
* @param bucketName 存储桶名称
* @return 操作结果
*/
@PostMapping("/buckets")
public ResponseEntity<Void> createBucket(@RequestParam("bucketName") String bucketName) {
fileStorageService.createBucket(bucketName);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
/**
* 删除存储桶
* @param bucketName 存储桶名称
* @return 操作结果
*/
@DeleteMapping("/buckets")
public ResponseEntity<Void> deleteBucket(@RequestParam("bucketName") String bucketName) {
fileStorageService.deleteBucket(bucketName);
return ResponseEntity.noContent().build();
}
/**
* 检查存储桶是否存在
* @param bucketName 存储桶名称
* @return 存储桶是否存在
*/
@GetMapping("/buckets/exists")
public ResponseEntity<Boolean> bucketExists(@RequestParam("bucketName") String bucketName) {
boolean exists = fileStorageService.bucketExists(bucketName);
return ResponseEntity.ok(exists);
}
}

View File

@ -0,0 +1,54 @@
package com.nailart.controller;
import com.nailart.dataobj.MainInfoDO;
import com.nailart.enums.RespCodeEnum;
import com.nailart.service.MainInfoService;
import com.nailart.service.impl.FileStorageServiceImpl;
import com.nailart.vo.RespVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Description 主页信息控制类
* @Classname MainInfoController
* @Date 2025/7/27 23:16
* @Created by 21616
*/
@RestController
@RequestMapping("/mainInfo")
public class MainInfoController {
private final MainInfoService mainInfoService;
private static final Logger logger = LoggerFactory.getLogger(MainInfoController.class);
public MainInfoController(MainInfoService mainInfoService) {
this.mainInfoService = mainInfoService;
}
@RequestMapping("/getMainInfo")
public RespVO getMainInfo() {
try {
MainInfoDO mainInfo = mainInfoService.getMainInfo();
return RespVO.success(mainInfo);
} catch (Exception e) {
logger.error("获取主页信息失败", e);
return RespVO.error(RespCodeEnum.DATABASE_ERROR, RespCodeEnum.DATABASE_ERROR.getMessage());
}
}
@RequestMapping("/updateMainInfo")
public RespVO updateMainInfo(@RequestBody MainInfoDO mainInfoDO) {
try {
mainInfoService.updateMainInfo(mainInfoDO);
return RespVO.success();
} catch (Exception e) {
logger.error("更新主信息失败", e);
return RespVO.error(RespCodeEnum.DATABASE_ERROR, RespCodeEnum.DATABASE_ERROR.getMessage());
}
}
}

View File

@ -26,8 +26,15 @@ public class PortfolioController {
this.portfolioService = portfolioService; this.portfolioService = portfolioService;
} }
/**
* 获取作品集列表
*
* @param page 页码
* @param size 每页数量
* @return RespVO
*/
@GetMapping("/list") @GetMapping("/list")
public RespVO getPortfolioList(@RequestParam("page") int page, @RequestParam("size") int size) { public RespVO getPortfolioList(@RequestParam("page") Integer page, @RequestParam("size") Integer size) {
RespVO respVO = new RespVO(); RespVO respVO = new RespVO();
if (page <= 0 || size <= 0) { if (page <= 0 || size <= 0) {
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode()); respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
@ -47,8 +54,14 @@ public class PortfolioController {
} }
/**
* 添加作品集
*
* @param portfolioDO 作品集信息
* @return RespVO
*/
@PostMapping("/add") @PostMapping("/add")
public RespVO addPortfolio(PortfolioDO portfolioDO) { public RespVO addPortfolio(@RequestBody PortfolioDO portfolioDO) {
RespVO respVO = new RespVO(); RespVO respVO = new RespVO();
if (portfolioDO == null) { if (portfolioDO == null) {
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode()); respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
@ -69,8 +82,14 @@ public class PortfolioController {
} }
/**
* 更新作品集
*
* @param portfolioDO 作品集信息
* @return RespVO
*/
@PostMapping("/update") @PostMapping("/update")
public RespVO updatePortfolio(PortfolioDO portfolioDO) { public RespVO updatePortfolio(@RequestBody PortfolioDO portfolioDO) {
RespVO respVO = new RespVO(); RespVO respVO = new RespVO();
if (portfolioDO == null) { if (portfolioDO == null) {
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode()); respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
@ -89,6 +108,12 @@ public class PortfolioController {
return respVO; return respVO;
} }
/**
* 删除作品集
*
* @param id 作品集id
* @return RespVO
*/
@PostMapping("/delete") @PostMapping("/delete")
public RespVO deletePortfolio(@RequestParam("id") Integer id) { public RespVO deletePortfolio(@RequestParam("id") Integer id) {
RespVO respVO = new RespVO(); RespVO respVO = new RespVO();
@ -109,8 +134,15 @@ public class PortfolioController {
return respVO; return respVO;
} }
/**
* 添加作品集
*
* @param id 作品集id
* @param changes 点赞数
* @return RespVO
*/
@PostMapping("/addLoves") @PostMapping("/addLoves")
public RespVO addLoves(@RequestParam("id") Integer id,@RequestParam Integer changes) { public RespVO addLoves(@RequestParam("id") Integer id, @RequestParam Integer changes) {
RespVO respVO = new RespVO(); RespVO respVO = new RespVO();
if (id == null) { if (id == null) {
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode()); respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
@ -125,7 +157,31 @@ public class PortfolioController {
} catch (Exception e) { } catch (Exception e) {
respVO.setCode(RespCodeEnum.INTERNAL_SERVER_ERROR.getCode()); respVO.setCode(RespCodeEnum.INTERNAL_SERVER_ERROR.getCode());
respVO.setMsg(RespCodeEnum.INTERNAL_SERVER_ERROR.getMessage()); respVO.setMsg(RespCodeEnum.INTERNAL_SERVER_ERROR.getMessage());
} }
return respVO; return respVO;
} }
@GetMapping("/getPortfolioByTag")
public RespVO getPortfolioByTag(@RequestParam("tagID") Long tagID,
@RequestParam("page") Integer page,
@RequestParam("size") Integer size) {
RespVO respVO = new RespVO();
if (tagID == null) {
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
respVO.setMsg(RespCodeEnum.BAD_REQUEST.getMessage());
return respVO;
}
try {
IPage<PortfolioDO> portfolioByTag = portfolioService.getPortfolioByTag(tagID, page, size);
respVO.setCode(RespCodeEnum.SUCCESS.getCode());
respVO.setMsg(RespCodeEnum.SUCCESS.getMessage());
respVO.setData(portfolioByTag);
} catch (Exception e) {
respVO.setCode(RespCodeEnum.INTERNAL_SERVER_ERROR.getCode());
respVO.setMsg(RespCodeEnum.INTERNAL_SERVER_ERROR.getMessage());
}
return respVO;
}
} }

View File

@ -0,0 +1,156 @@
package com.nailart.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.nailart.dataobj.SampleCollectionDO;
import com.nailart.enums.RespCodeEnum;
import com.nailart.service.SampleCollectionService;
import com.nailart.vo.RespVO;
import org.springframework.web.bind.annotation.*;
/**
* @Classname SampleCollectionController
* @Description 样品集控制类
* @Date 2025/7/10 18:48
* @Created by 21616
*/
@RestController
@RequestMapping("/sampleCollection")
public class SampleCollectionController {
private final SampleCollectionService SampleCollectionService;
public SampleCollectionController(SampleCollectionService SampleCollectionService) {
this.SampleCollectionService = SampleCollectionService;
}
/**
* 获取样品集列表
*
* @param page 页码
* @param size 每页数量
* @return RespVO
*/
@GetMapping("/list")
public RespVO getSampleCollectionList(@RequestParam("page") Integer page, @RequestParam("size") Integer 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<SampleCollectionDO> SampleCollectionDOIPage = SampleCollectionService.listByPage(page, size);
respVO.setCode(RespCodeEnum.SUCCESS.getCode());
respVO.setMsg(RespCodeEnum.SUCCESS.getMessage());
respVO.setData(SampleCollectionDOIPage);
} catch (Exception e) {
respVO.setCode(RespCodeEnum.INTERNAL_SERVER_ERROR.getCode());
respVO.setMsg(RespCodeEnum.INTERNAL_SERVER_ERROR.getMessage());
}
return respVO;
}
/**
* 添加样品集
*
* @param SampleCollectionDO 样品集信息
* @return RespVO
*/
@PostMapping("/add")
public RespVO addSampleCollection(@RequestBody SampleCollectionDO SampleCollectionDO) {
RespVO respVO = new RespVO();
if (SampleCollectionDO == null) {
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
respVO.setMsg(RespCodeEnum.BAD_REQUEST.getMessage());
return respVO;
}
try {
Integer i = SampleCollectionService.addSampleCollection(SampleCollectionDO);
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;
}
/**
* 更新样品集
*
* @param SampleCollectionDO 样品集信息
* @return RespVO
*/
@PostMapping("/update")
public RespVO updateSampleCollection(@RequestBody SampleCollectionDO SampleCollectionDO) {
RespVO respVO = new RespVO();
if (SampleCollectionDO == null) {
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
respVO.setMsg(RespCodeEnum.BAD_REQUEST.getMessage());
return respVO;
}
try {
Integer i = SampleCollectionService.updateSampleCollection(SampleCollectionDO);
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;
}
/**
* 删除样品集
*
* @param id 样品集id
* @return RespVO
*/
@PostMapping("/delete")
public RespVO deleteSampleCollection(@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 = SampleCollectionService.deleteSampleCollection(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;
}
@GetMapping("/getSampleCollectionByTag")
public RespVO getSampleCollectionByTag(@RequestParam("tagID") Long tagID,
@RequestParam("page") Integer page,
@RequestParam("size") Integer size) {
RespVO respVO = new RespVO();
if (tagID == null) {
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
respVO.setMsg(RespCodeEnum.BAD_REQUEST.getMessage());
return respVO;
}
try {
IPage<SampleCollectionDO> SampleCollectionByTag = SampleCollectionService.getSampleCollectionByTag(tagID, page, size);
respVO.setCode(RespCodeEnum.SUCCESS.getCode());
respVO.setMsg(RespCodeEnum.SUCCESS.getMessage());
respVO.setData(SampleCollectionByTag);
} catch (Exception e) {
respVO.setCode(RespCodeEnum.INTERNAL_SERVER_ERROR.getCode());
respVO.setMsg(RespCodeEnum.INTERNAL_SERVER_ERROR.getMessage());
}
return respVO;
}
}

View File

@ -0,0 +1,120 @@
package com.nailart.controller;
import com.nailart.dataobj.TagDO;
import com.nailart.service.TagInfoService;
import com.nailart.vo.RespVO;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @Description 标签信息控制类
* @Classname TagInfoController
* @Date 2025/7/14 23:44
* @Created by 21616
*/
@RestController
@RequestMapping("/tagInfo")
public class TagInfoController {
private final TagInfoService tagInfoService;
TagInfoController(TagInfoService tagInfoService) {
this.tagInfoService = tagInfoService;
}
@PostMapping("/save")
public RespVO saveTagInfo(@RequestBody TagDO tagDO) {
RespVO respVO = new RespVO();
if (tagDO == null) {
respVO.setCode(500);
respVO.setMsg("标签信息不能为空");
return respVO;
}
if (tagInfoService.saveTagInfo(tagDO) == 0) {
respVO.setCode(500);
respVO.setMsg("保存标签信息失败");
return respVO;
}
respVO.setCode(200);
respVO.setData(tagInfoService.saveTagInfo(tagDO));
return respVO;
}
@GetMapping("/getById")
public RespVO getTagInfoById(@RequestParam Long id) {
RespVO respVO = new RespVO();
if (id == null) {
respVO.setCode(500);
respVO.setMsg("标签id不能为空");
return respVO;
}
TagDO tagInfoById = tagInfoService.getTagInfoById(id);
if (tagInfoById == null) {
respVO.setCode(500);
respVO.setMsg("查询标签信息失败");
return respVO;
}
respVO.setCode(200);
respVO.setData(tagInfoById);
return respVO;
}
@PostMapping("/deleteById")
public RespVO deleteTagInfoById(@RequestBody Long id) {
RespVO respVO = new RespVO();
if (id == null) {
respVO.setCode(500);
respVO.setMsg("标签id不能为空");
return respVO;
}
TagDO tagInfoById = tagInfoService.getTagInfoById(id);
if (tagInfoById == null) {
respVO.setCode(500);
respVO.setMsg("查询标签信息失败");
return respVO;
}
respVO.setCode(200);
respVO.setData(tagInfoById);
return respVO;
}
@PostMapping("/updateById")
public RespVO updateTagInfoById(@RequestBody TagDO tagDO) {
RespVO respVO = new RespVO();
if (tagDO == null) {
respVO.setCode(500);
respVO.setMsg("标签信息不能为空");
return respVO;
}
TagDO tagInfoById = tagInfoService.getTagInfoById(tagDO.getId());
if (tagInfoById == null) {
respVO.setCode(500);
respVO.setMsg("查询标签信息失败");
return respVO;
}
respVO.setCode(200);
respVO.setData(tagInfoById);
return respVO;
}
@GetMapping("/getAll")
public RespVO getAllTagInfo() {
RespVO respVO = new RespVO();
List<TagDO> allTagInfo = tagInfoService.getAllTagInfo();
if (allTagInfo == null || allTagInfo.isEmpty()) {
respVO.setCode(500);
respVO.setMsg("获取标签信息失败");
return respVO;
}
respVO.setCode(200);
respVO.setData(allTagInfo);
return respVO;
}
}

View File

@ -0,0 +1,72 @@
package com.nailart.dataobj;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* @Description 主页信息表
* @Classname MainInfoDO
* @Date 2025/7/23 23:38
* @Created by 21616
*/
@Data
@TableName("main_info")
public class MainInfoDO {
/**
* id
*/
private Integer id;
/**
* 主页图
*/
private String headImg;
/**
* 店名
*/
private String name;
/**
* 介绍
*/
private String introduce;
/**
* 状态
*
*/
private Integer status;
/**
* 工作时间
*/
private String worktime;
/**
* 地址
*/
private String address;
/**
* 广告图&&分开
*/
private String publicityImg;
/**
* 新品图列表,&&分开
*/
private String newPortfolioImg;
/**
* 弹窗图
*/
private String popcard;
/**
* 指引图
*/
private String locationImg;
/**
* 环境图
*/
private String environmentImg;
/**
* 微信
*/
private String wechat;
/**
* 电话
*/
private String phone;
}

View File

@ -32,13 +32,10 @@ public class PortfolioDO {
*/ */
private String imgFilename; private String imgFilename;
/** /**
* 标签 * 标签ID
*/ */
private String tag; private String tagId;
/**
* 标签样式
*/
private String tagStyle;
/** /**
* 创建时间 * 创建时间
*/ */

View File

@ -0,0 +1,46 @@
package com.nailart.dataobj;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("sample_collection_info")
public class SampleCollectionDO {
/**
* id
*/
private Long id;
/**
* 标题
*/
private String title;
/**
* 描述
*/
private String description;
/**
* 图片地址
*/
private String imgUrl;
/**
* 图片文件名
*/
private String imgFilename;
/**
* 标签ID
*/
private String tagId;
/**
* 创建时间
*/
private String createTime;
/**
* 状态
*/
private Integer status;
/**
* 价格
*/
private Integer amt;
}

View File

@ -0,0 +1,19 @@
package com.nailart.dataobj;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* @Description 标签信息实体类
* @Classname TagDO
* @Date 2025/7/14 23:18
* @Created by 21616
*/
@Data
@TableName("tag_info")
public class TagDO {
private Long id;
private String tagName;
private String tagColor;
private String tagBgColor;
}

View File

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

View File

@ -1,5 +1,6 @@
package com.nailart.mapper; package com.nailart.mapper;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nailart.dataobj.PortfolioDO; import com.nailart.dataobj.PortfolioDO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;

View File

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

View File

@ -0,0 +1,17 @@
package com.nailart.mapper;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nailart.dataobj.TagDO;
import org.apache.ibatis.annotations.Mapper;
/**
* @Description 标签信息表
* @Classname TagInfoMapper
* @Date 2025/7/14 23:22
* @Created by 21616
*/
@Mapper
public interface TagInfoMapper extends BaseMapper<TagDO> {
}

View File

@ -0,0 +1,91 @@
package com.nailart.service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* @Description 文件存储服务接口定义文件管理操作
* @Classname FileStorageService
* @Date 2025/7/19 17:19
* @Created by 21616
*/
public interface FileStorageService {
/**
* 上传文件
* @param bucketName 存储桶名称
* @param filePath 文件路径
* @param file 文件内容
* @return 文件存储路径
*/
String uploadFile(String bucketName, String filePath, MultipartFile file);
/**
* 上传文件从输入流
* @param bucketName 存储桶名称
* @param filePath 文件路径
* @param inputStream 文件输入流
* @param contentType 文件内容类型
* @return 文件存储路径
*/
String uploadFile(String bucketName, String filePath, InputStream inputStream, String contentType);
/**
* 下载文件
* @param bucketName 存储桶名称
* @param filePath 文件路径
* @return 文件输入流
*/
InputStream downloadFile(String bucketName, String filePath);
/**
* 删除文件
* @param bucketName 存储桶名称
* @param filePath 文件路径
*/
void deleteFile(String bucketName, String filePath);
/**
* 检查文件是否存在
* @param bucketName 存储桶名称
* @param filePath 文件路径
* @return 文件是否存在
*/
boolean fileExists(String bucketName, String filePath);
/**
* 获取文件的预签名URL
* @param bucketName 存储桶名称
* @param filePath 文件路径
* @param expirySeconds 过期时间
* @return 预签名URL
*/
String getFileUrl(String bucketName, String filePath, int expirySeconds);
/**
* 获取存储桶列表
* @return 存储桶名称列表
*/
List<String> listBuckets();
/**
* 检查存储桶是否存在
* @param bucketName 存储桶名称
* @return 存储桶是否存在
*/
boolean bucketExists(String bucketName);
/**
* 创建存储桶
* @param bucketName 存储桶名称
*/
void createBucket(String bucketName);
/**
* 删除存储桶必须为空
* @param bucketName 存储桶名称
*/
void deleteBucket(String bucketName);
}

View File

@ -0,0 +1,10 @@
package com.nailart.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.nailart.dataobj.MainInfoDO;
public interface MainInfoService extends IService<MainInfoDO> {
public MainInfoDO getMainInfo();
public Integer updateMainInfo(MainInfoDO mainInfoDO);
}

View File

@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.nailart.dataobj.PortfolioDO; import com.nailart.dataobj.PortfolioDO;
import java.util.List;
public interface PortfolioService extends IService<PortfolioDO> { public interface PortfolioService extends IService<PortfolioDO> {
IPage<PortfolioDO> listByPage(int page, int size); IPage<PortfolioDO> listByPage(int page, int size);
@ -14,4 +16,6 @@ public interface PortfolioService extends IService<PortfolioDO> {
Integer deletePortfolio(Integer id); Integer deletePortfolio(Integer id);
Integer addLoves(Integer id, Integer changes); Integer addLoves(Integer id, Integer changes);
IPage<PortfolioDO> getPortfolioByTag(Long tagID,int page, int size);
} }

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.SampleCollectionDO;
public interface SampleCollectionService extends IService<SampleCollectionDO> {
IPage<SampleCollectionDO> listByPage(int page, int size);
Integer addSampleCollection(SampleCollectionDO SampleCollectionDO);
Integer updateSampleCollection(SampleCollectionDO SampleCollectionDO);
Integer deleteSampleCollection(Integer id);
IPage<SampleCollectionDO> getSampleCollectionByTag(Long tagID,int page, int size);
}

View File

@ -0,0 +1,55 @@
package com.nailart.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.nailart.dataobj.TagDO;
import org.springframework.data.relational.core.sql.In;
import java.util.List;
/**
* @Description 标签信息服务类
* @Classname TagInfoService
* @Date 2025/7/14 23:24
* @Created by 21616
*/
public interface TagInfoService extends IService<TagDO> {
/**
* 保存标签信息
*
* @param tagDO 标签信息
* @return 是否成功
*/
public Integer saveTagInfo(TagDO tagDO);
/**
* 根据id查询标签信息
*
* @param id 标签id
* @return 标签信息
*/
public TagDO getTagInfoById(Long id);
/**
* 根据id删除标签信息
*
* @param id 标签id
* @return 是否成功
*/
public Integer deleteTagInfoById(Long id);
/**
* 根据id更新标签信息
*
* @param tagDO 标签信息
* @return 是否成功
*/
public Integer updateTagInfoById(TagDO tagDO);
/**
* 查询所有标签信息
*
* @return 标签信息列表
*/
public List<TagDO> getAllTagInfo();
}

View File

@ -0,0 +1,202 @@
package com.nailart.service.impl;
import com.nailart.service.FileStorageService;
import com.nailart.utils.MinioUtils;
import io.minio.Result;
import io.minio.messages.Bucket;
import io.minio.messages.Item;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/**
* @Description MinIO 文件存储服务实现
* @Classname FileStorageServiceImpl
* @Date 2025/7/19 17:20
* @Created by 21616
*/
@Service
public class FileStorageServiceImpl implements FileStorageService {
private static final Logger logger = LoggerFactory.getLogger(FileStorageServiceImpl.class);
private final MinioUtils minioUtils;
@Autowired
public FileStorageServiceImpl(MinioUtils minioUtils) {
this.minioUtils = minioUtils;
}
@Override
public String uploadFile(String bucketName, String filePath, MultipartFile file) {
try {
ensureBucketExists(bucketName);
minioUtils.uploadFile(bucketName, filePath, file);
logger.info("文件上传成功: bucket={}, path={}", bucketName, filePath);
return filePath;
} catch (Exception e) {
logger.error("文件上传失败: bucket={}, path={}", bucketName, filePath, e);
throw new RuntimeException("文件上传失败", e);
}
}
@Override
public String uploadFile(String bucketName, String filePath, InputStream inputStream, String contentType) {
try {
ensureBucketExists(bucketName);
minioUtils.uploadFile(bucketName, filePath, inputStream, contentType);
logger.info("文件上传成功: bucket={}, path={}", bucketName, filePath);
return filePath;
} catch (Exception e) {
logger.error("文件上传失败: bucket={}, path={}", bucketName, filePath, e);
throw new RuntimeException("文件上传失败", e);
}
}
@Override
public InputStream downloadFile(String bucketName, String filePath) {
try {
if (!fileExists(bucketName, filePath)) {
logger.warn("文件不存在: bucket={}, path={}", bucketName, filePath);
throw new RuntimeException("文件不存在");
}
return minioUtils.getObject(bucketName, filePath);
} catch (Exception e) {
logger.error("文件下载失败: bucket={}, path={}", bucketName, filePath, e);
throw new RuntimeException("文件下载失败", e);
}
}
@Override
public void deleteFile(String bucketName, String filePath) {
try {
if (!fileExists(bucketName, filePath)) {
logger.warn("文件不存在,无需删除: bucket={}, path={}", bucketName, filePath);
return;
}
minioUtils.removeObject(bucketName, filePath);
logger.info("文件删除成功: bucket={}, path={}", bucketName, filePath);
} catch (Exception e) {
logger.error("文件删除失败: bucket={}, path={}", bucketName, filePath, e);
throw new RuntimeException("文件删除失败", e);
}
}
@Override
public boolean fileExists(String bucketName, String filePath) {
try {
if (!bucketExists(bucketName)) {
return false;
}
// 通过列出对象检查文件是否存在
Iterable<Result<Item>> results = minioUtils.listObjects(bucketName);
return StreamSupport.stream(results.spliterator(), false)
.anyMatch(itemResult -> {
try {
return itemResult.get().objectName().equals(filePath);
} catch (Exception e) {
logger.error("检查文件存在时出错", e);
return false;
}
});
} catch (Exception e) {
logger.error("检查文件存在失败: bucket={}, path={}", bucketName, filePath, e);
throw new RuntimeException("检查文件存在失败", e);
}
}
@Override
public String getFileUrl(String bucketName, String filePath, int expirySeconds) {
try {
if (!fileExists(bucketName, filePath)) {
logger.warn("文件不存在无法生成URL: bucket={}, path={}", bucketName, filePath);
throw new RuntimeException("文件不存在");
}
return minioUtils.getPresignedUrl(bucketName, filePath, expirySeconds);
} catch (Exception e) {
logger.error("生成文件URL失败: bucket={}, path={}", bucketName, filePath, e);
throw new RuntimeException("生成文件URL失败", e);
}
}
@Override
public List<String> listBuckets() {
try {
List<Bucket> buckets = minioUtils.listBuckets();
return buckets.stream().map(Bucket::name).collect(Collectors.toList());
} catch (Exception e) {
logger.error("获取存储桶列表失败", e);
throw new RuntimeException("获取存储桶列表失败", e);
}
}
@Override
public boolean bucketExists(String bucketName) {
try {
return minioUtils.bucketExists(bucketName);
} catch (Exception e) {
logger.error("检查存储桶存在失败: bucket={}", bucketName, e);
throw new RuntimeException("检查存储桶存在失败", e);
}
}
@Override
public void createBucket(String bucketName) {
try {
if (!bucketExists(bucketName)) {
minioUtils.makeBucket(bucketName);
logger.info("创建存储桶成功: bucket={}", bucketName);
} else {
logger.info("存储桶已存在: bucket={}", bucketName);
}
} catch (Exception e) {
logger.error("创建存储桶失败: bucket={}", bucketName, e);
throw new RuntimeException("创建存储桶失败", e);
}
}
@Override
public void deleteBucket(String bucketName) {
try {
if (bucketExists(bucketName)) {
// 检查存储桶是否为空
Iterable<Result<Item>> results = minioUtils.listObjects(bucketName);
boolean isEmpty = !results.iterator().hasNext();
if (isEmpty) {
minioUtils.removeBucket(bucketName);
logger.info("删除存储桶成功: bucket={}", bucketName);
} else {
logger.warn("无法删除非空存储桶: bucket={}", bucketName);
throw new RuntimeException("无法删除非空存储桶");
}
} else {
logger.info("存储桶不存在,无需删除: bucket={}", bucketName);
}
} catch (Exception e) {
logger.error("删除存储桶失败: bucket={}", bucketName, e);
throw new RuntimeException("删除存储桶失败", e);
}
}
/**
* 确保存储桶存在如果不存在则创建
*/
private void ensureBucketExists(String bucketName) {
try {
if (!minioUtils.bucketExists(bucketName)) {
minioUtils.makeBucket(bucketName);
logger.info("自动创建存储桶: {}", bucketName);
}
} catch (Exception e) {
logger.error("确保存储桶存在失败: bucket={}", bucketName, e);
throw new RuntimeException("初始化存储桶失败", e);
}
}
}

View File

@ -0,0 +1,45 @@
package com.nailart.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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.MainInfoDO;
import com.nailart.dataobj.PortfolioDO;
import com.nailart.enums.RespCodeEnum;
import com.nailart.mapper.MainInfoMapper;
import com.nailart.mapper.PortfolioMapper;
import com.nailart.service.MainInfoService;
import com.nailart.service.PortfolioService;
import com.nailart.utils.DateUtils;
import com.nailart.vo.RespVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
/**
* @Author: xiaowang
* @Date: 2021/5/10 14:48
*/
@Service
public class MainInfoServiceImpl extends ServiceImpl<MainInfoMapper, MainInfoDO> implements MainInfoService {
private final MainInfoMapper mainInfoMapper;
private static final Logger logger = LoggerFactory.getLogger(MainInfoServiceImpl.class);
public MainInfoServiceImpl(MainInfoMapper mainInfoMapper) {
this.mainInfoMapper = mainInfoMapper;
}
@Override
public MainInfoDO getMainInfo() {
return mainInfoMapper.selectById(1);
}
@Override
public Integer updateMainInfo(MainInfoDO mainInfoDO) {
return mainInfoMapper.updateById(mainInfoDO);
}
}

View File

@ -1,11 +1,15 @@
package com.nailart.service.impl; package com.nailart.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nailart.dataobj.PortfolioDO; import com.nailart.dataobj.PortfolioDO;
import com.nailart.dataobj.TagDO;
import com.nailart.mapper.PortfolioMapper; import com.nailart.mapper.PortfolioMapper;
import com.nailart.service.PortfolioService; import com.nailart.service.PortfolioService;
import com.nailart.service.TagInfoService;
import com.nailart.utils.DateUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/** /**
@ -18,7 +22,8 @@ public class PortfolioServiceImpl extends ServiceImpl<PortfolioMapper, Portfolio
private final PortfolioMapper portfolioMapper; private final PortfolioMapper portfolioMapper;
public PortfolioServiceImpl(PortfolioMapper portfolioMapper) { public PortfolioServiceImpl(PortfolioMapper portfolioMapper ) {
this.portfolioMapper = portfolioMapper; this.portfolioMapper = portfolioMapper;
} }
@ -36,6 +41,9 @@ public class PortfolioServiceImpl extends ServiceImpl<PortfolioMapper, Portfolio
@Override @Override
public Integer addPortfolio(PortfolioDO portfolioDO) { public Integer addPortfolio(PortfolioDO portfolioDO) {
if (portfolioDO.getCreateTime()==null){
portfolioDO.setCreateTime(DateUtils.getCurrentDateTime());
}
return portfolioMapper.insert(portfolioDO); return portfolioMapper.insert(portfolioDO);
} }
@ -58,4 +66,12 @@ public class PortfolioServiceImpl extends ServiceImpl<PortfolioMapper, Portfolio
portfolioDO.setLoves(portfolioDO.getLoves() + changes); portfolioDO.setLoves(portfolioDO.getLoves() + changes);
return portfolioMapper.updateById(portfolioDO); return portfolioMapper.updateById(portfolioDO);
} }
@Override
public IPage<PortfolioDO> getPortfolioByTag(Long tagID,int page, int size) {
Page<PortfolioDO> portfolioDOPage = new Page<>(page, size);
LambdaQueryWrapper<PortfolioDO> wrapper = new LambdaQueryWrapper<>();
wrapper.like(PortfolioDO::getTagId, tagID);
return page(portfolioDOPage, wrapper);
}
} }

View File

@ -0,0 +1,66 @@
package com.nailart.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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.SampleCollectionDO;
import com.nailart.mapper.SampleCollectionMapper;
import com.nailart.service.SampleCollectionService;
import com.nailart.utils.DateUtils;
import org.springframework.stereotype.Service;
/**
*
* @Author: xiaowang
* @Date: 2021/5/10 14:48
*/
@Service
public class SampleCollectionServiceImpl extends ServiceImpl<SampleCollectionMapper, SampleCollectionDO> implements SampleCollectionService {
private final SampleCollectionMapper SampleCollectionMapper;
public SampleCollectionServiceImpl(SampleCollectionMapper SampleCollectionMapper ) {
this.SampleCollectionMapper = SampleCollectionMapper;
}
/**
*
* @param page
* @param size
* @return
*/
@Override
public IPage<SampleCollectionDO> listByPage(int page, int size) {
Page<SampleCollectionDO> objectPage = new Page<>(page, size);
return SampleCollectionMapper.selectPage(objectPage, null);
}
@Override
public Integer addSampleCollection(SampleCollectionDO SampleCollectionDO) {
if (SampleCollectionDO.getCreateTime()==null){
SampleCollectionDO.setCreateTime(DateUtils.getCurrentDateTime());
}
return SampleCollectionMapper.insert(SampleCollectionDO);
}
@Override
public Integer updateSampleCollection(SampleCollectionDO SampleCollectionDO) {
return SampleCollectionMapper.updateById(SampleCollectionDO);
}
@Override
public Integer deleteSampleCollection(Integer id) {
return SampleCollectionMapper.deleteById(id);
}
@Override
public IPage<SampleCollectionDO> getSampleCollectionByTag(Long tagID,int page, int size) {
Page<SampleCollectionDO> SampleCollectionDOPage = new Page<>(page, size);
LambdaQueryWrapper<SampleCollectionDO> wrapper = new LambdaQueryWrapper<>();
wrapper.like(SampleCollectionDO::getTagId, tagID);
return page(SampleCollectionDOPage, wrapper);
}
}

View File

@ -0,0 +1,60 @@
package com.nailart.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nailart.dataobj.TagDO;
import com.nailart.mapper.TagInfoMapper;
import com.nailart.service.TagInfoService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @Description 标签信息服务实现类
* @Classname TagInfoServiceImpl
* @Date 2025/7/14 23:25
* @Created by 21616
*/
@Service
public class TagInfoServiceImpl extends ServiceImpl<TagInfoMapper, TagDO> implements TagInfoService {
@Override
public Integer saveTagInfo(TagDO tagDO) {
if (tagDO == null) {
return 0;
}
try {
return this.saveTagInfo(tagDO);
} catch (Exception e) {
return 0;
}
}
@Override
public TagDO getTagInfoById(Long id) {
if (id == null) {
return null;
}
return this.getTagInfoById(id);
}
@Override
public Integer deleteTagInfoById(Long id) {
if (id == null) {
return 0;
}
return this.deleteTagInfoById(id);
}
@Override
public Integer updateTagInfoById(TagDO tagDO) {
if (tagDO == null) {
return 0;
}
return this.updateTagInfoById(tagDO);
}
@Override
public List<TagDO> getAllTagInfo() {
return this.list();
}
}

View File

@ -0,0 +1,303 @@
package com.nailart.utils;
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.Item;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
/**
* @Description MinIO工具类提供文件存储操作的封装
* @Classname MinioUtils
* @Date 2025/7/16 10:15
* @Created by 21616
*/
@Component
public class MinioUtils {
private final MinioClient minioClient;
@Autowired
public MinioUtils(MinioClient minioClient) {
this.minioClient = minioClient;
}
/**
* 检查存储桶是否存在
* @param bucketName 存储桶名称
* @return 存在返回true否则返回false
* @throws ServerException 服务端异常
* @throws InsufficientDataException 数据不足异常
* @throws ErrorResponseException 错误响应异常
* @throws IOException IO异常
* @throws NoSuchAlgorithmException 算法不存在异常
* @throws InvalidKeyException 无效密钥异常
* @throws InvalidResponseException 无效响应异常
* @throws XmlParserException XML解析异常
* @throws InternalException 内部异常
*/
public boolean bucketExists(String bucketName) throws ServerException, InsufficientDataException,
ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException,
InvalidResponseException, XmlParserException, InternalException {
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
}
/**
* 创建存储桶
* @param bucketName 存储桶名称
* @throws ServerException 服务端异常
* @throws InsufficientDataException 数据不足异常
* @throws ErrorResponseException 错误响应异常
* @throws IOException IO异常
* @throws NoSuchAlgorithmException 算法不存在异常
* @throws InvalidKeyException 无效密钥异常
* @throws InvalidResponseException 无效响应异常
* @throws XmlParserException XML解析异常
* @throws InternalException 内部异常
*/
public void makeBucket(String bucketName) throws ServerException, InsufficientDataException,
ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException,
InvalidResponseException, XmlParserException, InternalException {
if (!bucketExists(bucketName)) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
}
/**
* 列出所有存储桶
* @return 存储桶列表
* @throws ServerException 服务端异常
* @throws InsufficientDataException 数据不足异常
* @throws ErrorResponseException 错误响应异常
* @throws IOException IO异常
* @throws NoSuchAlgorithmException 算法不存在异常
* @throws InvalidKeyException 无效密钥异常
* @throws InvalidResponseException 无效响应异常
* @throws XmlParserException XML解析异常
* @throws InternalException 内部异常
*/
public List<Bucket> listBuckets() throws ServerException, InsufficientDataException,
ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException,
InvalidResponseException, XmlParserException, InternalException {
return minioClient.listBuckets();
}
/**
* 删除存储桶必须为空桶
* @param bucketName 存储桶名称
* @throws ServerException 服务端异常
* @throws InsufficientDataException 数据不足异常
* @throws ErrorResponseException 错误响应异常
* @throws IOException IO异常
* @throws NoSuchAlgorithmException 算法不存在异常
* @throws InvalidKeyException 无效密钥异常
* @throws InvalidResponseException 无效响应异常
* @throws XmlParserException XML解析异常
* @throws InternalException 内部异常
*/
public void removeBucket(String bucketName) throws ServerException, InsufficientDataException,
ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException,
InvalidResponseException, XmlParserException, InternalException {
if (bucketExists(bucketName)) {
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
}
}
/**
* 上传本地文件到MinIO
* @param bucketName 存储桶名称
* @param objectName 对象名称文件路径
* @param filePath 本地文件路径
* @throws ServerException 服务端异常
* @throws InsufficientDataException 数据不足异常
* @throws ErrorResponseException 错误响应异常
* @throws IOException IO异常
* @throws NoSuchAlgorithmException 算法不存在异常
* @throws InvalidKeyException 无效密钥异常
* @throws InvalidResponseException 无效响应异常
* @throws XmlParserException XML解析异常
* @throws InternalException 内部异常
*/
public void uploadFile(String bucketName, String objectName, String filePath) throws ServerException,
InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException,
InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
minioClient.uploadObject(UploadObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.filename(filePath)
.build());
}
/**
* 通过InputStream上传文件
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @param inputStream 文件输入流
* @param contentType 文件内容类型
* @throws ServerException 服务端异常
* @throws InsufficientDataException 数据不足异常
* @throws ErrorResponseException 错误响应异常
* @throws IOException IO异常
* @throws NoSuchAlgorithmException 算法不存在异常
* @throws InvalidKeyException 无效密钥异常
* @throws InvalidResponseException 无效响应异常
* @throws XmlParserException XML解析异常
* @throws InternalException 内部异常
*/
public void uploadFile(String bucketName, String objectName, InputStream inputStream, String contentType)
throws ServerException, InsufficientDataException, ErrorResponseException, IOException,
NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException,
InternalException {
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(inputStream, inputStream.available(), -1)
.contentType(contentType)
.build());
}
/**
* 通过Spring MultipartFile上传文件
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @param file Spring MultipartFile文件对象
* @throws ServerException 服务端异常
* @throws InsufficientDataException 数据不足异常
* @throws ErrorResponseException 错误响应异常
* @throws IOException IO异常
* @throws NoSuchAlgorithmException 算法不存在异常
* @throws InvalidKeyException 无效密钥异常
* @throws InvalidResponseException 无效响应异常
* @throws XmlParserException XML解析异常
* @throws InternalException 内部异常
*/
public void uploadFile(String bucketName, String objectName, MultipartFile file)
throws ServerException, InsufficientDataException, ErrorResponseException, IOException,
NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException,
InternalException {
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build());
}
/**
* 下载文件到本地
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @param filePath 本地文件路径
* @throws ServerException 服务端异常
* @throws InsufficientDataException 数据不足异常
* @throws ErrorResponseException 错误响应异常
* @throws IOException IO异常
* @throws NoSuchAlgorithmException 算法不存在异常
* @throws InvalidKeyException 无效密钥异常
* @throws InvalidResponseException 无效响应异常
* @throws XmlParserException XML解析异常
* @throws InternalException 内部异常
*/
public void downloadFile(String bucketName, String objectName, String filePath) throws ServerException,
InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException,
InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
minioClient.downloadObject(DownloadObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.filename(filePath)
.build());
}
/**
* 获取文件输入流
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @return 文件输入流
* @throws ServerException 服务端异常
* @throws InsufficientDataException 数据不足异常
* @throws ErrorResponseException 错误响应异常
* @throws IOException IO异常
* @throws NoSuchAlgorithmException 算法不存在异常
* @throws InvalidKeyException 无效密钥异常
* @throws InvalidResponseException 无效响应异常
* @throws XmlParserException XML解析异常
* @throws InternalException 内部异常
*/
public InputStream getObject(String bucketName, String objectName) throws ServerException,
InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException,
InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
return minioClient.getObject(GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
}
/**
* 删除对象
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @throws ServerException 服务端异常
* @throws InsufficientDataException 数据不足异常
* @throws ErrorResponseException 错误响应异常
* @throws IOException IO异常
* @throws NoSuchAlgorithmException 算法不存在异常
* @throws InvalidKeyException 无效密钥异常
* @throws InvalidResponseException 无效响应异常
* @throws XmlParserException XML解析异常
* @throws InternalException 内部异常
*/
public void removeObject(String bucketName, String objectName) throws ServerException,
InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException,
InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
}
/**
* 列出存储桶中的对象
* @param bucketName 存储桶名称
* @return 对象迭代器
*/
public Iterable<Result<Item>> listObjects(String bucketName) {
return minioClient.listObjects(ListObjectsArgs.builder()
.bucket(bucketName)
.build());
}
/**
* 生成预签名URL用于临时访问私有对象
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @param expiry 过期时间
* @return 预签名URL
* @throws ServerException 服务端异常
* @throws InsufficientDataException 数据不足异常
* @throws ErrorResponseException 错误响应异常
* @throws IOException IO异常
* @throws NoSuchAlgorithmException 算法不存在异常
* @throws InvalidKeyException 无效密钥异常
* @throws InvalidResponseException 无效响应异常
* @throws XmlParserException XML解析异常
* @throws InternalException 内部异常
*/
public String getPresignedUrl(String bucketName, String objectName, int expiry) throws ServerException,
InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException,
InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(objectName)
.expiry(expiry)
.build());
}
}

View File

@ -1,5 +1,6 @@
package com.nailart.vo; package com.nailart.vo;
import com.nailart.enums.RespCodeEnum;
import lombok.Data; import lombok.Data;
/** /**
@ -13,4 +14,28 @@ public class RespVO {
private Integer code; private Integer code;
private String msg; private String msg;
private Object data; private Object data;
public static RespVO success() {
RespVO respVO = new RespVO();
respVO.setCode(RespCodeEnum.SUCCESS.getCode());
respVO.setMsg(RespCodeEnum.SUCCESS.getMessage());
return respVO;
}
public static RespVO success(Object data) {
RespVO respVO = success();
respVO.setData(data);
return respVO;
}
public static RespVO error(RespCodeEnum respCodeEnum) {
RespVO respVO = new RespVO();
respVO.setCode(respCodeEnum.getCode());
respVO.setMsg(respCodeEnum.getMessage());
return respVO;
}
public static RespVO error(RespCodeEnum respCodeEnum, Object data) {
RespVO respVO = error(respCodeEnum);
respVO.setData(data);
return respVO;
}
} }

View File

@ -3,8 +3,15 @@ server:
servlet: servlet:
context-path: /api # 应用访问路径前缀 context-path: /api # 应用访问路径前缀
# 数据源配置 # 数据源配置
spring: spring:
servlet:
multipart:
max-file-size: 2GB
max-request-size: 5GB
file-size-threshold: 1024MB # 超过10MB的文件直接写入磁盘
location: /data/temp # 指定临时存储目录(确保有足够空间)
datasource: datasource:
type: com.alibaba.druid.pool.DruidDataSource # 使用Druid连接池 type: com.alibaba.druid.pool.DruidDataSource # 使用Druid连接池
driver-class-name: com.mysql.cj.jdbc.Driver # MySQL驱动类 driver-class-name: com.mysql.cj.jdbc.Driver # MySQL驱动类
@ -73,4 +80,14 @@ logging:
root: info # 根日志级别 root: info # 根日志级别
com.example.mapper: debug # Mapper接口日志级别(调试用) com.example.mapper: debug # Mapper接口日志级别(调试用)
file: file:
name: logs/application.log # 日志文件存储路径 name: logs/application.log # 日志文件存储路径
minio:
endpoint: http://xiaowangnas.com:9000
accessKey: 3RDAaP9M8HT4RDWszNSr # 访问密钥ID
secretKey: uM7jdTIF3Xk77rwbPq0vaQWXR16zgXHqYwSRiHez # 访问密钥Secret
region: # 存储桶所在区域
# 超时配置(秒)
connectTimeout: 30
writeTimeout: 60
readTimeout: 60

View File

@ -1,7 +1,6 @@
<template> <template>
<div id="app"> <div id="app">
<!-- 固定顶部导航栏 -->
<NavBar class="nav-bar" />
<!-- 中间内容区包裹路由视图控制动画范围 --> <!-- 中间内容区包裹路由视图控制动画范围 -->
<div class="content-container"> <div class="content-container">
@ -11,25 +10,22 @@
<!-- </transition> --> <!-- </transition> -->
</div> </div>
<!-- 固定底部TabBar -->
<TabBar class="tab-bar" />
</div> </div>
</template> </template>
<script> <script>
import TabBar from './components/TabBar.vue';
import NavBar from './components/NavBar.vue';
export default { export default {
name: 'App', name: 'App',
components: { components: {
TabBar,
NavBar
} }
} }
</script> </script>
<style lang="less" scoped> <style lang="less">
#app { #app {
font-family: Avenir, Helvetica, Arial, sans-serif; font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@ -39,7 +35,15 @@ export default {
overflow: hidden; overflow: hidden;
min-width: 200px; min-width: 200px;
} }
html {
font-size: 20px; /* 基准字体大小 */
}
@media (max-width: 768px) {
html {
font-size: 14px; /* 在小屏幕上缩小基准字体大小 */
}
}
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -61,7 +65,7 @@ export default {
/* 中间内容区避开上下Bar的位置 */ /* 中间内容区避开上下Bar的位置 */
.content-container { .content-container {
position: relative; position: relative;
height: calc(100vh - 110px); // height: calc(100vh - 110px);
overflow: hidden; overflow: hidden;
/* 防止内容溢出 */ /* 防止内容溢出 */
} }

View File

@ -0,0 +1,47 @@
<template>
<div class="tab-bar">
<van-tabbar v-model="active" route :placeholder="true">
<van-tabbar-item replace to="/yumi">
<span>主页管理</span>
<template #icon="props">
<img :src="props.active ? icon.active : icon.inactive" />
</template>
</van-tabbar-item>
<van-tabbar-item replace to="/portfolioMan" icon="search"><span>作品集管理</span>
<template #icon="props">
<img :src="props.active ? icon.active : icon.inactive" />
</template></van-tabbar-item>
<van-tabbar-item replace to="/sampleCollectionman" icon="setting-o"><span>样品集管理</span>
<template #icon="props">
<img :src="props.active ? icon.active : icon.inactive" />
</template></van-tabbar-item>
<van-tabbar-item replace to="/tagManPage" icon="setting-o"><span>标签管理</span>
<template #icon="props">
<img :src="props.active ? icon.active : icon.inactive" />
</template></van-tabbar-item>
</van-tabbar>
</div>
</template>
<script>
export default {
name: 'ManTabBar',
components: {},
props: {},
data() {
return {
active: 0,
icon: {
active: 'https://img01.yzcdn.cn/vant/user-active.png',
inactive: 'https://img01.yzcdn.cn/vant/user-inactive.png',
},
};
},
watch: {},
computed: {},
methods: {},
created() { },
mounted() { }
};
</script>
<style lang="less" scoped></style>

View File

@ -1,9 +1,7 @@
<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="50" :fixed="true"> <van-nav-bar title="屿 ● Nail" left-text="" :placeholder="true" :safe-area-inset-top="true" z-index="50" :fixed="true">
<template #right>
<van-icon name="search" size="18" />
</template>
</van-nav-bar> </van-nav-bar>
</div> </div>
</template> </template>

View File

@ -7,11 +7,11 @@
<img :src="props.active ? icon.active : icon.inactive" /> <img :src="props.active ? icon.active : icon.inactive" />
</template> </template>
</van-tabbar-item> </van-tabbar-item>
<van-tabbar-item replace to="/portfolio" icon="search"><span>作品集</span> <van-tabbar-item replace to="/portfolio" icon="search"><span>作品集</span>
<template #icon="props"> <template #icon="props">
<img :src="props.active ? icon.active : icon.inactive" /> <img :src="props.active ? icon.active : icon.inactive" />
</template></van-tabbar-item> </template></van-tabbar-item>
<van-tabbar-item replace to="/sampleCollection" icon="setting-o"><span>样品集</span> <van-tabbar-item replace to="/sampleCollection" icon="setting-o"><span>样品集</span>
<template #icon="props"> <template #icon="props">
<img :src="props.active ? icon.active : icon.inactive" /> <img :src="props.active ? icon.active : icon.inactive" />
</template></van-tabbar-item> </template></van-tabbar-item>

View File

@ -3,6 +3,11 @@ 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' import axios from './utils/request'
import { FileServicePlugin } from './utils/file';
// 注册插件
Vue.use(FileServicePlugin , {
baseUrl: 'http://wcy111.top:35001' // 可选API基础URL
});
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";
@ -17,7 +22,36 @@ import { Swipe, SwipeItem } from 'vant';
import { Loading } from 'vant'; import { Loading } from 'vant';
import { NoticeBar } from 'vant'; import { NoticeBar } from 'vant';
import { Sticky } from 'vant'; import { Sticky } from 'vant';
import { Tab, Tabs } from 'vant';
import { List } from 'vant';
import { Overlay } from 'vant';
import { Form } from 'vant';
import { Field } from 'vant';
import { Uploader } from 'vant';
import { Dialog } from 'vant';
import { Picker } from 'vant';
import { DropdownMenu, DropdownItem } from 'vant';
import { Checkbox, CheckboxGroup } from 'vant';
import { Cell, CellGroup } from 'vant';
import { Divider } from 'vant';
Vue.use(Divider);
Vue.use(Cell);
Vue.use(CellGroup);
Vue.use(Checkbox);
Vue.use(CheckboxGroup);
Vue.use(DropdownMenu);
Vue.use(DropdownItem);
Vue.use(Picker);
// 全局注册
Vue.use(Dialog);
Vue.use(Uploader);
Vue.use(Form);
Vue.use(Field);
Vue.use(Overlay);
Vue.use(List);
Vue.use(Tab);
Vue.use(Tabs);
Vue.use(Sticky); Vue.use(Sticky);
Vue.use(NoticeBar); Vue.use(NoticeBar);
Vue.use(Loading); Vue.use(Loading);

View File

@ -0,0 +1,22 @@
<template>
<div class="detail-page">
</div>
</template>
<script>
export default {
components: {},
props: {},
data() {
return {
};
},
watch: {},
computed: {},
methods: {},
created() { },
mounted() { }
};
</script>
<style lang="less" scoped></style>

View File

@ -1,36 +1,69 @@
<template> <template>
<div> <div class="homepage">
<div class="homepage"> <!-- 固定顶部导航栏 -->
<h1>主页</h1> <NavBar class="nav-bar" />
<div class="images">
<h1>轮播图区域</h1>
</div> </div>
<div class="info">
<h1>信息展示区包含店名营业时间状态地址电话图标微信图标</h1>
</div>
<div class="new">
<h1>新品展示</h1>
</div>
<div class="navigation">
<h1>暂定导航组件</h1>
</div>
<!-- 固定底部TabBar -->
<TabBar class="tab-bar" />
</div> </div>
</template> </template>
<script> <script>
import TabBar from '@/components/TabBar.vue';
import NavBar from '@/components/NavBar.vue';
export default { export default {
name: 'HomePage', name: 'HomePage',
components: { components: {
TabBar,
NavBar
}, },
props: {}, props: {},
data() { data() {
return { return {
}; };
}, },
watch: {}, watch: {},
computed: {}, computed: {},
methods: {}, methods: {},
created() {}, created() { },
mounted() {} mounted() { }
}; };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.homepage { .homepage {
width: 100vw; width: 100vw;
height: 100%; height: 100vh;
overflow: auto; overflow: auto;
.images {
width: 100%;
height: 25vh;
}
.info {
width: 100%;
height: 20vh;
}
.new {
width: 100%;
}
.navigation {
width: 100%;
}
} }
</style> </style>

View File

@ -1,5 +1,11 @@
<template> <template>
<div class="page-container"> <div class="page-container">
<!-- 固定顶部导航栏 -->
<NavBar class="nav-bar" />
<van-tabs @click="onClickTab">
<van-tab v-for="tab in tagInfo" :title="tab.tagName" :key="tab.id" :name="tab.id">
</van-tab>
</van-tabs>
<!-- 使用原生 sticky 定位的公告栏 --> <!-- 使用原生 sticky 定位的公告栏 -->
<div class="sticky-notice"> <div class="sticky-notice">
<van-notice-bar left-icon="volume-o" :scrollable="true" text="所有标价均为款式价格,如需延长需补差价喔~~~半贴¥90,浅贴¥130" /> <van-notice-bar left-icon="volume-o" :scrollable="true" text="所有标价均为款式价格,如需延长需补差价喔~~~半贴¥90,浅贴¥130" />
@ -7,6 +13,10 @@
<!-- 内容容器 --> <!-- 内容容器 -->
<div class="cards-container"> <div class="cards-container">
<div class="card_loading" v-if="this.card_loading"
style="width: 100%;display: flex;justify-content: center;">
<van-loading size="24px">加载中...</van-loading>
</div>
<div class="card-grid"> <div class="card-grid">
<!-- 卡片列表 --> <!-- 卡片列表 -->
<div v-for="(card, index) in cards" :key="index" class="card" <div v-for="(card, index) in cards" :key="index" class="card"
@ -16,21 +26,23 @@
<div class="card-image-container"> <div class="card-image-container">
<van-swipe :autoplay="3000"> <van-swipe :autoplay="3000">
<van-swipe-item v-for="(image, imgIndex) in card.images" :key="imgIndex"> <van-swipe-item v-for="(image, imgIndex) in card.images" :key="imgIndex">
<van-image lazy-load fit="contain" :src="image"> <van-image lazy-load fit="fill" :src="image" class="card-image">
<template v-slot:loading> <template v-slot:loading>
<van-loading type="spinner" size="20" /> <van-loading type="spinner" size="20" />
</template> </template>
</van-image> </van-image>
</van-swipe-item> </van-swipe-item>
</van-swipe> </van-swipe>
</div> </div>
<div class="card-content"> <div class="card-content">
<h2 class="card-title">{{ card.title }}</h2> <h2 class="card-title">{{ card.title }}</h2>
<p class="card-description">{{ card.description }}</p> <p class="card-description">{{ card.description }}</p>
<div v-if="card.tag" class="card-tag"> <div class="card-tag">
<van-tag round type="primary">{{ card.tag }}</van-tag> <van-tag v-for="(tag, tagIndex) in card.tagInfo" :key="tagIndex" round type="primary"
:text-color="tag.tagColor" :color="tag.tagBgColor" style="margin-left: 5px;">
{{ tag.tagName }}
</van-tag>
</div> </div>
<div class="card-footer"> <div class="card-footer">
<span class="card-meta"> <span class="card-meta">
@ -50,45 +62,77 @@
</div> </div>
</div> </div>
</div> </div>
<!-- <div class="loading" v-show="loading">
<van-loading type="spinner" color="#1989fa" />
</div> -->
</div> </div>
<div class="card_loading" v-if="this.card_loading1" style="width: 100%;display: flex;justify-content: center;">
<van-loading size="24px">加载中...</van-loading>
</div>
<!-- 回到顶部按钮 -->
<div v-show="showBackToTop" class="back-to-top" @click="backToTop">
<van-icon name="arrow-up" size="16" color="#fff" />
</div>
<!-- 固定底部TabBar -->
<TabBar class="tab-bar" />
</div> </div>
</template> </template>
<script> <script>
import TabBar from '@/components/TabBar.vue';
import NavBar from '@/components/NavBar.vue';
export default { export default {
name: 'PortfolioPage', name: 'PortfolioPage',
components: {
TabBar,
NavBar
},
data() { data() {
return { return {
cards: [], cards: [],
card_loading: false,
card_loading1: false,
activeCardIndex: -1, activeCardIndex: -1,
page: 1, pageNum: 1,
pageSize: 2, pageSize: 20,
loading: false, loading: false,
changes: [], changes: [],
totalPages: 0 totalPages: 0,
showBackToTop: false, // /
scrollContainer: null,
scrollHandler: null,
tagInfo: [{ "id": 0, "tagName": "全部" }],
//
idToObjectMap: {},
tagID: 0
}; };
}, },
methods: { methods: {
async getCarts(pageNum, size) { async getCarts(tagID, pageNum, size) {
try { try {
this.loading = true; this.loading = true;
let res = await this.$axios.get('/portfolio/list', { let res = null;
params: { if (tagID == 0) {
page: pageNum, res = await this.$axios.get('/portfolio/list', {
size: size params: {
} page: pageNum,
}); size: size
}
if (res.code !== 200 && res.data.code !== 200) { })
const errorMsg = res.msg || res.data.msg || '获取数据失败'; } else {
res = await this.$axios.get('/portfolio/getPortfolioByTag', {
params: {
tagID: tagID,
page: pageNum,
size: size
}
})
}
if (res.code !== 200) {
const errorMsg = res.msg || '获取数据失败';
this.$toast.fail(errorMsg); this.$toast.fail(errorMsg);
return; return;
} }
const records = res.data?.data?.records || []; const records = res.data.records || [];
if (records.length === 0) { if (records.length === 0) {
this.$toast('没有更多数据了'); this.$toast('没有更多数据了');
return; return;
@ -98,46 +142,93 @@ export default {
const images = item.imgFilename const images = item.imgFilename
? item.imgFilename.split('&&').map(filename => `${item.imgUrl}/${filename}`) ? item.imgFilename.split('&&').map(filename => `${item.imgUrl}/${filename}`)
: []; : [];
let tagInfo = [];
const tagStyle = item.tagStyle for (let tagID of item.tagId.split("&&")) {
? { backgroundColor: item.tagStyle.split('&&')[0], color: item.tagStyle.split('&&')[1] } tagInfo.push(this.idToObjectMap[tagID])
: { backgroundColor: "#eeeeee", color: "#fff" }; }
return { return {
id: item.id, id: item.id,
title: item.title, title: item.title,
description: item.description, description: item.description,
images, images,
duration: '', duration: '',
tag: item.tag || '热门', tagInfo,
tagStyle,
loves: item.loves || 0, loves: item.loves || 0,
changes: 0, changes: 0,
checked: false, checked: false,
amt: item.amt || 0, amt: item.amt || 0,
}; };
}); });
this.cards = [...this.cards, ...formattedCards]; this.cards = [...this.cards, ...formattedCards];
this.loading = false; this.loading = false;
this.totalPages = res.data.data.pages; this.card_loading = false;
this.card_loading1 = false;
this.totalPages = res.data.pages;
this.pageNum++;
} catch (error) { } catch (error) {
console.error('获取卡片数据失败:', error); console.error('获取卡片数据失败:', error);
this.loading = false; this.loading = false;
this.card_loading = false;
this.$toast.fail('网络错误,请稍后重试'); this.$toast.fail('网络错误,请稍后重试');
} }
}, },
async getTagInfo() {
try {
let res = await this.$axios.get('/tagInfo/getAll');
if (res.code !== 200) {
const errorMsg = res.msg || '获取数据失败';
this.$toast.fail(errorMsg);
return;
}
this.tagInfo = [...this.tagInfo, ...res.data];
//
res.data.forEach(obj => {
this.idToObjectMap[obj.id] = obj;
});
} catch (error) {
console.error('获取标签信息失败:', error);
this.$toast.fail('网络错误,请稍后重试');
}
},
//
onClickTab(name, title) {
console.log(`标签 ${title} 被点击`);
this.card_loading = true;
this.cards = [];
this.tagID = name;
this.pageNum = 1;
this.getCarts(this.tagID, this.pageNum, this.pageSize);
},
/**
* 回到顶部
* @param index
*/
handleTouchStart(index) { handleTouchStart(index) {
this.activeCardIndex = index; this.activeCardIndex = index;
}, },
/**
* 处理触摸结束事件
*/
handleTouchEnd() { handleTouchEnd() {
setTimeout(() => { setTimeout(() => {
this.activeCardIndex = -1; this.activeCardIndex = -1;
}, 300); }, 300);
}, },
/**
* 卡片点击事件
* @param card 点击的卡片
*/
handleCardClick(card) { handleCardClick(card) {
console.log('点击了卡片:', card.title); console.log('点击了卡片:', card.title);
}, },
/**
* 点赞处理
* @param card 点击的卡片
*/
handleClickLove(card) { handleClickLove(card) {
if (card.checked) { if (card.checked) {
card.loves++; card.loves++;
@ -153,6 +244,7 @@ export default {
changes: card.changes changes: card.changes
}); });
}, },
async savaLoveChanes() { async savaLoveChanes() {
for (const change of this.changes) { for (const change of this.changes) {
try { try {
@ -168,25 +260,44 @@ export default {
} }
} }
}, },
handleScroll() {
const container = document.querySelector('.cards-container');
if (!container) return;
const scrollTop = container.scrollTop; handleScroll() {
const clientHeight = container.clientHeight; if (!this.scrollContainer) return;
const scrollHeight = container.scrollHeight; const scrollTop = this.scrollContainer.scrollTop;
// 300px
this.showBackToTop = scrollTop > 300;
const clientHeight = this.scrollContainer.clientHeight;
const scrollHeight = this.scrollContainer.scrollHeight;
if (scrollTop + clientHeight >= scrollHeight - 50) { if (scrollTop + clientHeight >= scrollHeight - 50) {
if (!this.loading && this.page < this.totalPages) { if (this.pageNum == this.totalPages) {
this.page++; this.$toast.fail('已经到底啦~~');
this.getCarts(this.page, this.pageSize || 2); return;
console.log('加载更多数据,当前页:', this.page); }
if (!this.loading && this.pageNum < this.totalPages) {
this.card_loading1 = true;
console.log(this.tagID);
this.getCarts(this.tagID, this.pageNum + 1, this.pageSize);
console.log('加载更多数据,当前页:', this.pageNum);
} }
} }
},
//
backToTop() {
if (!this.scrollContainer) return;
// 使
this.scrollContainer.scrollTo({
top: 0,
behavior: 'smooth'
});
} }
}, },
mounted() { mounted() {
this.getCarts(this.page, this.pageSize || 2); this.getTagInfo();
this.tagID = 0;
this.getCarts(this.tagID, this.pageNum, this.pageSize || 2);
this.scrollContainer = document.querySelector('.cards-container'); this.scrollContainer = document.querySelector('.cards-container');
if (this.scrollContainer) { if (this.scrollContainer) {
this.scrollHandler = () => this.handleScroll(); this.scrollHandler = () => this.handleScroll();
@ -204,7 +315,6 @@ export default {
} }
}; };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
// //
@base-bg: #f9fafb; @base-bg: #f9fafb;
@ -222,34 +332,33 @@ export default {
.page-container { .page-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100vh;
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
padding: 0 20px;
// //
.sticky-notice { .sticky-notice {
position: sticky; position: sticky;
top: 0; top: 50px;
z-index: 100; z-index: 100;
margin-bottom: 10px; margin-bottom: 10px;
} }
// //
.cards-container { .cards-container {
padding: 0 5px;
overflow: auto; overflow: auto;
padding-bottom: 20px;
// //
.card-grid { .card-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 24px; gap: 5px;
// // //
@media (max-width: 600px) { // @media (max-width: 600px) {
grid-template-columns: 1fr; // grid-template-columns: 1fr;
} // }
// //
.card { .card {
@ -259,7 +368,7 @@ export default {
box-shadow: @shadow-base; box-shadow: @shadow-base;
transition: all 0.3s ease; transition: all 0.3s ease;
cursor: pointer; cursor: pointer;
padding-bottom: 5px;
// //
&:hover { &:hover {
transform: translateY(-5px); transform: translateY(-5px);
@ -275,36 +384,38 @@ export default {
.card-image-container { .card-image-container {
position: relative; position: relative;
// .card-image {
.card-tag { width: 100%;
min-height: 100px;
}
padding: 4px 12px; //
border-radius: 9999px; .card-tag {
font-size: 14px; transform: translateX(-5px);
font-weight: 500;
} }
} }
// //
.card-content { .card-content {
padding: 20px; padding: 5px;
position: relative; position: relative;
// //
.card-title { .card-title {
font-size: 18px; padding-left: 5px;
font-size: 1.2rem;
font-weight: bold; font-weight: bold;
color: @text-primary; color: @text-primary;
margin-bottom: 8px; margin-bottom: 10px;
} }
// //
.card-description { .card-description {
padding-left: 5px;
color: @text-secondary; color: @text-secondary;
text-wrap: balance; text-wrap: balance;
margin-bottom: 12px; margin-bottom: 10px;
line-height: 1.5; line-height: 1.5;
font-size: 14px; font-size: 1rem;
} }
// //
@ -318,13 +429,12 @@ export default {
// //
.card-meta { .card-meta {
color: #9932CC; color: #FFFAFA;
font-weight: 500; font-weight: 500;
font-size: 20px; font-size: 0.9rem;
background-color: #D8BFD8; background-color: #8470FF;
border-radius: 8px; border-radius: 8px;
padding: 0 5px 0 5px; padding: 5px;
// //
.fa { .fa {
margin-right: 1px; margin-right: 1px;
@ -416,6 +526,29 @@ export default {
height: 100px; height: 100px;
} }
} }
//
.back-to-top {
position: fixed;
bottom: 60px;
right: 20px;
background-color: rgba(243, 148, 227, 0.8); // 使
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 100;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
&:hover {
background-color: @accent-dark;
transform: translateY(-2px);
}
}
} }
// //

View File

@ -1,112 +1,298 @@
<template> <template>
<div class="sample-collection-page"> <div class="sample-collection-page">
<div class="card-grid"> <!-- 固定顶部导航栏 -->
<!-- 卡片列表 --> <NavBar class="nav-bar" />
<div v-for="(card, index) in cards" :key="index" class="card" <van-tabs @click="onClickTab">
:class="{ 'card-active': activeCardIndex === index }" @touchstart="handleTouchStart(index)" <van-tab v-for="tab in tagInfo" :title="tab.tagName" :key="tab.id" :name="tab.id">
@touchend="handleTouchEnd(index)" @click="handleCardClick(card)"> </van-tab>
</van-tabs>
<!-- 使用原生 sticky 定位的公告栏 -->
<div class="sticky-notice">
<van-notice-bar left-icon="volume-o" :scrollable="true" text="所有标价均为款式价格,如需延长需补差价喔~~~半贴¥90,浅贴¥130" />
</div>
<div class="card-image-container"> <!-- 内容容器 -->
<van-swipe :autoplay="3000"> <div class="cards-container">
<van-swipe-item v-for="(image, imgIndex) in card.images" :key="imgIndex"> <div class="card_loading" v-if="this.card_loading"
<van-image :src="image" lazy-load> style="width: 100%;display: flex;justify-content: center;">
<template v-slot:loading> <van-loading size="24px">加载中...</van-loading>
<van-loading type="spinner" size="20" /> </div>
</template> <div class="card-grid">
</van-image> <!-- 卡片列表 -->
</van-swipe-item> <div v-for="(card, index) in cards" :key="index" class="card"
</van-swipe> :class="{ 'card-active': activeCardIndex === index }" @touchstart="handleTouchStart(index)"
<div v-if="card.tag" class="card-tag" :style="card.tagStyle"> @touchend="handleTouchEnd(index)" @click="handleCardClick(card)">
{{ card.tag }}
<div class="card-image-container">
<van-swipe :autoplay="3000">
<van-swipe-item v-for="(image, imgIndex) in card.images" :key="imgIndex">
<van-image lazy-load fit="fill" :src="image" class="card-image">
<template v-slot:loading>
<van-loading type="spinner" size="20" />
</template>
</van-image>
</van-swipe-item>
</van-swipe>
</div> </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 class="card-content">
<h2 class="card-title">{{ card.title }}</h2>
<p class="card-description">{{ card.description }}</p>
<div class="card-tag">
<van-tag v-for="(tag, tagIndex) in card.tagInfo" :key="tagIndex" round type="primary"
:text-color="tag.tagColor" :color="tag.tagBgColor" style="margin-left: 5px;">
{{ tag.tagName }}
</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> </div>
</div> </div>
</div> </div>
<div class="card_loading" v-if="this.card_loading1" style="width: 100%;display: flex;justify-content: center;">
<van-loading size="24px">加载中...</van-loading>
</div>
<!-- 回到顶部按钮 -->
<div v-show="showBackToTop" class="back-to-top" @click="backToTop">
<van-icon name="arrow-up" size="16" color="#fff" />
</div>
<!-- 固定底部TabBar -->
<TabBar class="tab-bar" />
</div> </div>
</template> </template>
<script> <script>
import TabBar from '@/components/TabBar.vue';
import NavBar from '@/components/NavBar.vue';
export default { export default {
name: 'SampleCollectionPage', name: 'SampleCollectionPage',
components: {}, components: {
props: {}, TabBar,
NavBar
},
data() { data() {
return { return {
cards: [ cards: [],
{ card_loading: false,
title: "探索自然之美", card_loading1: false,
description: "深入原始森林,感受大自然的鬼斧神工。这里有清澈的溪流、茂密的树木和各种珍稀野生动物。", activeCardIndex: -1,
images: ["https://picsum.photos/seed/card1/600/800", "https://picsum.photos/seed/card2/600/800"], pageNum: 1,
pageSize: 20,
tag: "热门", loading: false,
tagStyle: { changes: [],
backgroundColor: "#F97316", totalPages: 0,
color: "#fff" showBackToTop: false, // /
}, scrollContainer: null,
status: 0, scrollHandler: null,
employee: "小吕&&寒冰射手", tagInfo: [{ "id": 0, "tagName": "全部" }],
neddTime: 2.0 //
} idToObjectMap: {},
], tagID: 0
activeCardIndex: -1
}; };
},
watch: {},
computed: {
}, },
methods: { methods: {
async getCarts(tagID, pageNum, size) {
try {
this.loading = true;
let res = null;
if (tagID == 0) {
res = await this.$axios.get('/sampleCollection/list', {
params: {
page: pageNum,
size: size
}
})
} else {
res = await this.$axios.get('/sampleCollection/getPortfolioByTag', {
params: {
tagID: tagID,
page: pageNum,
size: size
}
})
}
console.log(res);
if (res.code !== 200) {
const errorMsg = res.msg || '获取数据失败';
this.$toast.fail(errorMsg);
return;
}
const records = res.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}`)
: [];
let tagInfo = [];
for (let tagID of item.tagId.split("&&")) {
tagInfo.push(this.idToObjectMap[tagID])
}
return {
id: item.id,
title: item.title,
description: item.description,
images,
duration: '',
tagInfo,
checked: false,
amt: item.amt || 0,
};
});
this.cards = [...this.cards, ...formattedCards];
console.log(this.cards);
this.loading = false;
this.card_loading = false;
this.card_loading1 = false;
this.totalPages = res.data.pages;
this.pageNum++;
} catch (error) {
console.error('获取卡片数据失败:', error);
this.loading = false;
this.card_loading = false;
this.$toast.fail('网络错误,请稍后重试');
}
},
async getTagInfo() {
try {
let res = await this.$axios.get('/tagInfo/getAll');
if (res.code !== 200) {
const errorMsg = res.msg || '获取数据失败';
this.$toast.fail(errorMsg);
return;
}
this.tagInfo = [...this.tagInfo, ...res.data];
//
res.data.forEach(obj => {
this.idToObjectMap[obj.id] = obj;
});
} catch (error) {
console.error('获取标签信息失败:', error);
this.$toast.fail('网络错误,请稍后重试');
}
},
//
onClickTab(name, title) {
console.log(`标签 ${title} 被点击`);
this.card_loading = true;
this.cards = [];
this.tagID = name;
this.pageNum = 1;
this.getCarts(this.tagID, this.pageNum, this.pageSize);
},
/**
* 回到顶部
* @param index
*/
handleTouchStart(index) { handleTouchStart(index) {
this.activeCardIndex = index; this.activeCardIndex = index;
}, },
/**
* 处理触摸结束事件
*/
handleTouchEnd() { handleTouchEnd() {
setTimeout(() => { setTimeout(() => {
this.activeCardIndex = -1; this.activeCardIndex = -1;
}, 300); }, 300);
}, },
/**
* 卡片点击事件
* @param card 点击的卡片
*/
handleCardClick(card) { handleCardClick(card) {
console.log('点击了卡片:', card.title); console.log('点击了卡片:', card.title);
}, },
/**
handleButtonClick(card) { * 点赞处理
console.log('点击了了解更多:', card.title); * @param card 点击的卡片
*/
handleClickLove(card) {
if (card.checked) {
card.loves++;
card.changes++;
} else {
if (card.loves > 0) {
card.loves--;
card.changes--;
}
}
this.changes.push({
id: card.id,
changes: card.changes
});
}, },
handleScroll() {
if (!this.scrollContainer) return;
const scrollTop = this.scrollContainer.scrollTop;
// 300px
this.showBackToTop = scrollTop > 300;
const clientHeight = this.scrollContainer.clientHeight;
const scrollHeight = this.scrollContainer.scrollHeight;
if (scrollTop + clientHeight >= scrollHeight - 50) {
if (this.pageNum == this.totalPages) {
this.$toast.fail('已经到底啦~~');
return;
}
if (!this.loading && this.pageNum < this.totalPages) {
this.card_loading1 = true;
console.log(this.tagID);
this.getCarts(this.tagID, this.pageNum + 1, this.pageSize);
console.log('加载更多数据,当前页:', this.pageNum);
}
}
},
//
backToTop() {
if (!this.scrollContainer) return;
// 使
this.scrollContainer.scrollTo({
top: 0,
behavior: 'smooth'
});
}
}, },
mounted() { mounted() {
// this.getTagInfo();
const buttons = document.querySelectorAll('.card-button'); this.tagID = 0;
buttons.forEach(button => { this.getCarts(this.tagID, this.pageNum, this.pageSize || 2);
button.addEventListener('touchstart', () => { this.scrollContainer = document.querySelector('.cards-container');
button.classList.add('button-active'); if (this.scrollContainer) {
}); this.scrollHandler = () => this.handleScroll();
this.scrollContainer.addEventListener('scroll', this.scrollHandler);
button.addEventListener('touchend', () => { }
setTimeout(() => { },
button.classList.remove('button-active'); beforeDestroy() {
}, 200); if (this.scrollContainer && this.scrollHandler) {
}); this.scrollContainer.removeEventListener('scroll', this.scrollHandler);
}); }
}, },
created() { },
}; };
</script> </script>
@ -123,199 +309,252 @@ export default {
@shadow-base: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); @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); @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 { .sample-collection-page {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
padding: 0 20px;
.page-title { //
font-size: clamp(1.5rem, 3vw, 2rem); .sticky-notice {
font-weight: bold; position: sticky;
color: @text-primary; top: 50px;
margin-bottom: 32px; z-index: 100;
text-align: center; margin-bottom: 10px;
} }
// //
.card-grid { .cards-container {
display: grid; padding: 0 5px;
grid-template-columns: repeat(2, 1fr); overflow: auto;
gap: 24px;
// //
@media (max-width: 600px) { .card-grid {
grid-template-columns: 1fr; display: grid;
} grid-template-columns: repeat(2, 1fr);
} gap: 5px;
// // //
.card { // @media (max-width: 600px) {
background-color: @card-bg; // grid-template-columns: 1fr;
border-radius: 16px; // }
overflow: hidden;
box-shadow: @shadow-base;
transition: all 0.3s ease;
cursor: pointer;
&:hover { //
transform: translateY(-5px); .card {
box-shadow: @shadow-active; background-color: @card-bg;
} border-radius: 16px;
overflow: hidden;
box-shadow: @shadow-base;
transition: all 0.3s ease;
cursor: pointer;
padding-bottom: 5px;
&.card-active { //
transform: translateY(-5px); &:hover {
box-shadow: @shadow-active; transform: translateY(-5px);
} box-shadow: @shadow-active;
}
// &.card-active {
.card-image-container { transform: translateY(-5px);
position: relative; box-shadow: @shadow-active;
}
// //
.card-tag { .card-image-container {
position: absolute; position: relative;
top: 16px;
right: 16px;
padding: 4px 12px;
border-radius: 9999px;
font-size: 14px;
font-weight: 500;
}
}
// .card-image {
.card-content { width: 100%;
padding: 20px; min-height: 100px;
}
.card-title { //
font-size: 18px; .card-tag {
font-weight: bold; transform: translateX(-5px);
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 { .card-content {
display: flex; padding: 5px;
align-items: center; position: relative;
gap: 4px;
.like-count { //
font-size: 13px; .card-title {
padding-left: 5px;
font-size: 1.2rem;
font-weight: bold;
color: @text-primary;
margin-bottom: 10px;
}
//
.card-description {
padding-left: 5px;
color: @text-secondary; color: @text-secondary;
text-wrap: balance;
margin-bottom: 10px;
line-height: 1.5;
font-size: 1rem;
} }
.action-btn { //
position: relative; .card-footer {
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; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: space-between;
margin-top: 12px;
position: relative;
bottom: 0px;
&::before { //
content: '❤'; .card-meta {
font-size: 1.25rem; color: #FFFAFA;
color: @gray-light; font-weight: 500;
transition: color 0.3s, transform 0.3s; font-size: 0.9rem;
position: relative; background-color: #8470FF;
line-height: 1; border-radius: 8px;
vertical-align: middle; padding: 5px;
transform-origin: center;
//
.fa {
margin-right: 1px;
}
} }
}
// //
.heart-checkbox:checked~.heart-btn .heart-icon::before { .heart {
color: @danger-color; display: flex;
animation: heartBeat 0.5s cubic-bezier(0.17, 0.89, 0.32, 1.49); 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; .loading {
padding: 4px 8px; display: flex;
border-radius: 9999px; justify-content: center;
background-color: #7B68EE; align-items: center;
color: white; height: 100px;
}
} }
} }
//
.back-to-top {
position: fixed;
bottom: 60px;
right: 20px;
background-color: rgba(243, 148, 227, 0.8); // 使
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 100;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
&:hover {
background-color: @accent-dark;
transform: translateY(-2px);
}
}
}
//
@keyframes heartBeat {
0% {
transform: scale(1);
}
25% {
transform: scale(1.3);
}
50% {
transform: scale(1);
}
75% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
} }
</style> </style>

View File

@ -0,0 +1,358 @@
<template>
<div class="home-man-page">
<!-- 固定顶部导航栏 -->
<NavBar class="nav-bar" />
<div class="images">
<van-swipe class="swipe" :autoplay="2000" indicator-color="white">
<van-swipe-item v-for="(image, index) in info.headImgs" :key="index">
<img fill="contain" v-lazy="image.url" />
</van-swipe-item>
</van-swipe>
</div>
<div class="info">
<div class="title" @click="showpop">
<div>{{ info.name }}</div>
</div>
<van-divider />
<div class="worktime">
<div>{{ info.status }} {{ info.worktime }}</div>
</div>
<van-divider />
<div class="location">
<div class="left">
<span style="color: black; font-size: 15px;">{{ info.location }}</span>
<br>
<van-icon name="location-o" size="12" />
<span>距您驾车或者打车8.0公里 需10分钟</span>
<br>
<van-icon name="guide-o" size="12" />
<span>距离1号线微电园站1号口步行400米 需8分钟</span>
</div>
<div class="right">
<van-icon name="wechat" size="25" />
<van-icon name="phone" size="25" />
</div>
</div>
</div>
<van-divider>新品展示</van-divider>
<div class="new">
<van-swipe class="swipe" :autoplay="2000" indicator-color="white">
<van-swipe-item v-for="(image, index) in info.newPortfolioImgs" :key="index">
<img fill="contain" v-lazy="image.url" />
</van-swipe-item>
</van-swipe>
</div>
<van-popup position="bottom" v-model="show" style="height: 80%;" closeable :close-on-click-overlay="false">
<van-form @submit="saveChangeInfo">
<van-field name="headImgs" label="主页店图">
<template #input>
<van-uploader v-model="info.headImgs" :after-read="uploadHeadImgs" />
</template>
</van-field>
<van-field v-model="info.name" name="name" label="店名" placeholder="店名"
:rules="[{ required: true, message: '请填写店名' }]" />
<van-field v-model="info.worktime" name="worktime" label="营业信息" placeholder="营业信息"
:rules="[{ required: true, message: '请填写营业信息' }]" />
<van-field readonly clickable label="营业状态" :value="info.status" placeholder="选择营业状态" @click="showPicker = true" />
<van-popup v-model="showPicker" round position="bottom">
<van-picker show-toolbar :columns="columns" @cancel="showPicker = false" @confirm="onPicker" />
</van-popup>
<van-field v-model="info.location" name="location" label="地址" placeholder="地址"
:rules="[{ required: true, message: '请填写地址' }]" />
<van-field v-model="info.wechat" name="wechat" label="微信" placeholder="微信"
:rules="[{ required: true, message: '请填写微信' }]" />
<van-field v-model="info.phone" name="phone" label="电话" placeholder="电话"
:rules="[{ required: true, message: '请填写电话' }]" />
<van-field name="locationImgs" label="指引图">
<template #input>
<van-uploader v-model="info.locationImgs" :after-read="uploadLocationImgs" />
</template>
</van-field>
<van-field name="newPortfolioImgs" label="新品图">
<template #input>
<van-uploader v-model="info.newPortfolioImgs" :after-read="uploadnewPortfolioImgs" />
</template>
</van-field>
<van-field name="environmentImgs" label="环境图">
<template #input>
<van-uploader v-model="info.environmentImgs" :after-read="uploadEnvironmentImgs" />
</template>
</van-field>
<div style="margin: 16px;">
<van-button round block type="info" native-type="submit">提交</van-button>
</div>
</van-form>
</van-popup>
<!-- 固定底部TabBar -->
<ManTabBar class="tab-bar" />
</div>
</template>
<script>
import NavBar from '@/components/NavBar.vue';
import ManTabBar from '@/components/ManTabBar.vue';
export default {
name: 'HomeManPage',
components: {
ManTabBar,
NavBar
},
props: {},
data() {
return {
show: false,
info: {
id: 1,
headImgs: [],
guide: '距离1号线微电园站1号口步行400米 需8分钟',
wechat: '',
phone: '',
newPortfolioImgs: [],
locationImgs: [],
environmentImgs: [
],
publicityImg: '',
status: ""
},
storageUrl: 'http://xiaowangnas.com:9000/yumiartnail',
saveInfo: {
id: 1,
headImg: '',
name: '',
worktime: '',
address: '',
wechat: '',
phone: '',
locationImg: '',
newPortfolioImg: '',
environmentImg: '',
publicityImg: '',
},
showPicker: false,
columns: ['营业中', '休息中'],
};
},
watch: {},
computed: {},
methods: {
async getInfo() {
const res = await this.$axios.get('/mainInfo/getMainInfo');
console.log(res);
if (res.code == 200) {
this.id = res.data.id;
this.info.headImgs = this.convertToObjectArray(res.data.headImg.split('&&'));
this.info.name = res.data.name;
this.info.status = res.data.status == 0 ? '休息中' : '营业中';
this.info.worktime = res.data.worktime;
this.info.location = res.data.address;
this.info.wechat = res.data.wechat;
this.info.phone = res.data.phone;
this.info.locationImgs = this.convertToObjectArray(res.data.locationImg.split('&&'));
this.info.newPortfolioImgs = this.convertToObjectArray(res.data.newPortfolioImg.split('&&'));
this.info.environmentImgs = this.convertToObjectArray(res.data.environmentImg.split('&&'));
}
},
async saveChangeInfo() {
this.show = false;
this.saveInfo.id = this.info.id;
this.saveInfo.headImg = this.convertUrlsToString(this.info.headImgs);
this.saveInfo.name = this.info.name;
this.saveInfo.worktime = this.info.worktime;
this.saveInfo.address = this.info.location;
this.saveInfo.wechat = this.info.wechat;
this.saveInfo.phone = this.info.phone;
this.saveInfo.locationImg = this.convertUrlsToString(this.info.locationImgs);
this.saveInfo.newPortfolioImg = this.convertUrlsToString(this.info.newPortfolioImgs);
this.saveInfo.environmentImg = this.convertUrlsToString(this.info.environmentImgs);
console.log(this.saveInfo);
const res = await this.$axios.post('/mainInfo/updateMainInfo', this.saveInfo);
console.log(res);
if (res.code == 200) {
this.$toast.success('修改成功');
} else {
this.$toast.fail('修改失败');
}
},
showpop() {
this.show = true;
},
uploadHeadImgs(file) {
this.uploadImgs(file, 'headImgs')
},
uploadLocationImgs(file) {
this.uploadImgs(file, 'locationImgs')
},
uploadnewPortfolioImgs(file) {
this.uploadImgs(file, 'newPortfolioImgs')
},
uploadEnvironmentImgs(file) {
this.uploadImgs(file, 'environmentImgs')
},
//
async uploadImgs(file, type) {
try {
const filePath = `${type}/${Date.now()}-${file.file.name}`;
let result = await this.$fileService.uploadFile({
bucketName: 'yumiartnail',
filePath: filePath,
file: file.file,
});
result = `${this.storageUrl}/${result}`;
switch (type) {
case 'headImgs':
this.info.headImgs.pop();
this.info.headImgs.push({ url: result });
break;
case 'locationImgs':
this.info.locationImgs.pop();
this.info.locationImgs.push({ url: result });
break;
case 'newPortfolioImgs':
this.info.newPortfolioImgs.pop();
this.info.newPortfolioImgs.push({ url: result });
break;
case 'environmentImgs':
this.info.environmentImgs.pop();
this.info.environmentImgs.push({ url: result });
break;
default:
break;
}
} catch (error) {
// this.uploadResult = `: ${error.message}`;
this.$toast.fail('上传失败');
console.error(error);
} finally {
this.uploading = false;
}
},
convertToObjectArray(urls) {
return urls.map(url => {
const modifiedUrl = this.storageUrl + url;
return { url: modifiedUrl }
});
},
convertUrlsToString(arr) {
return arr.map(item => {
let modifiedUrl = item.url.replace(this.storageUrl, '');
return modifiedUrl;
}).join('&&');
},
onPicker(value) {
this.info.status = value=='营业中'?1:0;
this.showPicker = false;
},
},
created() { },
mounted() {
this.getInfo();
}
};
</script>
<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);
.home-man-page {
width: 100vw;
height: 100vh;
overflow: auto;
.images {
width: 100%;
height: 25vh;
overflow: hidden;
min-height: 200px;
max-height: 500px;
.swipe {
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
}
}
}
.info {
width: 100%;
padding: 20px;
.location {
display: flex;
.left {
width: 70%;
span {
font-size: 12px;
color: #a19e9e;
margin-bottom: 10px;
}
}
.right {
width: 30%;
display: flex;
justify-content: space-evenly;
align-items: center;
color: #a19e9e;
}
}
}
.new {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
width: 90%;
height: 25vh;
overflow: hidden;
min-height: 200px;
max-height: 500px;
margin: auto;
.swipe {
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
}
}
}
.navigation {
width: 100%;
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,31 @@
<template>
<div class="tag-man-page">
<!-- 固定顶部导航栏 -->
<NavBar class="nav-bar" />
<!-- 固定底部TabBar -->
<ManTabBar class="tab-bar" />
</div>
</template>
<script>
import NavBar from '@/components/NavBar.vue';
import ManTabBar from '@/components/ManTabBar.vue';
export default {
name: 'TagManPage',
components: {
ManTabBar,
NavBar
},
props: {},
data() {
return {
};
},
watch: {},
computed: {},
methods: {},
created() { },
mounted() { }
};
</script>
<style lang="less" scoped></style>

View File

@ -3,6 +3,10 @@ import VueRouter from "vue-router";
import HomePage from "@/pages/HomePage.vue"; import HomePage from "@/pages/HomePage.vue";
import PortfolioPage from "@/pages/PortfolioPage.vue"; import PortfolioPage from "@/pages/PortfolioPage.vue";
import SampleCollectionPage from "@/pages/SampleCollectionPage.vue"; import SampleCollectionPage from "@/pages/SampleCollectionPage.vue";
import PortfolioMan from "@/pages/man/PortfolioManPage.vue";
import SampleCollectionMan from "@/pages/man/SampleCollectionManPage.vue";
import HomeManPage from "@/pages/man/HomeManPage.vue";
import TagManPage from "@/pages/man/TagManPage.vue";
Vue.use(VueRouter); Vue.use(VueRouter);
const routes = [ const routes = [
@ -10,20 +14,37 @@ const routes = [
path: "/", path: "/",
name: "HomePage", name: "HomePage",
component: HomePage, component: HomePage,
meta: { direction: "back" },
}, },
{ {
path: "/portfolio", path: "/portfolio",
name: "PortfolioPage", name: "PortfolioPage",
component: PortfolioPage, component: PortfolioPage,
meta: { direction: "forward" },
}, },
{ {
path: "/sampleCollection", path: "/sampleCollection",
name: "SampleCollectionPage", name: "SampleCollectionPage",
component: SampleCollectionPage, component: SampleCollectionPage,
meta: { direction: "forward" },
}, },
{
path: "/yumi",
name: "Yumi",
component: HomeManPage,
},
{
path: "/sampleCollectionman",
name: "SampleCollectionMan",
component: SampleCollectionMan,
},
{
path: "/portfolioMan",
name: "PortfolioMan",
component: PortfolioMan,
},
{
path: "/tagManPage",
name: "tagManPage",
component: TagManPage,
}
]; ];
const router = new VueRouter({ const router = new VueRouter({

View File

@ -0,0 +1,227 @@
import axios from 'axios';
/**
* 文件操作工具类支持上传下载和删除文件
*/
export class FileService {
constructor(baseUrl = '') {
this.baseUrl = baseUrl;
}
/**
* 上传文件到MinIO
* @param {Object} options - 上传参数
* @param {string} options.bucketName - 存储桶名称
* @param {string} options.filePath - 文件路径
* @param {File} options.file - 文件对象
* @param {Function} [options.onProgress] - 进度回调函数
* @returns {Promise<string>} - 上传成功后的文件路径
*/
uploadFile(options) {
const {
bucketName,
filePath,
file,
} = options;
if (!bucketName || !filePath || !file) {
return Promise.reject(new Error('缺少必要的上传参数'));
}
const formData = new FormData();
formData.append('bucketName', bucketName);
formData.append('filePath', filePath);
formData.append('file', file);
return axios({
url: `${this.baseUrl}/api/file/upload`,
method: 'post',
data: formData,
headers: { 'Accept': 'application/json' },
timeout: 60000
})
.then(response => {
if (response.status === 200) {
return response.data;
}
throw new Error(`上传失败,状态码:${response.status}`);
})
.catch(this._handleError);
}
/**
* 下载文件
* @param {Object} options - 下载参数
* @param {string} options.bucketName - 存储桶名称
* @param {string} options.filePath - 文件路径
* @param {string} [options.fileName] - 下载时使用的文件名
* @returns {Promise<void>}
*/
downloadFile(options) {
const { bucketName, filePath, fileName } = options;
if (!bucketName || !filePath) {
return Promise.reject(new Error('缺少必要的下载参数'));
}
return axios({
url: `${this.baseUrl}/api/file/download`,
method: 'get',
params: { bucketName, filePath },
responseType: 'blob',
timeout: 60000
})
.then(response => {
if (response.status === 200) {
this._saveFile(response.data, fileName || filePath.split('/').pop());
return;
}
throw new Error(`下载失败,状态码:${response.status}`);
})
.catch(this._handleError);
}
/**
* 删除文件
* @param {Object} options - 删除参数
* @param {string} options.bucketName - 存储桶名称
* @param {string} options.filePath - 文件路径
* @returns {Promise<void>}
*/
deleteFile(options) {
const { bucketName, filePath } = options;
if (!bucketName || !filePath) {
return Promise.reject(new Error('缺少必要的删除参数'));
}
return axios({
url: `${this.baseUrl}/api/file/delete`,
method: 'delete',
params: { bucketName, filePath },
timeout: 30000
})
.then(response => {
if (response.status === 204) {
return;
}
throw new Error(`删除失败,状态码:${response.status}`);
})
.catch(this._handleError);
}
/**
* 检查文件是否存在
* @param {Object} options - 参数
* @param {string} options.bucketName - 存储桶名称
* @param {string} options.filePath - 文件路径
* @returns {Promise<boolean>} - 文件是否存在
*/
fileExists(options) {
const { bucketName, filePath } = options;
if (!bucketName || !filePath) {
return Promise.reject(new Error('缺少必要的参数'));
}
return axios({
url: `${this.baseUrl}/api/file/exists`,
method: 'get',
params: { bucketName, filePath },
timeout: 30000
})
.then(response => {
if (response.status === 200) {
return response.data;
}
throw new Error(`检查失败,状态码:${response.status}`);
})
.catch(this._handleError);
}
/**
* 获取文件URL
* @param {Object} options - 参数
* @param {string} options.bucketName - 存储桶名称
* @param {string} options.filePath - 文件路径
* @param {number} [options.expirySeconds=3600] - URL有效期
* @returns {Promise<string>} - 文件URL
*/
getFileUrl(options) {
const { bucketName, filePath, expirySeconds = 3600 } = options;
if (!bucketName || !filePath) {
return Promise.reject(new Error('缺少必要的参数'));
}
return axios({
url: `${this.baseUrl}/api/file/url`,
method: 'get',
params: { bucketName, filePath, expirySeconds },
timeout: 30000
})
.then(response => {
if (response.status === 200) {
return response.data;
}
throw new Error(`获取URL失败状态码${response.status}`);
})
.catch(this._handleError);
}
/**
* 保存文件到本地
* @param {Blob} blob - 文件内容
* @param {string} fileName - 文件名
*/
_saveFile(blob, fileName) {
// 处理中文文件名
const encodedFileName = encodeURIComponent(fileName);
if (navigator.msSaveBlob) {
// 兼容IE
navigator.msSaveBlob(blob, fileName);
} else {
// 现代浏览器
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.setAttribute('download', encodedFileName);
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
}
}
/**
* 统一处理错误
* @param {Error} error - 错误对象
*/
_handleError(error) {
let errorMsg = '操作失败';
if (error.response) {
errorMsg += `,服务器错误:${error.response.status} ${error.response.statusText}`;
} else if (error.request) {
errorMsg += ',未收到服务器响应';
} else {
errorMsg += `${error.message}`;
}
console.error(errorMsg, error);
throw new Error(errorMsg);
}
}
// 创建Vue插件
export const FileServicePlugin = {
install(Vue, options = {}) {
// 创建全局实例
const fileService = new FileService(options.baseUrl);
// 方式1添加到Vue原型全局可用
Vue.prototype.$fileService = fileService;
// 方式2注册为全局组件
Vue.fileService = fileService;
}
};

View File

@ -2,7 +2,7 @@ import axios from 'axios'
// 创建 axios 实例 // 创建 axios 实例
const service = axios.create({ const service = axios.create({
baseURL: "http://127.0.0.1:8090/api", baseURL: "http://wcy111.top:35001/api",
timeout: 10000 timeout: 10000
}) })
@ -19,31 +19,11 @@ const getRequestKey = (config) => {
const dataStr = data ? JSON.stringify(data) : ''; const dataStr = data ? JSON.stringify(data) : '';
return `${method}:${url}:${paramsStr}:${dataStr}`; return `${method}:${url}:${paramsStr}:${dataStr}`;
}; };
// 请求拦截器:添加防抖逻辑
// 请求拦截器:仅保留防抖逻辑
service.interceptors.request.use( service.interceptors.request.use(
config => { 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) => { return new Promise((resolve) => {
const requestKey = getRequestKey(config); const requestKey = getRequestKey(config);
// 清除之前的定时器 // 清除之前的定时器
@ -63,4 +43,29 @@ service.interceptors.request.use(
return Promise.reject(error); return Promise.reject(error);
} }
); );
// 响应拦截器保持不变
service.interceptors.response.use(
response => {
return response.data;
},
error => {
let errorMessage = '请求失败';
if (error.response) {
errorMessage = `请求失败,状态码: ${error.response.status}`;
if (error.response.data && error.response.data.message) {
errorMessage += `,原因: ${error.response.data.message}`;
}
} else if (error.request) {
errorMessage = '请求已发送,但没有收到响应';
} else {
errorMessage = `请求错误: ${error.message}`;
}
console.error(errorMessage);
return Promise.reject(errorMessage);
}
);
export default service; export default service;