初版提交

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>
<scope>provided</scope>
</dependency>
<!-- MinIO 客户端 -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.6</version>
</dependency>
</dependencies>
<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;
}
/**
* 获取作品集列表
*
* @param page 页码
* @param size 每页数量
* @return RespVO
*/
@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();
if (page <= 0 || size <= 0) {
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
@ -47,8 +54,14 @@ public class PortfolioController {
}
/**
* 添加作品集
*
* @param portfolioDO 作品集信息
* @return RespVO
*/
@PostMapping("/add")
public RespVO addPortfolio(PortfolioDO portfolioDO) {
public RespVO addPortfolio(@RequestBody PortfolioDO portfolioDO) {
RespVO respVO = new RespVO();
if (portfolioDO == null) {
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
@ -69,8 +82,14 @@ public class PortfolioController {
}
/**
* 更新作品集
*
* @param portfolioDO 作品集信息
* @return RespVO
*/
@PostMapping("/update")
public RespVO updatePortfolio(PortfolioDO portfolioDO) {
public RespVO updatePortfolio(@RequestBody PortfolioDO portfolioDO) {
RespVO respVO = new RespVO();
if (portfolioDO == null) {
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
@ -89,6 +108,12 @@ public class PortfolioController {
return respVO;
}
/**
* 删除作品集
*
* @param id 作品集id
* @return RespVO
*/
@PostMapping("/delete")
public RespVO deletePortfolio(@RequestParam("id") Integer id) {
RespVO respVO = new RespVO();
@ -109,6 +134,13 @@ public class PortfolioController {
return respVO;
}
/**
* 添加作品集
*
* @param id 作品集id
* @param changes 点赞数
* @return RespVO
*/
@PostMapping("/addLoves")
public RespVO addLoves(@RequestParam("id") Integer id, @RequestParam Integer changes) {
RespVO respVO = new RespVO();
@ -128,4 +160,28 @@ public class PortfolioController {
}
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;
/**
* 标签
* 标签ID
*/
private String tag;
/**
* 标签样式
*/
private String tagStyle;
private String tagId;
/**
* 创建时间
*/

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;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nailart.dataobj.PortfolioDO;
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.nailart.dataobj.PortfolioDO;
import java.util.List;
public interface PortfolioService extends IService<PortfolioDO> {
IPage<PortfolioDO> listByPage(int page, int size);
@ -14,4 +16,6 @@ public interface PortfolioService extends IService<PortfolioDO> {
Integer deletePortfolio(Integer id);
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;
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.PortfolioDO;
import com.nailart.dataobj.TagDO;
import com.nailart.mapper.PortfolioMapper;
import com.nailart.service.PortfolioService;
import com.nailart.service.TagInfoService;
import com.nailart.utils.DateUtils;
import org.springframework.stereotype.Service;
/**
@ -19,6 +23,7 @@ public class PortfolioServiceImpl extends ServiceImpl<PortfolioMapper, Portfolio
private final PortfolioMapper portfolioMapper;
public PortfolioServiceImpl(PortfolioMapper portfolioMapper ) {
this.portfolioMapper = portfolioMapper;
}
@ -36,6 +41,9 @@ public class PortfolioServiceImpl extends ServiceImpl<PortfolioMapper, Portfolio
@Override
public Integer addPortfolio(PortfolioDO portfolioDO) {
if (portfolioDO.getCreateTime()==null){
portfolioDO.setCreateTime(DateUtils.getCurrentDateTime());
}
return portfolioMapper.insert(portfolioDO);
}
@ -58,4 +66,12 @@ public class PortfolioServiceImpl extends ServiceImpl<PortfolioMapper, Portfolio
portfolioDO.setLoves(portfolioDO.getLoves() + changes);
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;
import com.nailart.enums.RespCodeEnum;
import lombok.Data;
/**
@ -13,4 +14,28 @@ public class RespVO {
private Integer code;
private String msg;
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:
context-path: /api # 应用访问路径前缀
# 数据源配置
spring:
servlet:
multipart:
max-file-size: 2GB
max-request-size: 5GB
file-size-threshold: 1024MB # 超过10MB的文件直接写入磁盘
location: /data/temp # 指定临时存储目录(确保有足够空间)
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 使用Druid连接池
driver-class-name: com.mysql.cj.jdbc.Driver # MySQL驱动类
@ -74,3 +81,13 @@ logging:
com.example.mapper: debug # Mapper接口日志级别(调试用)
file:
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>
<div id="app">
<!-- 固定顶部导航栏 -->
<NavBar class="nav-bar" />
<!-- 中间内容区包裹路由视图控制动画范围 -->
<div class="content-container">
@ -11,25 +10,22 @@
<!-- </transition> -->
</div>
<!-- 固定底部TabBar -->
<TabBar class="tab-bar" />
</div>
</template>
<script>
import TabBar from './components/TabBar.vue';
import NavBar from './components/NavBar.vue';
export default {
name: 'App',
components: {
TabBar,
NavBar
}
}
</script>
<style lang="less" scoped>
<style lang="less">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
@ -39,7 +35,15 @@ export default {
overflow: hidden;
min-width: 200px;
}
html {
font-size: 20px; /* 基准字体大小 */
}
@media (max-width: 768px) {
html {
font-size: 14px; /* 在小屏幕上缩小基准字体大小 */
}
}
* {
margin: 0;
padding: 0;
@ -61,7 +65,7 @@ export default {
/* 中间内容区避开上下Bar的位置 */
.content-container {
position: relative;
height: calc(100vh - 110px);
// height: calc(100vh - 110px);
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>
<div class="nav-bar">
<van-nav-bar title="屿" 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 title="屿 ● Nail" left-text="" :placeholder="true" :safe-area-inset-top="true" z-index="50" :fixed="true">
</van-nav-bar>
</div>
</template>

View File

@ -3,6 +3,11 @@ import Vuex from "vuex";
import App from "./App.vue";
import router from "./router";
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 { NavBar } from "vant";
import { Toast } from "vant";
@ -17,7 +22,36 @@ import { Swipe, SwipeItem } from 'vant';
import { Loading } from 'vant';
import { NoticeBar } 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(NoticeBar);
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,18 +1,32 @@
<template>
<div>
<div class="homepage">
<h1>主页</h1>
<!-- 固定顶部导航栏 -->
<NavBar class="nav-bar" />
<div class="images">
<h1>轮播图区域</h1>
</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>
</template>
<script>
import TabBar from '@/components/TabBar.vue';
import NavBar from '@/components/NavBar.vue';
export default {
name: 'HomePage',
components: {
TabBar,
NavBar
},
props: {},
data() {
@ -30,7 +44,26 @@ export default {
<style lang="less" scoped>
.homepage {
width: 100vw;
height: 100%;
height: 100vh;
overflow: auto;
.images {
width: 100%;
height: 25vh;
}
.info {
width: 100%;
height: 20vh;
}
.new {
width: 100%;
}
.navigation {
width: 100%;
}
}
</style>

View File

@ -1,5 +1,11 @@
<template>
<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 定位的公告栏 -->
<div class="sticky-notice">
<van-notice-bar left-icon="volume-o" :scrollable="true" text="所有标价均为款式价格,如需延长需补差价喔~~~半贴¥90,浅贴¥130" />
@ -7,6 +13,10 @@
<!-- 内容容器 -->
<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 v-for="(card, index) in cards" :key="index" class="card"
@ -16,21 +26,23 @@
<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="contain" :src="image">
<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 class="card-content">
<h2 class="card-title">{{ card.title }}</h2>
<p class="card-description">{{ card.description }}</p>
<div v-if="card.tag" class="card-tag">
<van-tag round type="primary">{{ card.tag }}</van-tag>
<div 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">
@ -50,45 +62,77 @@
</div>
</div>
</div>
<!-- <div class="loading" v-show="loading">
<van-loading type="spinner" color="#1989fa" />
</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>
</template>
<script>
import TabBar from '@/components/TabBar.vue';
import NavBar from '@/components/NavBar.vue';
export default {
name: 'PortfolioPage',
components: {
TabBar,
NavBar
},
data() {
return {
cards: [],
card_loading: false,
card_loading1: false,
activeCardIndex: -1,
page: 1,
pageSize: 2,
pageNum: 1,
pageSize: 20,
loading: false,
changes: [],
totalPages: 0
totalPages: 0,
showBackToTop: false, // /
scrollContainer: null,
scrollHandler: null,
tagInfo: [{ "id": 0, "tagName": "全部" }],
//
idToObjectMap: {},
tagID: 0
};
},
methods: {
async getCarts(pageNum, size) {
async getCarts(tagID, pageNum, size) {
try {
this.loading = true;
let res = await this.$axios.get('/portfolio/list', {
let res = null;
if (tagID == 0) {
res = await this.$axios.get('/portfolio/list', {
params: {
page: pageNum,
size: size
}
});
if (res.code !== 200 && res.data.code !== 200) {
const errorMsg = res.msg || res.data.msg || '获取数据失败';
})
} 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);
return;
}
const records = res.data?.data?.records || [];
const records = res.data.records || [];
if (records.length === 0) {
this.$toast('没有更多数据了');
return;
@ -98,46 +142,93 @@ export default {
const images = item.imgFilename
? item.imgFilename.split('&&').map(filename => `${item.imgUrl}/${filename}`)
: [];
const tagStyle = item.tagStyle
? { backgroundColor: item.tagStyle.split('&&')[0], color: item.tagStyle.split('&&')[1] }
: { backgroundColor: "#eeeeee", color: "#fff" };
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: '',
tag: item.tag || '热门',
tagStyle,
tagInfo,
loves: item.loves || 0,
changes: 0,
checked: false,
amt: item.amt || 0,
};
});
this.cards = [...this.cards, ...formattedCards];
this.loading = false;
this.totalPages = res.data.data.pages;
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) {
this.activeCardIndex = index;
},
/**
* 处理触摸结束事件
*/
handleTouchEnd() {
setTimeout(() => {
this.activeCardIndex = -1;
}, 300);
},
/**
* 卡片点击事件
* @param card 点击的卡片
*/
handleCardClick(card) {
console.log('点击了卡片:', card.title);
},
/**
* 点赞处理
* @param card 点击的卡片
*/
handleClickLove(card) {
if (card.checked) {
card.loves++;
@ -153,6 +244,7 @@ export default {
changes: card.changes
});
},
async savaLoveChanes() {
for (const change of this.changes) {
try {
@ -168,25 +260,44 @@ export default {
}
}
},
handleScroll() {
const container = document.querySelector('.cards-container');
if (!container) return;
const scrollTop = container.scrollTop;
const clientHeight = container.clientHeight;
const scrollHeight = container.scrollHeight;
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.loading && this.page < this.totalPages) {
this.page++;
this.getCarts(this.page, this.pageSize || 2);
console.log('加载更多数据,当前页:', this.page);
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() {
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');
if (this.scrollContainer) {
this.scrollHandler = () => this.handleScroll();
@ -204,7 +315,6 @@ export default {
}
};
</script>
<style lang="less" scoped>
//
@base-bg: #f9fafb;
@ -222,34 +332,33 @@ export default {
.page-container {
display: flex;
flex-direction: column;
height: 100%;
height: 100vh;
max-width: 800px;
margin: 0 auto;
padding: 0 20px;
//
.sticky-notice {
position: sticky;
top: 0;
top: 50px;
z-index: 100;
margin-bottom: 10px;
}
//
.cards-container {
padding: 0 5px;
overflow: auto;
padding-bottom: 20px;
//
.card-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
gap: 5px;
//
@media (max-width: 600px) {
grid-template-columns: 1fr;
}
// //
// @media (max-width: 600px) {
// grid-template-columns: 1fr;
// }
//
.card {
@ -259,7 +368,7 @@ export default {
box-shadow: @shadow-base;
transition: all 0.3s ease;
cursor: pointer;
padding-bottom: 5px;
//
&:hover {
transform: translateY(-5px);
@ -275,36 +384,38 @@ export default {
.card-image-container {
position: relative;
//
.card-tag {
.card-image {
width: 100%;
min-height: 100px;
}
padding: 4px 12px;
border-radius: 9999px;
font-size: 14px;
font-weight: 500;
//
.card-tag {
transform: translateX(-5px);
}
}
//
.card-content {
padding: 20px;
padding: 5px;
position: relative;
//
.card-title {
font-size: 18px;
padding-left: 5px;
font-size: 1.2rem;
font-weight: bold;
color: @text-primary;
margin-bottom: 8px;
margin-bottom: 10px;
}
//
.card-description {
padding-left: 5px;
color: @text-secondary;
text-wrap: balance;
margin-bottom: 12px;
margin-bottom: 10px;
line-height: 1.5;
font-size: 14px;
font-size: 1rem;
}
//
@ -318,13 +429,12 @@ export default {
//
.card-meta {
color: #9932CC;
color: #FFFAFA;
font-weight: 500;
font-size: 20px;
background-color: #D8BFD8;
font-size: 0.9rem;
background-color: #8470FF;
border-radius: 8px;
padding: 0 5px 0 5px;
padding: 5px;
//
.fa {
margin-right: 1px;
@ -416,6 +526,29 @@ export default {
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,5 +1,22 @@
<template>
<div class="sample-collection-page">
<!-- 固定顶部导航栏 -->
<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 定位的公告栏 -->
<div class="sticky-notice">
<van-notice-bar left-icon="volume-o" :scrollable="true" text="所有标价均为款式价格,如需延长需补差价喔~~~半贴¥90,浅贴¥130" />
</div>
<!-- 内容容器 -->
<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 v-for="(card, index) in cards" :key="index" class="card"
@ -9,104 +26,273 @@
<div class="card-image-container">
<van-swipe :autoplay="3000">
<van-swipe-item v-for="(image, imgIndex) in card.images" :key="imgIndex">
<van-image :src="image" lazy-load>
<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 v-if="card.tag" class="card-tag" :style="card.tagStyle">
{{ card.tag }}
</div>
</div>
<div class="card-content">
<h2 class="card-title">{{ card.title }}</h2>
<p class="card-description">{{ card.description }}</p>
<div class="card-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">
<div class="employee">
<van-tag round type="primary" v-for="employee in card.employee.split('&&')"
:key="employee">{{ employee
}}</van-tag>
<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 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>
</template>
<script>
import TabBar from '@/components/TabBar.vue';
import NavBar from '@/components/NavBar.vue';
export default {
name: 'SampleCollectionPage',
components: {},
props: {},
data() {
return {
cards: [
{
title: "探索自然之美",
description: "深入原始森林,感受大自然的鬼斧神工。这里有清澈的溪流、茂密的树木和各种珍稀野生动物。",
images: ["https://picsum.photos/seed/card1/600/800", "https://picsum.photos/seed/card2/600/800"],
tag: "热门",
tagStyle: {
backgroundColor: "#F97316",
color: "#fff"
components: {
TabBar,
NavBar
},
status: 0,
employee: "小吕&&寒冰射手",
neddTime: 2.0
}
],
activeCardIndex: -1
data() {
return {
cards: [],
card_loading: false,
card_loading1: false,
activeCardIndex: -1,
pageNum: 1,
pageSize: 20,
loading: false,
changes: [],
totalPages: 0,
showBackToTop: false, // /
scrollContainer: null,
scrollHandler: null,
tagInfo: [{ "id": 0, "tagName": "全部" }],
//
idToObjectMap: {},
tagID: 0
};
},
watch: {},
computed: {
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('网络错误,请稍后重试');
}
},
methods: {
//
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) {
this.activeCardIndex = index;
},
/**
* 处理触摸结束事件
*/
handleTouchEnd() {
setTimeout(() => {
this.activeCardIndex = -1;
}, 300);
},
/**
* 卡片点击事件
* @param card 点击的卡片
*/
handleCardClick(card) {
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() {
//
const buttons = document.querySelectorAll('.card-button');
buttons.forEach(button => {
button.addEventListener('touchstart', () => {
button.classList.add('button-active');
});
button.addEventListener('touchend', () => {
setTimeout(() => {
button.classList.remove('button-active');
}, 200);
});
});
this.getTagInfo();
this.tagID = 0;
this.getCarts(this.tagID, this.pageNum, this.pageSize || 2);
this.scrollContainer = document.querySelector('.cards-container');
if (this.scrollContainer) {
this.scrollHandler = () => this.handleScroll();
this.scrollContainer.addEventListener('scroll', this.scrollHandler);
}
},
beforeDestroy() {
if (this.scrollContainer && this.scrollHandler) {
this.scrollContainer.removeEventListener('scroll', this.scrollHandler);
}
},
created() { },
};
</script>
@ -123,47 +309,39 @@ 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-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 {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 800px;
margin: 0 auto;
padding: 0 20px;
.page-title {
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: bold;
color: @text-primary;
margin-bottom: 32px;
text-align: center;
//
.sticky-notice {
position: sticky;
top: 50px;
z-index: 100;
margin-bottom: 10px;
}
//
.cards-container {
padding: 0 5px;
overflow: auto;
//
.card-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
gap: 5px;
//
@media (max-width: 600px) {
grid-template-columns: 1fr;
}
}
// //
// @media (max-width: 600px) {
// grid-template-columns: 1fr;
// }
//
//
.card {
background-color: @card-bg;
border-radius: 16px;
@ -171,7 +349,9 @@ body {
box-shadow: @shadow-base;
transition: all 0.3s ease;
cursor: pointer;
padding-bottom: 5px;
//
&:hover {
transform: translateY(-5px);
box-shadow: @shadow-active;
@ -186,70 +366,84 @@ body {
.card-image-container {
position: relative;
.card-image {
width: 100%;
min-height: 100px;
}
//
.card-tag {
position: absolute;
top: 16px;
right: 16px;
padding: 4px 12px;
border-radius: 9999px;
font-size: 14px;
font-weight: 500;
transform: translateX(-5px);
}
}
//
//
.card-content {
padding: 20px;
padding: 5px;
position: relative;
//
.card-title {
font-size: 18px;
padding-left: 5px;
font-size: 1.2rem;
font-weight: bold;
color: @text-primary;
margin-bottom: 8px;
margin-bottom: 10px;
}
//
.card-description {
padding-left: 5px;
color: @text-secondary;
text-wrap: balance;
margin-bottom: 12px;
margin-bottom: 10px;
line-height: 1.5;
font-size: 14px;
font-size: 1rem;
}
//
//
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12px;
position: relative;
bottom: 0px;
//
.card-meta {
color: @accent-color;
color: #FFFAFA;
font-weight: 500;
font-size: 13px;
font-size: 0.9rem;
background-color: #8470FF;
border-radius: 8px;
padding: 5px;
//
.fa {
margin-right: 1px;
}
}
//
//
.heart {
display: flex;
align-items: center;
gap: 4px;
//
.like-count {
font-size: 13px;
color: @text-secondary;
}
//
.action-btn {
position: relative;
cursor: pointer;
}
//
.heart-checkbox {
position: absolute;
opacity: 0;
@ -258,6 +452,7 @@ body {
width: 0;
}
//
.heart-btn {
position: relative;
width: 10px;
@ -272,6 +467,7 @@ body {
}
}
//
.heart-icon {
position: absolute;
top: 0;
@ -295,7 +491,7 @@ body {
}
}
//
//
.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);
@ -303,19 +499,62 @@ body {
}
}
}
.employee {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
}
.van-tag {
font-size: 12px;
padding: 4px 8px;
border-radius: 9999px;
background-color: #7B68EE;
color: white;
//
.loading {
display: flex;
justify-content: center;
align-items: center;
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>

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 PortfolioPage from "@/pages/PortfolioPage.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);
const routes = [
@ -10,20 +14,37 @@ const routes = [
path: "/",
name: "HomePage",
component: HomePage,
meta: { direction: "back" },
},
{
path: "/portfolio",
name: "PortfolioPage",
component: PortfolioPage,
meta: { direction: "forward" },
},
{
path: "/sampleCollection",
name: "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({

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 实例
const service = axios.create({
baseURL: "http://127.0.0.1:8090/api",
baseURL: "http://wcy111.top:35001/api",
timeout: 10000
})
@ -19,31 +19,11 @@ const getRequestKey = (config) => {
const dataStr = data ? JSON.stringify(data) : '';
return `${method}:${url}:${paramsStr}:${dataStr}`;
};
// 请求拦截器:添加防抖逻辑
// 请求拦截器:仅保留防抖逻辑
service.interceptors.request.use(
config => {
// 1. 检查POST请求体和参数是否都为空
if (config.method === 'post') {
// 检查请求体是否为空
const bodyEmpty =
config.data === undefined ||
config.data === null ||
(typeof config.data === 'object' && Object.keys(config.data).length === 0) ||
(typeof config.data === 'string' && config.data.trim().length === 0);
// 检查URL参数是否为空
const paramsEmpty =
config.params === undefined ||
config.params === null ||
(typeof config.params === 'object' && Object.keys(config.params).length === 0);
// 如果请求体和参数都为空,则打印提示
if (bodyEmpty && paramsEmpty) {
console.log('提示POST请求的请求体和参数均为空');
}
}
// 2. 防抖逻辑
// 防抖逻辑
return new Promise((resolve) => {
const requestKey = getRequestKey(config);
// 清除之前的定时器
@ -63,4 +43,29 @@ service.interceptors.request.use(
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;