初始化美若彩妆销售平台项目
This commit is contained in:
77
.gitignore
vendored
Normal file
77
.gitignore
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Compiled class files
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# BlueJ files
|
||||||
|
*.ctxt
|
||||||
|
|
||||||
|
# Mobile Tools for Java (J2ME)
|
||||||
|
.mtj.tmp/
|
||||||
|
|
||||||
|
# Package Files
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.nar
|
||||||
|
*.ear
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
|
||||||
|
# Maven
|
||||||
|
target/
|
||||||
|
pom.xml.tag
|
||||||
|
pom.xml.releaseBackup
|
||||||
|
pom.xml.versionsBackup
|
||||||
|
pom.xml.next
|
||||||
|
release.properties
|
||||||
|
dependency-reduced-pom.xml
|
||||||
|
buildNumber.properties
|
||||||
|
.mvn/timing.properties
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.settings/
|
||||||
|
.vscode/
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Application
|
||||||
|
application-local.yml
|
||||||
|
application-prod.yml
|
||||||
|
upload/
|
||||||
|
temp/
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
86
meiruo-backend/pom.xml
Normal file
86
meiruo-backend/pom.xml
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.2.0</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>com.meiruo</groupId>
|
||||||
|
<artifactId>meiruo-cosmetics</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<name>meiruo-cosmetics</name>
|
||||||
|
<description>美若彩妆销售平台</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>17</java.version>
|
||||||
|
<mybatis-spring-boot.version>3.0.3</mybatis-spring-boot.version>
|
||||||
|
<sa-token.version>1.38.0</sa-token.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mybatis.spring.boot</groupId>
|
||||||
|
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||||
|
<version>${mybatis-spring-boot.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.dev33</groupId>
|
||||||
|
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||||
|
<version>${sa-token.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.dev33</groupId>
|
||||||
|
<artifactId>sa-token-jwt</artifactId>
|
||||||
|
<version>${sa-token.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<excludes>
|
||||||
|
<exclude>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</exclude>
|
||||||
|
</excludes>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
135
meiruo-backend/sql/database.sql
Normal file
135
meiruo-backend/sql/database.sql
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
-- 创建数据库
|
||||||
|
CREATE DATABASE IF NOT EXISTS meiruo_cosmetics DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE meiruo_cosmetics;
|
||||||
|
|
||||||
|
-- 用户表
|
||||||
|
CREATE TABLE IF NOT EXISTS `user` (
|
||||||
|
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
|
||||||
|
`username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
|
||||||
|
`password` VARCHAR(100) NOT NULL COMMENT '密码',
|
||||||
|
`nickname` VARCHAR(50) COMMENT '昵称',
|
||||||
|
`phone` VARCHAR(20) COMMENT '手机号',
|
||||||
|
`email` VARCHAR(100) COMMENT '邮箱',
|
||||||
|
`avatar` VARCHAR(255) COMMENT '头像',
|
||||||
|
`role` TINYINT DEFAULT 0 COMMENT '角色:0-普通用户,1-管理员',
|
||||||
|
`status` TINYINT DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
|
||||||
|
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
INDEX idx_username (`username`),
|
||||||
|
INDEX idx_phone (`phone`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
|
||||||
|
|
||||||
|
-- 分类表
|
||||||
|
CREATE TABLE IF NOT EXISTS `category` (
|
||||||
|
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '分类ID',
|
||||||
|
`name` VARCHAR(50) NOT NULL COMMENT '分类名称',
|
||||||
|
`description` VARCHAR(255) COMMENT '分类描述',
|
||||||
|
`sort` INT DEFAULT 0 COMMENT '排序',
|
||||||
|
`status` TINYINT DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
|
||||||
|
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
INDEX idx_status (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品分类表';
|
||||||
|
|
||||||
|
-- 商品表
|
||||||
|
CREATE TABLE IF NOT EXISTS `product` (
|
||||||
|
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '商品ID',
|
||||||
|
`name` VARCHAR(100) NOT NULL COMMENT '商品名称',
|
||||||
|
`description` TEXT COMMENT '商品描述',
|
||||||
|
`price` DECIMAL(10,2) NOT NULL COMMENT '价格',
|
||||||
|
`stock` INT DEFAULT 0 COMMENT '库存',
|
||||||
|
`category_id` BIGINT COMMENT '分类ID',
|
||||||
|
`image` VARCHAR(255) COMMENT '主图',
|
||||||
|
`images` TEXT COMMENT '图片列表',
|
||||||
|
`status` TINYINT DEFAULT 1 COMMENT '状态:0-下架,1-正常',
|
||||||
|
`sales` INT DEFAULT 0 COMMENT '销量',
|
||||||
|
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
INDEX idx_category (`category_id`),
|
||||||
|
INDEX idx_status (`status`),
|
||||||
|
INDEX idx_sales (`sales`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
|
||||||
|
|
||||||
|
-- 购物车表
|
||||||
|
CREATE TABLE IF NOT EXISTS `cart` (
|
||||||
|
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '购物车ID',
|
||||||
|
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||||
|
`product_id` BIGINT NOT NULL COMMENT '商品ID',
|
||||||
|
`quantity` INT NOT NULL DEFAULT 1 COMMENT '数量',
|
||||||
|
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
UNIQUE KEY uk_user_product (`user_id`, `product_id`),
|
||||||
|
INDEX idx_user (`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车表';
|
||||||
|
|
||||||
|
-- 订单表
|
||||||
|
CREATE TABLE IF NOT EXISTS `order` (
|
||||||
|
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID',
|
||||||
|
`order_no` VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号',
|
||||||
|
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||||
|
`total_amount` DECIMAL(10,2) NOT NULL COMMENT '总金额',
|
||||||
|
`status` TINYINT DEFAULT 1 COMMENT '状态:1-待付款,2-已付款,3-已发货,4-已完成,5-已取消',
|
||||||
|
`receiver_name` VARCHAR(50) COMMENT '收货人姓名',
|
||||||
|
`receiver_phone` VARCHAR(20) COMMENT '收货人电话',
|
||||||
|
`receiver_address` VARCHAR(255) COMMENT '收货地址',
|
||||||
|
`remark` VARCHAR(255) COMMENT '备注',
|
||||||
|
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`pay_time` DATETIME COMMENT '付款时间',
|
||||||
|
`ship_time` DATETIME COMMENT '发货时间',
|
||||||
|
`receive_time` DATETIME COMMENT '收货时间',
|
||||||
|
INDEX idx_user (`user_id`),
|
||||||
|
INDEX idx_order_no (`order_no`),
|
||||||
|
INDEX idx_status (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
|
||||||
|
|
||||||
|
-- 订单项表
|
||||||
|
CREATE TABLE IF NOT EXISTS `order_item` (
|
||||||
|
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '订单项ID',
|
||||||
|
`order_id` BIGINT NOT NULL COMMENT '订单ID',
|
||||||
|
`product_id` BIGINT NOT NULL COMMENT '商品ID',
|
||||||
|
`product_name` VARCHAR(100) NOT NULL COMMENT '商品名称',
|
||||||
|
`product_image` VARCHAR(255) COMMENT '商品图片',
|
||||||
|
`price` DECIMAL(10,2) NOT NULL COMMENT '商品价格',
|
||||||
|
`quantity` INT NOT NULL COMMENT '数量',
|
||||||
|
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
INDEX idx_order (`order_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单项表';
|
||||||
|
|
||||||
|
-- 轮播图表
|
||||||
|
CREATE TABLE IF NOT EXISTS `banner` (
|
||||||
|
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '轮播图ID',
|
||||||
|
`title` VARCHAR(50) COMMENT '标题',
|
||||||
|
`image` VARCHAR(255) NOT NULL COMMENT '图片地址',
|
||||||
|
`link` VARCHAR(255) COMMENT '链接地址',
|
||||||
|
`sort` INT DEFAULT 0 COMMENT '排序',
|
||||||
|
`status` TINYINT DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
|
||||||
|
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
INDEX idx_status (`status`),
|
||||||
|
INDEX idx_sort (`sort`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='轮播图表';
|
||||||
|
|
||||||
|
-- 插入默认管理员账号
|
||||||
|
INSERT INTO `user` (`username`, `password`, `nickname`, `role`, `status`) VALUES
|
||||||
|
('admin', 'e10adc3949ba59abbe56e057f20f883e', '管理员', 1, 1);
|
||||||
|
|
||||||
|
-- 插入默认分类
|
||||||
|
INSERT INTO `category` (`name`, `description`, `sort`) VALUES
|
||||||
|
('面部彩妆', '粉底液、遮瑕膏、粉饼等', 1),
|
||||||
|
('眼妆', '眼影、眼线、睫毛膏等', 2),
|
||||||
|
('唇妆', '口红、唇釉、唇彩等', 3),
|
||||||
|
('腮红', '腮红、修容等', 4),
|
||||||
|
('卸妆', '卸妆油、卸妆水等', 5);
|
||||||
|
|
||||||
|
-- 插入示例商品
|
||||||
|
INSERT INTO `product` (`name`, `description`, `price`, `stock`, `category_id`, `image`, `status`, `sales`) VALUES
|
||||||
|
('水润粉底液', '轻薄水润,遮瑕保湿,打造自然裸妆感', 168.00, 100, 1, '/images/product1.jpg', 1, 50),
|
||||||
|
('大地色眼影盘', '12色日常大地色眼影盘,珠光哑光搭配', 128.00, 80, 2, '/images/product2.jpg', 1, 30),
|
||||||
|
('哑光正红色口红', '经典正红色,哑光质地,持久显色', 89.00, 200, 3, '/images/product3.jpg', 1, 100),
|
||||||
|
('腮红', '柔美腮红,轻薄自然,打造好气色', 68.00, 150, 4, '/images/product4.jpg', 1, 25),
|
||||||
|
('卸妆油', '温和卸妆油,深层清洁不紧绷', 98.00, 120, 5, '/images/product5.jpg', 1, 40);
|
||||||
|
|
||||||
|
-- 插入示例轮播图
|
||||||
|
INSERT INTO `banner` (`title`, `image`, `link`, `sort`) VALUES
|
||||||
|
('新品上市', '/images/banner1.jpg', '/product/1', 1),
|
||||||
|
('限时优惠', '/images/banner2.jpg', '/product/2', 2),
|
||||||
|
('热销推荐', '/images/banner3.jpg', '/product/3', 3);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.meiruo.cosmetics;
|
||||||
|
|
||||||
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
@MapperScan("com.meiruo.cosmetics.mapper")
|
||||||
|
public class MeiruoCosmeticsApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(MeiruoCosmeticsApplication.class, args);
|
||||||
|
System.out.println("====================================");
|
||||||
|
System.out.println(" 美若彩妆销售平台启动成功!");
|
||||||
|
System.out.println("====================================");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.meiruo.cosmetics.config;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||||
|
import cn.dev33.satoken.jwt.SaJwtManager;
|
||||||
|
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
|
||||||
|
import cn.dev33.satoken.stp.StpInterface;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class SaTokenConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public StpLogicJwtForSimple stpLogicJwtForSimple() {
|
||||||
|
return new StpLogicJwtForSimple();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public StpInterface stpInterface() {
|
||||||
|
return new StpInterface() {
|
||||||
|
@Override
|
||||||
|
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getRoleList(Object loginId, String loginType) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/api/**").excludePathPatterns("/api/user/login", "/api/user/register");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.meiruo.cosmetics.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
registry.addResourceHandler("/upload/**")
|
||||||
|
.addResourceLocations("file:src/main/resources/static/upload/")
|
||||||
|
.addResourceLocations("classpath:/static/upload/");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.meiruo.cosmetics.controller;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Banner;
|
||||||
|
import com.meiruo.cosmetics.service.BannerService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/banner")
|
||||||
|
public class BannerController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BannerService bannerService;
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Map<String, Object> getList(@RequestParam(required = false) Integer status) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
List<Banner> list = bannerService.getList(status);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("data", list);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public Map<String, Object> getById(@PathVariable Long id) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("data", bannerService.getById(id));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public Map<String, Object> add(@RequestBody Banner banner) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
bannerService.add(banner);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "添加成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping
|
||||||
|
public Map<String, Object> update(@RequestBody Banner banner) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
bannerService.update(banner);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "修改成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public Map<String, Object> delete(@PathVariable Long id) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
bannerService.delete(id);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "删除成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/sort/{id}")
|
||||||
|
public Map<String, Object> updateSort(@PathVariable Long id, @RequestParam Integer sort) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
bannerService.updateSort(id, sort);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "修改成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.meiruo.cosmetics.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import com.meiruo.cosmetics.service.CartService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/cart")
|
||||||
|
public class CartController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CartService cartService;
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Map<String, Object> getList() {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
Long userId = StpUtil.getLoginIdAsLong();
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("data", cartService.getCartList(userId));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/add")
|
||||||
|
public Map<String, Object> add(@RequestParam Long productId, @RequestParam Integer quantity) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
Long userId = StpUtil.getLoginIdAsLong();
|
||||||
|
cartService.add(userId, productId, quantity);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "添加成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/quantity/{id}")
|
||||||
|
public Map<String, Object> updateQuantity(@PathVariable Long id, @RequestParam Integer quantity) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
cartService.updateQuantity(id, quantity);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "修改成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public Map<String, Object> delete(@PathVariable Long id) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
cartService.delete(id);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "删除成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/clear")
|
||||||
|
public Map<String, Object> clear() {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
Long userId = StpUtil.getLoginIdAsLong();
|
||||||
|
cartService.clear(userId);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "清空成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.meiruo.cosmetics.controller;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Category;
|
||||||
|
import com.meiruo.cosmetics.service.CategoryService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/category")
|
||||||
|
public class CategoryController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CategoryService categoryService;
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Map<String, Object> getList(@RequestParam(required = false) Integer status) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
List<Category> list = categoryService.getList(status);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("data", list);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public Map<String, Object> getById(@PathVariable Long id) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("data", categoryService.getById(id));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public Map<String, Object> add(@RequestBody Category category) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
categoryService.add(category);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "添加成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping
|
||||||
|
public Map<String, Object> update(@RequestBody Category category) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
categoryService.update(category);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "修改成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public Map<String, Object> delete(@PathVariable Long id) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
categoryService.delete(id);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "删除成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.meiruo.cosmetics.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import com.meiruo.cosmetics.entity.Order;
|
||||||
|
import com.meiruo.cosmetics.service.OrderService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/order")
|
||||||
|
public class OrderController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private OrderService orderService;
|
||||||
|
|
||||||
|
@PostMapping("/create")
|
||||||
|
public Map<String, Object> create(
|
||||||
|
@RequestBody Map<String, Object> params) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
Long userId = StpUtil.getLoginIdAsLong();
|
||||||
|
List<Long> cartIds = (List<Long>) params.get("cartIds");
|
||||||
|
String receiverName = (String) params.get("receiverName");
|
||||||
|
String receiverPhone = (String) params.get("receiverPhone");
|
||||||
|
String receiverAddress = (String) params.get("receiverAddress");
|
||||||
|
String remark = (String) params.get("remark");
|
||||||
|
|
||||||
|
Order order = orderService.create(userId, cartIds, receiverName, receiverPhone, receiverAddress, remark);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "下单成功");
|
||||||
|
result.put("data", order);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Map<String, Object> getList(@RequestParam(required = false) Integer status) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
Long userId = StpUtil.getLoginIdAsLong();
|
||||||
|
List<Order> list = orderService.getByUserId(userId, status);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("data", list);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/admin/list")
|
||||||
|
public Map<String, Object> adminList(@RequestParam(required = false) String keyword, @RequestParam(required = false) Integer status) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
List<Order> list = orderService.getList(keyword, status);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("data", list);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public Map<String, Object> getById(@PathVariable Long id) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
Order order = orderService.getById(id);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("data", order);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/status/{id}")
|
||||||
|
public Map<String, Object> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
orderService.updateStatus(id, status);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "操作成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/revenue/{type}")
|
||||||
|
public Map<String, Object> getRevenueStatistics(@PathVariable String type) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("data", orderService.getRevenueStatistics(type));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/top/{limit}")
|
||||||
|
public Map<String, Object> getTopProducts(@PathVariable Integer limit) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("data", orderService.getTopProducts(limit));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.meiruo.cosmetics.controller;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Product;
|
||||||
|
import com.meiruo.cosmetics.service.ProductService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/product")
|
||||||
|
public class ProductController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ProductService productService;
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Map<String, Object> getList(
|
||||||
|
@RequestParam(required = false) Long categoryId,
|
||||||
|
@RequestParam(required = false) String keyword) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
List<Product> list = productService.getList(categoryId, keyword);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("data", list);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/recommend")
|
||||||
|
public Map<String, Object> getRecommend() {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("data", productService.getRecommend());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public Map<String, Object> getById(@PathVariable Long id) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
Product product = productService.getById(id);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("data", product);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public Map<String, Object> add(@RequestBody Product product) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
productService.add(product);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "添加成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping
|
||||||
|
public Map<String, Object> update(@RequestBody Product product) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
productService.update(product);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "修改成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public Map<String, Object> delete(@PathVariable Long id) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
productService.delete(id);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "删除成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.meiruo.cosmetics.controller;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/upload")
|
||||||
|
@CrossOrigin
|
||||||
|
public class UploadController {
|
||||||
|
|
||||||
|
private static final String UPLOAD_PATH = "src/main/resources/static/upload/";
|
||||||
|
|
||||||
|
@PostMapping("/image")
|
||||||
|
public Map<String, Object> uploadImage(@RequestParam("file") MultipartFile file) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
result.put("code", 500);
|
||||||
|
result.put("msg", "请选择要上传的文件");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String originalFilename = file.getOriginalFilename();
|
||||||
|
String ext = originalFilename.substring(originalFilename.lastIndexOf("."));
|
||||||
|
|
||||||
|
String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||||
|
String fileName = System.currentTimeMillis() + ext;
|
||||||
|
|
||||||
|
Path uploadDir = Paths.get(UPLOAD_PATH + dateDir);
|
||||||
|
if (!Files.exists(uploadDir)) {
|
||||||
|
Files.createDirectories(uploadDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path filePath = uploadDir.resolve(fileName);
|
||||||
|
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
|
||||||
|
String url = "/upload/" + dateDir + "/" + fileName;
|
||||||
|
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "上传成功");
|
||||||
|
result.put("url", url);
|
||||||
|
return result;
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
result.put("code", 500);
|
||||||
|
result.put("msg", "文件上传失败");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.meiruo.cosmetics.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import com.meiruo.cosmetics.entity.User;
|
||||||
|
import com.meiruo.cosmetics.service.UserService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/user")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
@PostMapping("/login")
|
||||||
|
public Map<String, Object> login(@RequestBody User user) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
User loginUser = userService.login(user.getUsername(), user.getPassword());
|
||||||
|
if (loginUser == null) {
|
||||||
|
result.put("code", 500);
|
||||||
|
result.put("msg", "用户名或密码错误");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("id", loginUser.getId());
|
||||||
|
data.put("username", loginUser.getUsername());
|
||||||
|
data.put("nickname", loginUser.getNickname());
|
||||||
|
data.put("avatar", loginUser.getAvatar());
|
||||||
|
data.put("role", loginUser.getRole());
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "登录成功");
|
||||||
|
result.put("data", data);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
public Map<String, Object> register(@RequestBody User user) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
User registerUser = userService.register(user);
|
||||||
|
if (registerUser == null) {
|
||||||
|
result.put("code", 500);
|
||||||
|
result.put("msg", "用户名已存在");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "注册成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/info")
|
||||||
|
public Map<String, Object> getInfo() {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
Long userId = StpUtil.getLoginIdAsLong();
|
||||||
|
User user = userService.getById(userId);
|
||||||
|
if (user == null) {
|
||||||
|
result.put("code", 500);
|
||||||
|
result.put("msg", "用户不存在");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("data", user);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/info")
|
||||||
|
public Map<String, Object> updateInfo(@RequestBody User user) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
Long userId = StpUtil.getLoginIdAsLong();
|
||||||
|
user.setId(userId);
|
||||||
|
userService.update(user);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "修改成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/logout")
|
||||||
|
public Map<String, Object> logout() {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
StpUtil.logout();
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "退出成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Map<String, Object> getList(@RequestParam(required = false) String query) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("data", userService.getList(query));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/status/{id}")
|
||||||
|
public Map<String, Object> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
userService.updateStatus(id, status);
|
||||||
|
result.put("code", 200);
|
||||||
|
result.put("msg", "操作成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.meiruo.cosmetics.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Banner {
|
||||||
|
private Long id;
|
||||||
|
private String title;
|
||||||
|
private String image;
|
||||||
|
private String link;
|
||||||
|
private Integer sort;
|
||||||
|
private Integer status;
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.meiruo.cosmetics.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Cart {
|
||||||
|
private Long id;
|
||||||
|
private Long userId;
|
||||||
|
private Long productId;
|
||||||
|
private Integer quantity;
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.meiruo.cosmetics.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Category {
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private Integer sort;
|
||||||
|
private Integer status;
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.meiruo.cosmetics.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Order {
|
||||||
|
private Long id;
|
||||||
|
private String orderNo;
|
||||||
|
private Long userId;
|
||||||
|
private BigDecimal totalAmount;
|
||||||
|
private Integer status;
|
||||||
|
private String receiverName;
|
||||||
|
private String receiverPhone;
|
||||||
|
private String receiverAddress;
|
||||||
|
private String remark;
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
private LocalDateTime payTime;
|
||||||
|
private LocalDateTime shipTime;
|
||||||
|
private LocalDateTime receiveTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.meiruo.cosmetics.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class OrderItem {
|
||||||
|
private Long id;
|
||||||
|
private Long orderId;
|
||||||
|
private Long productId;
|
||||||
|
private String productName;
|
||||||
|
private String productImage;
|
||||||
|
private BigDecimal price;
|
||||||
|
private Integer quantity;
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.meiruo.cosmetics.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Product {
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private BigDecimal price;
|
||||||
|
private Integer stock;
|
||||||
|
private Long categoryId;
|
||||||
|
private String image;
|
||||||
|
private String images;
|
||||||
|
private Integer status;
|
||||||
|
private Integer sales;
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.meiruo.cosmetics.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class User {
|
||||||
|
private Long id;
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
private String nickname;
|
||||||
|
private String phone;
|
||||||
|
private String email;
|
||||||
|
private String avatar;
|
||||||
|
private Integer role;
|
||||||
|
private Integer status;
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.meiruo.cosmetics.mapper;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Banner;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface BannerMapper {
|
||||||
|
|
||||||
|
List<Banner> selectList(@Param("status") Integer status);
|
||||||
|
|
||||||
|
Banner selectById(@Param("id") Long id);
|
||||||
|
|
||||||
|
int insert(Banner banner);
|
||||||
|
|
||||||
|
int update(Banner banner);
|
||||||
|
|
||||||
|
int delete(@Param("id") Long id);
|
||||||
|
|
||||||
|
int updateSort(@Param("id") Long id, @Param("sort") Integer sort);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.meiruo.cosmetics.mapper;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Cart;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface CartMapper {
|
||||||
|
|
||||||
|
Cart selectByUserAndProduct(@Param("userId") Long userId, @Param("productId") Long productId);
|
||||||
|
|
||||||
|
List<Cart> selectByUserId(@Param("userId") Long userId);
|
||||||
|
|
||||||
|
int insert(Cart cart);
|
||||||
|
|
||||||
|
int update(Cart cart);
|
||||||
|
|
||||||
|
int delete(@Param("id") Long id);
|
||||||
|
|
||||||
|
int deleteByUserId(@Param("userId") Long userId);
|
||||||
|
|
||||||
|
int updateQuantity(@Param("id") Long id, @Param("quantity") Integer quantity);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.meiruo.cosmetics.mapper;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Category;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface CategoryMapper {
|
||||||
|
|
||||||
|
Category selectById(@Param("id") Long id);
|
||||||
|
|
||||||
|
List<Category> selectList(@Param("status") Integer status);
|
||||||
|
|
||||||
|
int insert(Category category);
|
||||||
|
|
||||||
|
int update(Category category);
|
||||||
|
|
||||||
|
int delete(@Param("id") Long id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.meiruo.cosmetics.mapper;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.OrderItem;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface OrderItemMapper {
|
||||||
|
|
||||||
|
List<OrderItem> selectByOrderId(@Param("orderId") Long orderId);
|
||||||
|
|
||||||
|
int insert(OrderItem orderItem);
|
||||||
|
|
||||||
|
int insertBatch(@Param("items") List<OrderItem> items);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.meiruo.cosmetics.mapper;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Order;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface OrderMapper {
|
||||||
|
|
||||||
|
Order selectById(@Param("id") Long id);
|
||||||
|
|
||||||
|
Order selectByOrderNo(@Param("orderNo") String orderNo);
|
||||||
|
|
||||||
|
List<Order> selectByUserId(@Param("userId") Long userId, @Param("status") Integer status);
|
||||||
|
|
||||||
|
List<Order> selectList(@Param("keyword") String keyword, @Param("status") Integer status);
|
||||||
|
|
||||||
|
int insert(Order order);
|
||||||
|
|
||||||
|
int update(Order order);
|
||||||
|
|
||||||
|
int updateStatus(@Param("id") Long id, @Param("status") Integer status);
|
||||||
|
|
||||||
|
List<Map<String, Object>> selectRevenueStatistics(@Param("type") String type);
|
||||||
|
|
||||||
|
List<Map<String, Object>> selectTopProducts(@Param("limit") Integer limit);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.meiruo.cosmetics.mapper;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Product;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ProductMapper {
|
||||||
|
|
||||||
|
Product selectById(@Param("id") Long id);
|
||||||
|
|
||||||
|
List<Product> selectList(@Param("categoryId") Long categoryId, @Param("keyword") String keyword);
|
||||||
|
|
||||||
|
List<Product> selectRecommend();
|
||||||
|
|
||||||
|
List<Product> selectByIds(@Param("ids") List<Long> ids);
|
||||||
|
|
||||||
|
int insert(Product product);
|
||||||
|
|
||||||
|
int update(Product product);
|
||||||
|
|
||||||
|
int delete(@Param("id") Long id);
|
||||||
|
|
||||||
|
int updateStock(@Param("id") Long id, @Param("count") Integer count);
|
||||||
|
|
||||||
|
int incrementSales(@Param("id") Long id, @Param("count") Integer count);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.meiruo.cosmetics.mapper;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.User;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface UserMapper {
|
||||||
|
|
||||||
|
User selectById(@Param("id") Long id);
|
||||||
|
|
||||||
|
User selectByUsername(@Param("username") String username);
|
||||||
|
|
||||||
|
User selectByPhone(@Param("phone") String phone);
|
||||||
|
|
||||||
|
List<User> selectList(@Param("query") String query);
|
||||||
|
|
||||||
|
int insert(User user);
|
||||||
|
|
||||||
|
int update(User user);
|
||||||
|
|
||||||
|
int delete(@Param("id") Long id);
|
||||||
|
|
||||||
|
int updateStatus(@Param("id") Long id, @Param("status") Integer status);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.meiruo.cosmetics.service;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Banner;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface BannerService {
|
||||||
|
|
||||||
|
List<Banner> getList(Integer status);
|
||||||
|
|
||||||
|
Banner getById(Long id);
|
||||||
|
|
||||||
|
void add(Banner banner);
|
||||||
|
|
||||||
|
void update(Banner banner);
|
||||||
|
|
||||||
|
void delete(Long id);
|
||||||
|
|
||||||
|
void updateSort(Long id, Integer sort);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.meiruo.cosmetics.service;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Cart;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface CartService {
|
||||||
|
|
||||||
|
List<Map<String, Object>> getCartList(Long userId);
|
||||||
|
|
||||||
|
void add(Long userId, Long productId, Integer quantity);
|
||||||
|
|
||||||
|
void updateQuantity(Long id, Integer quantity);
|
||||||
|
|
||||||
|
void delete(Long id);
|
||||||
|
|
||||||
|
void clear(Long userId);
|
||||||
|
|
||||||
|
void mergeFromCookie(Long userId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.meiruo.cosmetics.service;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Category;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface CategoryService {
|
||||||
|
|
||||||
|
Category getById(Long id);
|
||||||
|
|
||||||
|
List<Category> getList(Integer status);
|
||||||
|
|
||||||
|
void add(Category category);
|
||||||
|
|
||||||
|
void update(Category category);
|
||||||
|
|
||||||
|
void delete(Long id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.meiruo.cosmetics.service;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Order;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface OrderService {
|
||||||
|
|
||||||
|
Order create(Long userId, List<Long> cartIds, String receiverName, String receiverPhone, String receiverAddress, String remark);
|
||||||
|
|
||||||
|
Order getById(Long id);
|
||||||
|
|
||||||
|
Order getByOrderNo(String orderNo);
|
||||||
|
|
||||||
|
List<Order> getByUserId(Long userId, Integer status);
|
||||||
|
|
||||||
|
List<Order> getList(String keyword, Integer status);
|
||||||
|
|
||||||
|
void updateStatus(Long id, Integer status);
|
||||||
|
|
||||||
|
Map<String, Object> getRevenueStatistics(String type);
|
||||||
|
|
||||||
|
List<Map<String, Object>> getTopProducts(Integer limit);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.meiruo.cosmetics.service;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Product;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ProductService {
|
||||||
|
|
||||||
|
Product getById(Long id);
|
||||||
|
|
||||||
|
List<Product> getList(Long categoryId, String keyword);
|
||||||
|
|
||||||
|
List<Product> getRecommend();
|
||||||
|
|
||||||
|
List<Product> getByIds(List<Long> ids);
|
||||||
|
|
||||||
|
void add(Product product);
|
||||||
|
|
||||||
|
void update(Product product);
|
||||||
|
|
||||||
|
void delete(Long id);
|
||||||
|
|
||||||
|
void updateStock(Long id, Integer count);
|
||||||
|
|
||||||
|
void incrementSales(Long id, Integer count);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.meiruo.cosmetics.service;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.User;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface UserService {
|
||||||
|
|
||||||
|
User login(String username, String password);
|
||||||
|
|
||||||
|
User register(User user);
|
||||||
|
|
||||||
|
User getById(Long id);
|
||||||
|
|
||||||
|
User getByUsername(String username);
|
||||||
|
|
||||||
|
List<User> getList(String query);
|
||||||
|
|
||||||
|
void update(User user);
|
||||||
|
|
||||||
|
void delete(Long id);
|
||||||
|
|
||||||
|
void updateStatus(Long id, Integer status);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.meiruo.cosmetics.service.impl;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Banner;
|
||||||
|
import com.meiruo.cosmetics.mapper.BannerMapper;
|
||||||
|
import com.meiruo.cosmetics.service.BannerService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class BannerServiceImpl implements BannerService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BannerMapper bannerMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Banner> getList(Integer status) {
|
||||||
|
return bannerMapper.selectList(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Banner getById(Long id) {
|
||||||
|
return bannerMapper.selectById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void add(Banner banner) {
|
||||||
|
banner.setStatus(1);
|
||||||
|
bannerMapper.insert(banner);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(Banner banner) {
|
||||||
|
bannerMapper.update(banner);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(Long id) {
|
||||||
|
bannerMapper.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateSort(Long id, Integer sort) {
|
||||||
|
bannerMapper.updateSort(id, sort);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.meiruo.cosmetics.service.impl;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Cart;
|
||||||
|
import com.meiruo.cosmetics.entity.Product;
|
||||||
|
import com.meiruo.cosmetics.mapper.CartMapper;
|
||||||
|
import com.meiruo.cosmetics.mapper.ProductMapper;
|
||||||
|
import com.meiruo.cosmetics.service.CartService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CartServiceImpl implements CartService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CartMapper cartMapper;
|
||||||
|
@Autowired
|
||||||
|
private ProductMapper productMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getCartList(Long userId) {
|
||||||
|
return cartMapper.selectByUserId(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void add(Long userId, Long productId, Integer quantity) {
|
||||||
|
Cart existCart = cartMapper.selectByUserAndProduct(userId, productId);
|
||||||
|
if (existCart != null) {
|
||||||
|
cartMapper.updateQuantity(existCart.getId(), existCart.getQuantity() + quantity);
|
||||||
|
} else {
|
||||||
|
Cart cart = new Cart();
|
||||||
|
cart.setUserId(userId);
|
||||||
|
cart.setProductId(productId);
|
||||||
|
cart.setQuantity(quantity);
|
||||||
|
cartMapper.insert(cart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateQuantity(Long id, Integer quantity) {
|
||||||
|
cartMapper.updateQuantity(id, quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(Long id) {
|
||||||
|
cartMapper.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear(Long userId) {
|
||||||
|
cartMapper.deleteByUserId(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void mergeFromCookie(Long userId) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.meiruo.cosmetics.service.impl;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Category;
|
||||||
|
import com.meiruo.cosmetics.mapper.CategoryMapper;
|
||||||
|
import com.meiruo.cosmetics.service.CategoryService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CategoryServiceImpl implements CategoryService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CategoryMapper categoryMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Category getById(Long id) {
|
||||||
|
return categoryMapper.selectById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Category> getList(Integer status) {
|
||||||
|
return categoryMapper.selectList(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void add(Category category) {
|
||||||
|
category.setStatus(1);
|
||||||
|
categoryMapper.insert(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(Category category) {
|
||||||
|
categoryMapper.update(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(Long id) {
|
||||||
|
categoryMapper.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.meiruo.cosmetics.service.impl;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Order;
|
||||||
|
import com.meiruo.cosmetics.entity.OrderItem;
|
||||||
|
import com.meiruo.cosmetics.mapper.OrderItemMapper;
|
||||||
|
import com.meiruo.cosmetics.mapper.OrderMapper;
|
||||||
|
import com.meiruo.cosmetics.mapper.ProductMapper;
|
||||||
|
import com.meiruo.cosmetics.service.OrderService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class OrderServiceImpl implements OrderService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private OrderMapper orderMapper;
|
||||||
|
@Autowired
|
||||||
|
private OrderItemMapper orderItemMapper;
|
||||||
|
@Autowired
|
||||||
|
private ProductMapper productMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Order create(Long userId, List<Long> cartIds, String receiverName, String receiverPhone, String receiverAddress, String remark) {
|
||||||
|
Order order = new Order();
|
||||||
|
order.setOrderNo(UUID.randomUUID().toString().replace("-", ""));
|
||||||
|
order.setUserId(userId);
|
||||||
|
order.setReceiverName(receiverName);
|
||||||
|
order.setReceiverPhone(receiverPhone);
|
||||||
|
order.setReceiverAddress(receiverAddress);
|
||||||
|
order.setRemark(remark);
|
||||||
|
order.setStatus(1);
|
||||||
|
|
||||||
|
List<OrderItem> orderItems = new ArrayList<>();
|
||||||
|
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
for (Long cartId : cartIds) {
|
||||||
|
}
|
||||||
|
|
||||||
|
order.setTotalAmount(totalAmount);
|
||||||
|
orderMapper.insert(order);
|
||||||
|
|
||||||
|
for (OrderItem item : orderItems) {
|
||||||
|
item.setOrderId(order.getId());
|
||||||
|
orderItemMapper.insert(item);
|
||||||
|
productMapper.updateStock(item.getProductId(), item.getQuantity());
|
||||||
|
productMapper.incrementSales(item.getProductId(), item.getQuantity());
|
||||||
|
}
|
||||||
|
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Order getById(Long id) {
|
||||||
|
return orderMapper.selectById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Order getByOrderNo(String orderNo) {
|
||||||
|
return orderMapper.selectByOrderNo(orderNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Order> getByUserId(Long userId, Integer status) {
|
||||||
|
return orderMapper.selectByUserId(userId, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Order> getList(String keyword, Integer status) {
|
||||||
|
return orderMapper.selectList(keyword, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateStatus(Long id, Integer status) {
|
||||||
|
orderMapper.updateStatus(id, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getRevenueStatistics(String type) {
|
||||||
|
List<Map<String, Object>> statistics = orderMapper.selectRevenueStatistics(type);
|
||||||
|
BigDecimal total = BigDecimal.ZERO;
|
||||||
|
for (Map<String, Object> stat : statistics) {
|
||||||
|
total = total.add(new BigDecimal(stat.get("amount").toString()));
|
||||||
|
}
|
||||||
|
return Map.of("list", statistics, "total", total);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getTopProducts(Integer limit) {
|
||||||
|
return orderMapper.selectTopProducts(limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.meiruo.cosmetics.service.impl;
|
||||||
|
|
||||||
|
import com.meiruo.cosmetics.entity.Product;
|
||||||
|
import com.meiruo.cosmetics.mapper.ProductMapper;
|
||||||
|
import com.meiruo.cosmetics.service.ProductService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ProductServiceImpl implements ProductService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ProductMapper productMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Product getById(Long id) {
|
||||||
|
return productMapper.selectById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Product> getList(Long categoryId, String keyword) {
|
||||||
|
return productMapper.selectList(categoryId, keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Product> getRecommend() {
|
||||||
|
return productMapper.selectRecommend();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Product> getByIds(List<Long> ids) {
|
||||||
|
return productMapper.selectByIds(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void add(Product product) {
|
||||||
|
product.setStatus(1);
|
||||||
|
product.setSales(0);
|
||||||
|
productMapper.insert(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(Product product) {
|
||||||
|
productMapper.update(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(Long id) {
|
||||||
|
productMapper.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateStock(Long id, Integer count) {
|
||||||
|
productMapper.updateStock(id, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void incrementSales(Long id, Integer count) {
|
||||||
|
productMapper.incrementSales(id, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.meiruo.cosmetics.service.impl;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import cn.dev33.satoken.util.SaResult;
|
||||||
|
import com.meiruo.cosmetics.entity.User;
|
||||||
|
import com.meiruo.cosmetics.mapper.UserMapper;
|
||||||
|
import com.meiruo.cosmetics.service.UserService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.DigestUtils;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class UserServiceImpl implements UserService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserMapper userMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User login(String username, String password) {
|
||||||
|
User user = userMapper.selectByUsername(username);
|
||||||
|
if (user == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String md5Password = DigestUtils.md5DigestAsHex(password.getBytes(StandardCharsets.UTF_8));
|
||||||
|
if (!md5Password.equals(user.getPassword())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (user.getStatus() != 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
StpUtil.login(user.getId());
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User register(User user) {
|
||||||
|
User existUser = userMapper.selectByUsername(user.getUsername());
|
||||||
|
if (existUser != null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
user.setPassword(DigestUtils.md5DigestAsHex(user.getPassword().getBytes(StandardCharsets.UTF_8)));
|
||||||
|
user.setRole(0);
|
||||||
|
user.setStatus(1);
|
||||||
|
userMapper.insert(user);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getById(Long id) {
|
||||||
|
return userMapper.selectById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getByUsername(String username) {
|
||||||
|
return userMapper.selectByUsername(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<User> getList(String query) {
|
||||||
|
return userMapper.selectList(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(User user) {
|
||||||
|
userMapper.update(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(Long id) {
|
||||||
|
userMapper.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateStatus(Long id, Integer status) {
|
||||||
|
userMapper.updateStatus(id, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
meiruo-backend/src/main/resources/application.yml
Normal file
31
meiruo-backend/src/main/resources/application.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
url: jdbc:mysql://localhost:3306/meiruo_cosmetics?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||||
|
username: root
|
||||||
|
password: root
|
||||||
|
|
||||||
|
mybatis:
|
||||||
|
mapper-locations: classpath:mapper/*.xml
|
||||||
|
type-aliases-package: com.meiruo.cosmetics.entity
|
||||||
|
configuration:
|
||||||
|
map-underscore-to-camel-case: true
|
||||||
|
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||||
|
|
||||||
|
sa-token:
|
||||||
|
token-name: Authorization
|
||||||
|
header: Authorization
|
||||||
|
type: jwt
|
||||||
|
jwt-secret-key: meiruo-cosmetics-secret-key-2024
|
||||||
|
token-prefix: Bearer
|
||||||
|
expiration: 2592000
|
||||||
|
is-share: true
|
||||||
|
is-log: true
|
||||||
|
|
||||||
|
pagehelper:
|
||||||
|
helper-dialect: mysql
|
||||||
|
reasonable: true
|
||||||
|
support-methods-arguments: true
|
||||||
52
meiruo-backend/src/main/resources/mapper/BannerMapper.xml
Normal file
52
meiruo-backend/src/main/resources/mapper/BannerMapper.xml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.meiruo.cosmetics.mapper.BannerMapper">
|
||||||
|
|
||||||
|
<resultMap id="BaseResultMap" type="com.meiruo.cosmetics.entity.Banner">
|
||||||
|
<id column="id" property="id"/>
|
||||||
|
<result column="title" property="title"/>
|
||||||
|
<result column="image" property="image"/>
|
||||||
|
<result column="link" property="link"/>
|
||||||
|
<result column="sort" property="sort"/>
|
||||||
|
<result column="status" property="status"/>
|
||||||
|
<result column="create_time" property="createTime"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<select id="selectList" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM banner
|
||||||
|
<where>
|
||||||
|
<if test="status != null">AND status = #{status}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY sort ASC, create_time DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectById" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM banner WHERE id = #{id}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO banner (title, image, link, sort, status, create_time)
|
||||||
|
VALUES (#{title}, #{image}, #{link}, #{sort}, #{status}, NOW())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="update">
|
||||||
|
UPDATE banner
|
||||||
|
<set>
|
||||||
|
<if test="title != null">title = #{title},</if>
|
||||||
|
<if test="image != null">image = #{image},</if>
|
||||||
|
<if test="link != null">link = #{link},</if>
|
||||||
|
<if test="sort != null">sort = #{sort},</if>
|
||||||
|
<if test="status != null">status = #{status},</if>
|
||||||
|
</set>
|
||||||
|
WHERE id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="delete">
|
||||||
|
DELETE FROM banner WHERE id = #{id}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<update id="updateSort">
|
||||||
|
UPDATE banner SET sort = #{sort} WHERE id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
62
meiruo-backend/src/main/resources/mapper/CartMapper.xml
Normal file
62
meiruo-backend/src/main/resources/mapper/CartMapper.xml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.meiruo.cosmetics.mapper.CartMapper">
|
||||||
|
|
||||||
|
<resultMap id="BaseResultMap" type="com.meiruo.cosmetics.entity.Cart">
|
||||||
|
<id column="id" property="id"/>
|
||||||
|
<result column="user_id" property="userId"/>
|
||||||
|
<result column="product_id" property="productId"/>
|
||||||
|
<result column="quantity" property="quantity"/>
|
||||||
|
<result column="create_time" property="createTime"/>
|
||||||
|
<result column="update_time" property="updateTime"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<resultMap id="CartWithProductResultMap" type="java.util.Map">
|
||||||
|
<id column="id" property="id"/>
|
||||||
|
<result column="user_id" property="userId"/>
|
||||||
|
<result column="product_id" property="productId"/>
|
||||||
|
<result column="quantity" property="quantity"/>
|
||||||
|
<result column="product_name" property="productName"/>
|
||||||
|
<result column="product_price" property="productPrice"/>
|
||||||
|
<result column="product_image" property="productImage"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<select id="selectByUserAndProduct" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM cart WHERE user_id = #{userId} AND product_id = #{productId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByUserId" resultMap="CartWithProductResultMap">
|
||||||
|
SELECT c.id, c.user_id, c.product_id, c.quantity,
|
||||||
|
p.name as product_name, p.price as product_price, p.image as product_image
|
||||||
|
FROM cart c
|
||||||
|
LEFT JOIN product p ON c.product_id = p.id
|
||||||
|
WHERE c.user_id = #{userId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO cart (user_id, product_id, quantity, create_time, update_time)
|
||||||
|
VALUES (#{userId}, #{productId}, #{quantity}, NOW(), NOW())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="update">
|
||||||
|
UPDATE cart
|
||||||
|
<set>
|
||||||
|
<if test="quantity != null">quantity = #{quantity},</if>
|
||||||
|
update_time = NOW()
|
||||||
|
</set>
|
||||||
|
WHERE id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="delete">
|
||||||
|
DELETE FROM cart WHERE id = #{id}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<delete id="deleteByUserId">
|
||||||
|
DELETE FROM cart WHERE user_id = #{userId}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<update id="updateQuantity">
|
||||||
|
UPDATE cart SET quantity = #{quantity}, update_time = NOW() WHERE id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
46
meiruo-backend/src/main/resources/mapper/CategoryMapper.xml
Normal file
46
meiruo-backend/src/main/resources/mapper/CategoryMapper.xml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.meiruo.cosmetics.mapper.CategoryMapper">
|
||||||
|
|
||||||
|
<resultMap id="BaseResultMap" type="com.meiruo.cosmetics.entity.Category">
|
||||||
|
<id column="id" property="id"/>
|
||||||
|
<result column="name" property="name"/>
|
||||||
|
<result column="description" property="description"/>
|
||||||
|
<result column="sort" property="sort"/>
|
||||||
|
<result column="status" property="status"/>
|
||||||
|
<result column="create_time" property="createTime"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<select id="selectById" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM category WHERE id = #{id}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectList" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM category
|
||||||
|
<where>
|
||||||
|
<if test="status != null">AND status = #{status}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY sort ASC, create_time DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO category (name, description, sort, status, create_time)
|
||||||
|
VALUES (#{name}, #{description}, #{sort}, #{status}, NOW())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="update">
|
||||||
|
UPDATE category
|
||||||
|
<set>
|
||||||
|
<if test="name != null">name = #{name},</if>
|
||||||
|
<if test="description != null">description = #{description},</if>
|
||||||
|
<if test="sort != null">sort = #{sort},</if>
|
||||||
|
<if test="status != null">status = #{status},</if>
|
||||||
|
</set>
|
||||||
|
WHERE id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="delete">
|
||||||
|
DELETE FROM category WHERE id = #{id}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
33
meiruo-backend/src/main/resources/mapper/OrderItemMapper.xml
Normal file
33
meiruo-backend/src/main/resources/mapper/OrderItemMapper.xml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.meiruo.cosmetics.mapper.OrderItemMapper">
|
||||||
|
|
||||||
|
<resultMap id="BaseResultMap" type="com.meiruo.cosmetics.entity.OrderItem">
|
||||||
|
<id column="id" property="id"/>
|
||||||
|
<result column="order_id" property="orderId"/>
|
||||||
|
<result column="product_id" property="productId"/>
|
||||||
|
<result column="product_name" property="productName"/>
|
||||||
|
<result column="product_image" property="productImage"/>
|
||||||
|
<result column="price" property="price"/>
|
||||||
|
<result column="quantity" property="quantity"/>
|
||||||
|
<result column="create_time" property="createTime"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<select id="selectByOrderId" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM order_item WHERE order_id = #{orderId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO order_item (order_id, product_id, product_name, product_image, price, quantity, create_time)
|
||||||
|
VALUES (#{orderId}, #{productId}, #{productName}, #{productImage}, #{price}, #{quantity}, NOW())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<insert id="insertBatch">
|
||||||
|
INSERT INTO order_item (order_id, product_id, product_name, product_image, price, quantity, create_time)
|
||||||
|
VALUES
|
||||||
|
<foreach collection="items" item="item" separator=",">
|
||||||
|
(#{item.orderId}, #{item.productId}, #{item.productName}, #{item.productImage}, #{item.price}, #{item.quantity}, NOW())
|
||||||
|
</foreach>
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
119
meiruo-backend/src/main/resources/mapper/OrderMapper.xml
Normal file
119
meiruo-backend/src/main/resources/mapper/OrderMapper.xml
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.meiruo.cosmetics.mapper.OrderMapper">
|
||||||
|
|
||||||
|
<resultMap id="BaseResultMap" type="com.meiruo.cosmetics.entity.Order">
|
||||||
|
<id column="id" property="id"/>
|
||||||
|
<result column="order_no" property="orderNo"/>
|
||||||
|
<result column="user_id" property="userId"/>
|
||||||
|
<result column="total_amount" property="totalAmount"/>
|
||||||
|
<result column="status" property="status"/>
|
||||||
|
<result column="receiver_name" property="receiverName"/>
|
||||||
|
<result column="receiver_phone" property="receiverPhone"/>
|
||||||
|
<result column="receiver_address" property="receiverAddress"/>
|
||||||
|
<result column="remark" property="remark"/>
|
||||||
|
<result column="create_time" property="createTime"/>
|
||||||
|
<result column="pay_time" property="payTime"/>
|
||||||
|
<result column="ship_time" property="shipTime"/>
|
||||||
|
<result column="receive_time" property="receiveTime"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<select id="selectById" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM `order` WHERE id = #{id}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByOrderNo" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM `order` WHERE order_no = #{orderNo}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByUserId" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM `order`
|
||||||
|
<where>
|
||||||
|
<if test="userId != null">user_id = #{userId}</if>
|
||||||
|
<if test="status != null">AND status = #{status}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY create_time DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectList" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM `order`
|
||||||
|
<where>
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
AND (order_no LIKE CONCAT('%', #{keyword}, '%')
|
||||||
|
OR receiver_name LIKE CONCAT('%', #{keyword}, '%'))
|
||||||
|
</if>
|
||||||
|
<if test="status != null">AND status = #{status}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY create_time DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO `order` (order_no, user_id, total_amount, status, receiver_name, receiver_phone, receiver_address, remark, create_time)
|
||||||
|
VALUES (#{orderNo}, #{userId}, #{totalAmount}, #{status}, #{receiverName}, #{receiverPhone}, #{receiverAddress}, #{remark}, NOW())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="update">
|
||||||
|
UPDATE `order`
|
||||||
|
<set>
|
||||||
|
<if test="status != null">status = #{status},</if>
|
||||||
|
<if test="receiverName != null">receiver_name = #{receiverName},</if>
|
||||||
|
<if test="receiverPhone != null">receiver_phone = #{receiverPhone},</if>
|
||||||
|
<if test="receiverAddress != null">receiver_address = #{receiverAddress},</if>
|
||||||
|
<if test="remark != null">remark = #{remark},</if>
|
||||||
|
<if test="payTime != null">pay_time = #{payTime},</if>
|
||||||
|
<if test="shipTime != null">ship_time = #{shipTime},</if>
|
||||||
|
<if test="receiveTime != null">receive_time = #{receiveTime},</if>
|
||||||
|
</set>
|
||||||
|
WHERE id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="updateStatus">
|
||||||
|
UPDATE `order`
|
||||||
|
<set>
|
||||||
|
status = #{status}
|
||||||
|
<if test="status == 2">, pay_time = NOW()</if>
|
||||||
|
<if test="status == 3">, ship_time = NOW()</if>
|
||||||
|
<if test="status == 4">, receive_time = NOW()</if>
|
||||||
|
</set>
|
||||||
|
WHERE id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<select id="selectRevenueStatistics" resultType="java.util.Map">
|
||||||
|
SELECT
|
||||||
|
<choose>
|
||||||
|
<when test="type == 'week'">
|
||||||
|
DATE(create_time) as date,
|
||||||
|
</when>
|
||||||
|
<when test="type == 'month'">
|
||||||
|
DATE(create_time) as date,
|
||||||
|
</when>
|
||||||
|
<otherwise>
|
||||||
|
DATE(create_time) as date,
|
||||||
|
</otherwise>
|
||||||
|
</choose>
|
||||||
|
SUM(total_amount) as amount,
|
||||||
|
COUNT(*) as orderCount
|
||||||
|
FROM `order`
|
||||||
|
WHERE status IN (2, 3, 4)
|
||||||
|
<if test="type == 'week'">
|
||||||
|
AND create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
|
||||||
|
</if>
|
||||||
|
<if test="type == 'month'">
|
||||||
|
AND create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||||
|
</if>
|
||||||
|
GROUP BY DATE(create_time)
|
||||||
|
ORDER BY date
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectTopProducts" resultType="java.util.Map">
|
||||||
|
SELECT p.id, p.name, p.image, SUM(oi.quantity) as totalSales
|
||||||
|
FROM order_item oi
|
||||||
|
LEFT JOIN product p ON oi.product_id = p.id
|
||||||
|
LEFT JOIN `order` o ON oi.order_id = o.id
|
||||||
|
WHERE o.status IN (2, 3, 4)
|
||||||
|
GROUP BY p.id
|
||||||
|
ORDER BY totalSales DESC
|
||||||
|
LIMIT #{limit}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
81
meiruo-backend/src/main/resources/mapper/ProductMapper.xml
Normal file
81
meiruo-backend/src/main/resources/mapper/ProductMapper.xml
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.meiruo.cosmetics.mapper.ProductMapper">
|
||||||
|
|
||||||
|
<resultMap id="BaseResultMap" type="com.meiruo.cosmetics.entity.Product">
|
||||||
|
<id column="id" property="id"/>
|
||||||
|
<result column="name" property="name"/>
|
||||||
|
<result column="description" property="description"/>
|
||||||
|
<result column="price" property="price"/>
|
||||||
|
<result column="stock" property="stock"/>
|
||||||
|
<result column="category_id" property="categoryId"/>
|
||||||
|
<result column="image" property="image"/>
|
||||||
|
<result column="images" property="images"/>
|
||||||
|
<result column="status" property="status"/>
|
||||||
|
<result column="sales" property="sales"/>
|
||||||
|
<result column="create_time" property="createTime"/>
|
||||||
|
<result column="update_time" property="updateTime"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<select id="selectById" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM product WHERE id = #{id}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectList" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM product
|
||||||
|
<where>
|
||||||
|
<if test="categoryId != null">AND category_id = #{categoryId}</if>
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
AND (name LIKE CONCAT('%', #{keyword}, '%')
|
||||||
|
OR description LIKE CONCAT('%', #{keyword}, '%'))
|
||||||
|
</if>
|
||||||
|
AND status = 1
|
||||||
|
</where>
|
||||||
|
ORDER BY sales DESC, create_time DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectRecommend" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM product WHERE status = 1 ORDER BY sales DESC LIMIT 10
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByIds" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM product WHERE id IN
|
||||||
|
<foreach collection="ids" item="id" open="(" separator="," close=")">
|
||||||
|
#{id}
|
||||||
|
</foreach>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO product (name, description, price, stock, category_id, image, images, status, sales, create_time, update_time)
|
||||||
|
VALUES (#{name}, #{description}, #{price}, #{stock}, #{categoryId}, #{image}, #{images}, #{status}, 0, NOW(), NOW())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="update">
|
||||||
|
UPDATE product
|
||||||
|
<set>
|
||||||
|
<if test="name != null">name = #{name},</if>
|
||||||
|
<if test="description != null">description = #{description},</if>
|
||||||
|
<if test="price != null">price = #{price},</if>
|
||||||
|
<if test="stock != null">stock = #{stock},</if>
|
||||||
|
<if test="categoryId != null">category_id = #{categoryId},</if>
|
||||||
|
<if test="image != null">image = #{image},</if>
|
||||||
|
<if test="images != null">images = #{images},</if>
|
||||||
|
<if test="status != null">status = #{status},</if>
|
||||||
|
update_time = NOW()
|
||||||
|
</set>
|
||||||
|
WHERE id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="delete">
|
||||||
|
DELETE FROM product WHERE id = #{id}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<update id="updateStock">
|
||||||
|
UPDATE product SET stock = stock - #{count} WHERE id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="incrementSales">
|
||||||
|
UPDATE product SET sales = sales + #{count} WHERE id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
70
meiruo-backend/src/main/resources/mapper/UserMapper.xml
Normal file
70
meiruo-backend/src/main/resources/mapper/UserMapper.xml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.meiruo.cosmetics.mapper.UserMapper">
|
||||||
|
|
||||||
|
<resultMap id="BaseResultMap" type="com.meiruo.cosmetics.entity.User">
|
||||||
|
<id column="id" property="id"/>
|
||||||
|
<result column="username" property="username"/>
|
||||||
|
<result column="password" property="password"/>
|
||||||
|
<result column="nickname" property="nickname"/>
|
||||||
|
<result column="phone" property="phone"/>
|
||||||
|
<result column="email" property="email"/>
|
||||||
|
<result column="avatar" property="avatar"/>
|
||||||
|
<result column="role" property="role"/>
|
||||||
|
<result column="status" property="status"/>
|
||||||
|
<result column="create_time" property="createTime"/>
|
||||||
|
<result column="update_time" property="updateTime"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<select id="selectById" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM user WHERE id = #{id}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByUsername" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM user WHERE username = #{username}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByPhone" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM user WHERE phone = #{phone}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectList" resultMap="BaseResultMap">
|
||||||
|
SELECT * FROM user
|
||||||
|
<where>
|
||||||
|
<if test="query != null and query != ''">
|
||||||
|
AND (username LIKE CONCAT('%', #{query}, '%')
|
||||||
|
OR nickname LIKE CONCAT('%', #{query}, '%')
|
||||||
|
OR phone LIKE CONCAT('%', #{query}, '%'))
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY create_time DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO user (username, password, nickname, phone, email, avatar, role, status, create_time, update_time)
|
||||||
|
VALUES (#{username}, #{password}, #{nickname}, #{phone}, #{email}, #{avatar}, #{role}, #{status}, NOW(), NOW())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="update">
|
||||||
|
UPDATE user
|
||||||
|
<set>
|
||||||
|
<if test="nickname != null">nickname = #{nickname},</if>
|
||||||
|
<if test="phone != null">phone = #{phone},</if>
|
||||||
|
<if test="email != null">email = #{email},</if>
|
||||||
|
<if test="avatar != null">avatar = #{avatar},</if>
|
||||||
|
<if test="password != null">password = #{password},</if>
|
||||||
|
<if test="status != null">status = #{status},</if>
|
||||||
|
update_time = NOW()
|
||||||
|
</set>
|
||||||
|
WHERE id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="delete">
|
||||||
|
DELETE FROM user WHERE id = #{id}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<update id="updateStatus">
|
||||||
|
UPDATE user SET status = #{status}, update_time = NOW() WHERE id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
13
meiruo-frontend/index.html
Normal file
13
meiruo-frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>美若彩妆销售平台</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
25
meiruo-frontend/package.json
Normal file
25
meiruo-frontend/package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "meiruo-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.4.0",
|
||||||
|
"vue-router": "^4.2.5",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"sa-token": "^1.0.0",
|
||||||
|
"element-plus": "^2.4.4",
|
||||||
|
"@element-plus/icons-vue": "^2.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^4.5.2",
|
||||||
|
"vite": "^5.0.10",
|
||||||
|
"sass": "^1.69.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
meiruo-frontend/src/App.vue
Normal file
12
meiruo-frontend/src/App.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<el-config-provider :locale="locale">
|
||||||
|
<router-view />
|
||||||
|
</el-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
|
|
||||||
|
const locale = ref(zhCn)
|
||||||
|
</script>
|
||||||
108
meiruo-frontend/src/api/index.js
Normal file
108
meiruo-frontend/src/api/index.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import router from '../router'
|
||||||
|
import { useUserStore } from '../stores/user'
|
||||||
|
|
||||||
|
const request = axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
|
||||||
|
request.interceptors.request.use(
|
||||||
|
config => {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
request.interceptors.response.use(
|
||||||
|
response => {
|
||||||
|
const res = response.data
|
||||||
|
if (res.code === 200) {
|
||||||
|
return res
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '请求失败')
|
||||||
|
return Promise.reject(res)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
if (error.response && error.response.status === 401) {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('userInfo')
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
ElMessage.error(error.response?.data?.msg || '请求失败')
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default request
|
||||||
|
|
||||||
|
export const userApi = {
|
||||||
|
login: (data) => request.post('/user/login', data),
|
||||||
|
register: (data) => request.post('/user/register', data),
|
||||||
|
getInfo: () => request.get('/user/info'),
|
||||||
|
updateInfo: (data) => request.put('/user/info', data),
|
||||||
|
logout: () => request.post('/user/logout'),
|
||||||
|
getList: (query) => request.get('/user/list', { params: { query } }),
|
||||||
|
updateStatus: (id, status) => request.put(`/user/status/${id}`, null, { params: { status } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const productApi = {
|
||||||
|
getList: (params) => request.get('/product/list', { params }),
|
||||||
|
getRecommend: () => request.get('/product/recommend'),
|
||||||
|
getById: (id) => request.get(`/product/${id}`),
|
||||||
|
add: (data) => request.post('/product', data),
|
||||||
|
update: (data) => request.put('/product', data),
|
||||||
|
delete: (id) => request.delete(`/product/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const categoryApi = {
|
||||||
|
getList: (status) => request.get('/category/list', { params: { status } }),
|
||||||
|
getById: (id) => request.get(`/category/${id}`),
|
||||||
|
add: (data) => request.post('/category', data),
|
||||||
|
update: (data) => request.put('/category', data),
|
||||||
|
delete: (id) => request.delete(`/category/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cartApi = {
|
||||||
|
getList: () => request.get('/cart/list'),
|
||||||
|
add: (productId, quantity) => request.post('/cart/add', null, { params: { productId, quantity } }),
|
||||||
|
updateQuantity: (id, quantity) => request.put(`/cart/quantity/${id}`, null, { params: { quantity } }),
|
||||||
|
delete: (id) => request.delete(`/cart/${id}`),
|
||||||
|
clear: () => request.delete('/cart/clear')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const orderApi = {
|
||||||
|
create: (data) => request.post('/order/create', data),
|
||||||
|
getList: (status) => request.get('/order/list', { params: { status } }),
|
||||||
|
adminList: (params) => request.get('/order/admin/list', { params }),
|
||||||
|
getById: (id) => request.get(`/order/${id}`),
|
||||||
|
updateStatus: (id, status) => request.put(`/order/status/${id}`, null, { params: { status } }),
|
||||||
|
getRevenue: (type) => request.get(`/order/revenue/${type}`),
|
||||||
|
getTopProducts: (limit) => request.get(`/order/top/${limit}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bannerApi = {
|
||||||
|
getList: (status) => request.get('/banner/list', { params: { status } }),
|
||||||
|
getById: (id) => request.get(`/banner/${id}`),
|
||||||
|
add: (data) => request.post('/banner', data),
|
||||||
|
update: (data) => request.put('/banner', data),
|
||||||
|
delete: (id) => request.delete(`/banner/${id}`),
|
||||||
|
updateSort: (id, sort) => request.put(`/banner/sort/${id}`, null, { params: { sort } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uploadApi = {
|
||||||
|
uploadImage: (file) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
return request.post('/upload/image', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
18
meiruo-frontend/src/components/Footer.vue
Normal file
18
meiruo-frontend/src/components/Footer.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<el-footer class="footer">
|
||||||
|
<p>© 2024 美若彩妆销售平台 版权所有</p>
|
||||||
|
</el-footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.footer {
|
||||||
|
background: #333;
|
||||||
|
color: #999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
160
meiruo-frontend/src/components/Header.vue
Normal file
160
meiruo-frontend/src/components/Header.vue
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<template>
|
||||||
|
<el-header class="header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="logo">
|
||||||
|
<router-link to="/">美若彩妆</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="nav">
|
||||||
|
<router-link to="/" :class="{ active: $route.name === 'Home' }">首页</router-link>
|
||||||
|
<router-link to="/product/1" :class="{ active: $route.name === 'ProductDetail' }">商品</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="search">
|
||||||
|
<el-input v-model="keyword" placeholder="搜索商品" @keyup.enter="handleSearch">
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
<div class="user-actions">
|
||||||
|
<template v-if="isLoggedIn">
|
||||||
|
<el-dropdown>
|
||||||
|
<span class="user-info">
|
||||||
|
<el-avatar :size="32" :src="userInfo.avatar">{{ userInfo.nickname?.charAt(0) }}</el-avatar>
|
||||||
|
<span>{{ userInfo.nickname || userInfo.username }}</span>
|
||||||
|
</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item @click="$router.push('/user')">个人中心</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="$router.push('/order')">我的订单</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="$router.push('/admin')" v-if="isAdmin">管理后台</el-dropdown-item>
|
||||||
|
<el-dropdown-item divided @click="handleLogout">退出登录</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<router-link to="/login">登录</router-link>
|
||||||
|
<router-link to="/register">注册</router-link>
|
||||||
|
</template>
|
||||||
|
<router-link to="/cart" class="cart-icon">
|
||||||
|
<el-badge :value="cartCount" :hidden="cartCount === 0">
|
||||||
|
<el-icon :size="24"><ShoppingCart /></el-icon>
|
||||||
|
</el-badge>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useUserStore } from '../stores/user'
|
||||||
|
import { cartApi } from '../api'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const keyword = ref('')
|
||||||
|
const cartCount = ref(0)
|
||||||
|
|
||||||
|
const isLoggedIn = computed(() => userStore.isLoggedIn)
|
||||||
|
const isAdmin = computed(() => userStore.isAdmin)
|
||||||
|
const userInfo = computed(() => userStore.userInfo)
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
router.push({ name: 'Home', query: { keyword: keyword.value } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await userStore.logout()
|
||||||
|
ElMessage.success('退出成功')
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchCartCount = async () => {
|
||||||
|
if (isLoggedIn.value) {
|
||||||
|
try {
|
||||||
|
const res = await cartApi.getList()
|
||||||
|
if (res.code === 200) {
|
||||||
|
cartCount.value = res.data.length
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchCartCount()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.header {
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
a {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e93b3d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
a {
|
||||||
|
margin: 0 20px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
&:hover,
|
||||||
|
&.active {
|
||||||
|
color: #e93b3d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #333;
|
||||||
|
&:hover {
|
||||||
|
color: #e93b3d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
20
meiruo-frontend/src/main.js
Normal file
20
meiruo-frontend/src/main.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import './style.css'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
app.use(ElementPlus)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
120
meiruo-frontend/src/router/index.js
Normal file
120
meiruo-frontend/src/router/index.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import { useUserStore } from '../stores/user'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: () => import('../views/user/Home.vue'),
|
||||||
|
meta: { title: '首页' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/product/:id',
|
||||||
|
name: 'ProductDetail',
|
||||||
|
component: () => import('../views/user/ProductDetail.vue'),
|
||||||
|
meta: { title: '商品详情' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/cart',
|
||||||
|
name: 'Cart',
|
||||||
|
component: () => import('../views/user/Cart.vue'),
|
||||||
|
meta: { title: '购物车', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/order',
|
||||||
|
name: 'Order',
|
||||||
|
component: () => import('../views/user/Order.vue'),
|
||||||
|
meta: { title: '我的订单', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/checkout',
|
||||||
|
name: 'Checkout',
|
||||||
|
component: () => import('../views/user/Checkout.vue'),
|
||||||
|
meta: { title: '结算', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: () => import('../views/user/Login.vue'),
|
||||||
|
meta: { title: '登录' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/register',
|
||||||
|
name: 'Register',
|
||||||
|
component: () => import('../views/user/Register.vue'),
|
||||||
|
meta: { title: '注册' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/user',
|
||||||
|
name: 'UserCenter',
|
||||||
|
component: () => import('../views/user/UserCenter.vue'),
|
||||||
|
meta: { title: '个人中心', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin',
|
||||||
|
name: 'Admin',
|
||||||
|
component: () => import('../views/admin/Admin.vue'),
|
||||||
|
meta: { title: '管理后台', requiresAuth: true, isAdmin: true },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'dashboard',
|
||||||
|
name: 'Dashboard',
|
||||||
|
component: () => import('../views/admin/Dashboard.vue'),
|
||||||
|
meta: { title: '数据统计' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'product',
|
||||||
|
name: 'ProductManage',
|
||||||
|
component: () => import('../views/admin/ProductManage.vue'),
|
||||||
|
meta: { title: '商品管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'category',
|
||||||
|
name: 'CategoryManage',
|
||||||
|
component: () => import('../views/admin/CategoryManage.vue'),
|
||||||
|
meta: { title: '分类管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'order',
|
||||||
|
name: 'OrderManage',
|
||||||
|
component: () => import('../views/admin/OrderManage.vue'),
|
||||||
|
meta: { title: '订单管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'banner',
|
||||||
|
name: 'BannerManage',
|
||||||
|
component: () => import('../views/admin/BannerManage.vue'),
|
||||||
|
meta: { title: '轮播图管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'user',
|
||||||
|
name: 'UserManage',
|
||||||
|
component: () => import('../views/admin/UserManage.vue'),
|
||||||
|
meta: { title: '用户管理' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
document.title = to.meta.title || '美若彩妆'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const isLoggedIn = userStore.isLoggedIn
|
||||||
|
const isAdmin = userStore.userInfo.role === 1
|
||||||
|
|
||||||
|
if (to.meta.requiresAuth && !isLoggedIn) {
|
||||||
|
next({ name: 'Login', query: { redirect: to.fullPath } })
|
||||||
|
} else if (to.meta.isAdmin && !isAdmin) {
|
||||||
|
next({ name: 'Home' })
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
65
meiruo-frontend/src/stores/user.js
Normal file
65
meiruo-frontend/src/stores/user.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { userApi } from '../api'
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
const token = ref(localStorage.getItem('token') || '')
|
||||||
|
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || '{}'))
|
||||||
|
|
||||||
|
const isLoggedIn = computed(() => !!token.value)
|
||||||
|
const isAdmin = computed(() => userInfo.value.role === 1)
|
||||||
|
|
||||||
|
async function login(username, password) {
|
||||||
|
const res = await userApi.login({ username, password })
|
||||||
|
if (res.code === 200) {
|
||||||
|
token.value = res.data.token || 'token'
|
||||||
|
userInfo.value = res.data
|
||||||
|
localStorage.setItem('token', token.value)
|
||||||
|
localStorage.setItem('userInfo', JSON.stringify(userInfo.value))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function register(data) {
|
||||||
|
const res = await userApi.register(data)
|
||||||
|
return res.code === 200
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
await userApi.logout()
|
||||||
|
token.value = ''
|
||||||
|
userInfo.value = {}
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('userInfo')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getInfo() {
|
||||||
|
try {
|
||||||
|
const res = await userApi.getInfo()
|
||||||
|
if (res.code === 200) {
|
||||||
|
userInfo.value = res.data
|
||||||
|
localStorage.setItem('userInfo', JSON.stringify(userInfo.value))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUserInfo(info) {
|
||||||
|
userInfo.value = { ...userInfo.value, ...info }
|
||||||
|
localStorage.setItem('userInfo', JSON.stringify(userInfo.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
userInfo,
|
||||||
|
isLoggedIn,
|
||||||
|
isAdmin,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
logout,
|
||||||
|
getInfo,
|
||||||
|
updateUserInfo
|
||||||
|
}
|
||||||
|
})
|
||||||
37
meiruo-frontend/src/style.css
Normal file
37
meiruo-frontend/src/style.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-header {
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-main {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-footer {
|
||||||
|
background-color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
119
meiruo-frontend/src/views/admin/Admin.vue
Normal file
119
meiruo-frontend/src/views/admin/Admin.vue
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<div class="admin-layout">
|
||||||
|
<el-container>
|
||||||
|
<el-aside width="200px">
|
||||||
|
<div class="logo">美若彩妆管理后台</div>
|
||||||
|
<el-menu
|
||||||
|
:default-active="activeMenu"
|
||||||
|
router
|
||||||
|
background-color="#304156"
|
||||||
|
text-color="#bfcbd9"
|
||||||
|
active-text-color="#409EFF"
|
||||||
|
>
|
||||||
|
<el-menu-item index="/admin/dashboard">
|
||||||
|
<el-icon><DataAnalysis /></el-icon>
|
||||||
|
<span>数据统计</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/admin/product">
|
||||||
|
<el-icon><Goods /></el-icon>
|
||||||
|
<span>商品管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/admin/category">
|
||||||
|
<el-icon><Grid /></el-icon>
|
||||||
|
<span>分类管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/admin/order">
|
||||||
|
<el-icon><List /></el-icon>
|
||||||
|
<span>订单管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/admin/banner">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
<span>轮播图管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/admin/user">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>用户管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/" divided>
|
||||||
|
<el-icon><HomeFilled /></el-icon>
|
||||||
|
<span>返回前台</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-aside>
|
||||||
|
<el-container>
|
||||||
|
<el-header>
|
||||||
|
<div class="header-right">
|
||||||
|
<span class="admin-name">管理员: {{ userInfo.nickname }}</span>
|
||||||
|
<el-button type="text" @click="handleLogout">退出</el-button>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
<el-main>
|
||||||
|
<router-view />
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useUserStore } from '../../stores/user'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const activeMenu = ref('/admin/dashboard')
|
||||||
|
|
||||||
|
const userInfo = computed(() => userStore.userInfo)
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await userStore.logout()
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
activeMenu.value = router.currentRoute.value.path
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.admin-layout {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-aside {
|
||||||
|
background: #304156;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 60px;
|
||||||
|
line-height: 60px;
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
background: #263445;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-header {
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-name {
|
||||||
|
margin-right: 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-main {
|
||||||
|
background: #f0f2f5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
366
meiruo-frontend/src/views/admin/BannerManage.vue
Normal file
366
meiruo-frontend/src/views/admin/BannerManage.vue
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
<template>
|
||||||
|
<div class="banner-manage">
|
||||||
|
<el-card class="main-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<el-icon class="header-icon"><Picture /></el-icon>
|
||||||
|
<span>轮播图管理</span>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
添加轮播图
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-table :data="bannerList" stripe style="width: 100%" :header-cell-style="{ background: '#f8f9fa' }">
|
||||||
|
<el-table-column label="轮播图" width="220">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="banner-cell">
|
||||||
|
<img :src="row.image || '/images/default.png'" :alt="row.title" class="banner-thumb" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="title" label="标题" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="banner-title">{{ row.title || '暂无标题' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="跳转链接" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link v-if="row.link" type="primary" :underline="false" class="link-text">
|
||||||
|
{{ row.link }}
|
||||||
|
</el-link>
|
||||||
|
<span v-else class="no-link">暂无链接</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="排序" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag type="info" size="small">{{ row.sort }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ row.status === 1 ? '显示' : '隐藏' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="160" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link size="small" @click="handleEdit(row)">
|
||||||
|
<el-icon><Edit /></el-icon>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" link size="small" @click="handleDelete(row)">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px" destroy-on-close>
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px" class="banner-form">
|
||||||
|
<el-form-item label="标题" prop="title">
|
||||||
|
<el-input v-model="form.title" placeholder="请输入轮播图标题(选填)" maxlength="50" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="轮播图" prop="image">
|
||||||
|
<el-upload
|
||||||
|
class="banner-uploader"
|
||||||
|
action="#"
|
||||||
|
:show-file-list="false"
|
||||||
|
:auto-upload="true"
|
||||||
|
:http-request="handleImageUpload"
|
||||||
|
>
|
||||||
|
<img v-if="form.image" :src="form.image" class="upload-image" />
|
||||||
|
<div v-else class="upload-placeholder">
|
||||||
|
<el-icon class="upload-icon"><Plus /></el-icon>
|
||||||
|
<span>上传图片</span>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
<div class="upload-tip">建议尺寸: 1920x450 像素,支持 jpg、png 格式,大小不超过 5MB</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="跳转链接">
|
||||||
|
<el-input v-model="form.link" placeholder="请输入点击后的跳转链接(选填)" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序">
|
||||||
|
<el-input-number v-model="form.sort" :min="0" :max="999" controls-position="right" style="width: 100%" />
|
||||||
|
<span class="form-tip">数字越小越靠前</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="显示状态">
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio :label="1">
|
||||||
|
<el-icon><CircleCheck /></el-icon>
|
||||||
|
显示
|
||||||
|
</el-radio>
|
||||||
|
<el-radio :label="0">
|
||||||
|
<el-icon><CircleClose /></el-icon>
|
||||||
|
隐藏
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||||
|
<el-icon v-if="!submitting"><Check /></el-icon>
|
||||||
|
确定保存
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { bannerApi, uploadApi } from '../../api'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Picture, Plus, Edit, Delete, CircleCheck, CircleClose, Check } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const bannerList = ref([])
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogTitle = ref('添加轮播图')
|
||||||
|
const formRef = ref(null)
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const uploadLoading = ref(false)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
id: null,
|
||||||
|
title: '',
|
||||||
|
image: '',
|
||||||
|
link: '',
|
||||||
|
sort: 0,
|
||||||
|
status: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
image: [{ required: true, message: '请上传轮播图', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchBanners = async () => {
|
||||||
|
try {
|
||||||
|
const res = await bannerApi.getList()
|
||||||
|
if (res.code === 200) {
|
||||||
|
bannerList.value = res.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageUpload = async (options) => {
|
||||||
|
const file = options.file
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
ElMessage.error('图片大小不能超过 5MB')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isImage = file.type === 'image/jpeg' || file.type === 'image/png'
|
||||||
|
if (!isImage) {
|
||||||
|
ElMessage.error('只能上传 JPG/PNG 格式的图片')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await uploadApi.uploadImage(file)
|
||||||
|
if (res.code === 200) {
|
||||||
|
form.image = res.url
|
||||||
|
ElMessage.success('图片上传成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '图片上传失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
ElMessage.error('图片上传失败')
|
||||||
|
} finally {
|
||||||
|
uploadLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.id = null
|
||||||
|
form.title = ''
|
||||||
|
form.image = ''
|
||||||
|
form.link = ''
|
||||||
|
form.sort = 0
|
||||||
|
form.status = 1
|
||||||
|
if (formRef.value) {
|
||||||
|
formRef.value.resetFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
isEdit.value = false
|
||||||
|
dialogTitle.value = '添加轮播图'
|
||||||
|
resetForm()
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
isEdit.value = true
|
||||||
|
dialogTitle.value = '编辑轮播图'
|
||||||
|
Object.assign(form, row)
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定删除该轮播图吗?删除后无法恢复!', '确认删除', {
|
||||||
|
confirmButtonText: '确定删除',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
await bannerApi.delete(row.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchBanners()
|
||||||
|
} catch (e) {
|
||||||
|
if (e !== 'cancel') {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
submitting.value = true
|
||||||
|
if (isEdit.value) {
|
||||||
|
await bannerApi.update(form)
|
||||||
|
} else {
|
||||||
|
await bannerApi.add(form)
|
||||||
|
}
|
||||||
|
ElMessage.success(isEdit.value ? '修改成功' : '添加成功')
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchBanners()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchBanners()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.banner-manage {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
color: #e93b3d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-cell {
|
||||||
|
.banner-thumb {
|
||||||
|
width: 180px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-title {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-text {
|
||||||
|
max-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-link {
|
||||||
|
color: #bbb;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-uploader {
|
||||||
|
width: 360px;
|
||||||
|
|
||||||
|
:deep(.el-upload) {
|
||||||
|
border: 1px dashed #d9d9d9;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 360px;
|
||||||
|
height: 100px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #e93b3d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-image {
|
||||||
|
width: 360px;
|
||||||
|
height: 100px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #8c939d;
|
||||||
|
background: #fafafa;
|
||||||
|
|
||||||
|
.upload-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-tip {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
260
meiruo-frontend/src/views/admin/CategoryManage.vue
Normal file
260
meiruo-frontend/src/views/admin/CategoryManage.vue
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
<template>
|
||||||
|
<div class="category-manage">
|
||||||
|
<el-card class="main-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<el-icon class="header-icon"><Grid /></el-icon>
|
||||||
|
<span>分类管理</span>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
添加分类
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-table :data="categoryList" stripe style="width: 100%" :header-cell-style="{ background: '#f8f9fa' }">
|
||||||
|
<el-table-column label="分类名称" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="category-cell">
|
||||||
|
<div class="category-icon-wrapper">
|
||||||
|
<el-icon><Grid /></el-icon>
|
||||||
|
</div>
|
||||||
|
<span class="category-name">{{ row.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="description" label="分类描述" min-width="250">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="description-text">{{ row.description || '暂无描述' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="排序" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag type="info" size="small">{{ row.sort }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ row.status === 1 ? '正常' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="160" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link size="small" @click="handleEdit(row)">
|
||||||
|
<el-icon><Edit /></el-icon>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" link size="small" @click="handleDelete(row)">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="500px" destroy-on-close>
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px" class="category-form">
|
||||||
|
<el-form-item label="分类名称" prop="name">
|
||||||
|
<el-input v-model="form.name" placeholder="请输入分类名称" maxlength="50" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="分类描述">
|
||||||
|
<el-input v-model="form.description" type="textarea" rows="3" placeholder="请输入分类描述(选填)" maxlength="200" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序">
|
||||||
|
<el-input-number v-model="form.sort" :min="0" :max="999" controls-position="right" style="width: 100%" />
|
||||||
|
<span class="form-tip">数字越小越靠前</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="分类状态">
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio :label="1">
|
||||||
|
<el-icon><CircleCheck /></el-icon>
|
||||||
|
正常
|
||||||
|
</el-radio>
|
||||||
|
<el-radio :label="0">
|
||||||
|
<el-icon><CircleClose /></el-icon>
|
||||||
|
禁用
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||||
|
<el-icon v-if="!submitting"><Check /></el-icon>
|
||||||
|
确定保存
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { categoryApi } from '../../api'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Grid, Plus, Edit, Delete, CircleCheck, CircleClose, Check } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const categoryList = ref([])
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogTitle = ref('添加分类')
|
||||||
|
const formRef = ref(null)
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
sort: 0,
|
||||||
|
status: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchCategories = async () => {
|
||||||
|
try {
|
||||||
|
const res = await categoryApi.getList()
|
||||||
|
if (res.code === 200) {
|
||||||
|
categoryList.value = res.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.id = null
|
||||||
|
form.name = ''
|
||||||
|
form.description = ''
|
||||||
|
form.sort = 0
|
||||||
|
form.status = 1
|
||||||
|
if (formRef.value) {
|
||||||
|
formRef.value.resetFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
isEdit.value = false
|
||||||
|
dialogTitle.value = '添加分类'
|
||||||
|
resetForm()
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
isEdit.value = true
|
||||||
|
dialogTitle.value = '编辑分类'
|
||||||
|
Object.assign(form, row)
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定删除该分类吗?删除后无法恢复!', '确认删除', {
|
||||||
|
confirmButtonText: '确定删除',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
await categoryApi.delete(row.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchCategories()
|
||||||
|
} catch (e) {
|
||||||
|
if (e !== 'cancel') {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
submitting.value = true
|
||||||
|
if (isEdit.value) {
|
||||||
|
await categoryApi.update(form)
|
||||||
|
} else {
|
||||||
|
await categoryApi.add(form)
|
||||||
|
}
|
||||||
|
ElMessage.success(isEdit.value ? '修改成功' : '添加成功')
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchCategories()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchCategories()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.category-manage {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
color: #e93b3d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.category-icon-wrapper {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #fff5f5 0%, #ffe5e5 100%);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #e93b3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
129
meiruo-frontend/src/views/admin/Dashboard.vue
Normal file
129
meiruo-frontend/src/views/admin/Dashboard.vue
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard">
|
||||||
|
<div class="stat-cards">
|
||||||
|
<el-card v-for="stat in stats" :key="stat.title" class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-title">{{ stat.title }}</div>
|
||||||
|
<div class="stat-value">{{ stat.value }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-icon" :style="{ background: stat.color }">
|
||||||
|
<el-icon :size="30"><component :is="stat.icon" /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card class="chart-card">
|
||||||
|
<template #header>
|
||||||
|
<span>本周收入趋势</span>
|
||||||
|
</template>
|
||||||
|
<div class="chart-placeholder">
|
||||||
|
<el-table :data="revenueData" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="date" label="日期" />
|
||||||
|
<el-table-column prop="amount" label="收入金额" />
|
||||||
|
<el-table-column prop="orderCount" label="订单数" />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card class="chart-card">
|
||||||
|
<template #header>
|
||||||
|
<span>热销商品排行</span>
|
||||||
|
</template>
|
||||||
|
<div class="chart-placeholder">
|
||||||
|
<el-table :data="topProducts" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="name" label="商品名称" />
|
||||||
|
<el-table-column prop="totalSales" label="销量" />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { orderApi } from '../../api'
|
||||||
|
|
||||||
|
const stats = ref([
|
||||||
|
{ title: '今日订单', value: 0, icon: 'ShoppingCart', color: '#409EFF' },
|
||||||
|
{ title: '今日收入', value: '¥0', icon: 'Money', color: '#67C23A' },
|
||||||
|
{ title: '商品总数', value: 0, icon: 'Goods', color: '#E6A23C' },
|
||||||
|
{ title: '用户总数', value: 0, icon: 'User', color: '#F56C6C' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const revenueData = ref([])
|
||||||
|
const topProducts = ref([])
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const weekRes = await orderApi.getRevenue('week')
|
||||||
|
if (weekRes.code === 200) {
|
||||||
|
revenueData.value = weekRes.data?.list || []
|
||||||
|
}
|
||||||
|
|
||||||
|
const topRes = await orderApi.getTopProducts(10)
|
||||||
|
if (topRes.code === 200) {
|
||||||
|
topProducts.value = topRes.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.dashboard {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-cards {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
.stat-title {
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-placeholder {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
119
meiruo-frontend/src/views/admin/OrderManage.vue
Normal file
119
meiruo-frontend/src/views/admin/OrderManage.vue
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<div class="order-manage">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>订单管理</span>
|
||||||
|
<el-input
|
||||||
|
v-model="keyword"
|
||||||
|
placeholder="搜索订单号/收货人"
|
||||||
|
style="width: 250px"
|
||||||
|
@keyup.enter="fetchOrders"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="fetchOrders">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-table :data="orderList" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="order_no" label="订单号" width="200" />
|
||||||
|
<el-table-column prop="receiver_name" label="收货人" width="120" />
|
||||||
|
<el-table-column prop="receiver_phone" label="联系电话" width="140" />
|
||||||
|
<el-table-column prop="total_amount" label="订单金额">
|
||||||
|
<template #default="{ row }">
|
||||||
|
¥{{ parseFloat(row.total_amount).toFixed(2) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusType(row.status)">
|
||||||
|
{{ getStatusText(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="create_time" label="下单时间" width="180" />
|
||||||
|
<el-table-column label="操作" width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" size="small" @click="viewDetail(row)">查看</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="row.status === 1"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
@click="updateStatus(row.id, 2)"
|
||||||
|
>
|
||||||
|
发货
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { orderApi } from '../../api'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const orderList = ref([])
|
||||||
|
const keyword = ref('')
|
||||||
|
|
||||||
|
const statusMap = {
|
||||||
|
1: '待付款',
|
||||||
|
2: '已付款',
|
||||||
|
3: '已发货',
|
||||||
|
4: '已完成',
|
||||||
|
5: '已取消'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status) => statusMap[status] || '未知'
|
||||||
|
const getStatusType = (status) => {
|
||||||
|
const types = { 1: 'warning', 2: 'primary', 3: 'info', 4: 'success', 5: 'danger' }
|
||||||
|
return types[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchOrders = async () => {
|
||||||
|
try {
|
||||||
|
const res = await orderApi.adminList({ keyword: keyword.value })
|
||||||
|
if (res.code === 200) {
|
||||||
|
orderList.value = res.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewDetail = (order) => {
|
||||||
|
console.log('查看订单详情', order)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateStatus = async (id, status) => {
|
||||||
|
try {
|
||||||
|
await orderApi.updateStatus(id, status)
|
||||||
|
ElMessage.success('操作成功')
|
||||||
|
fetchOrders()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchOrders()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.order-manage {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
442
meiruo-frontend/src/views/admin/ProductManage.vue
Normal file
442
meiruo-frontend/src/views/admin/ProductManage.vue
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
<template>
|
||||||
|
<div class="product-manage">
|
||||||
|
<el-card class="main-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<el-icon class="header-icon"><Goods /></el-icon>
|
||||||
|
<span>商品管理</span>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
添加商品
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-table :data="productList" stripe style="width: 100%" :header-cell-style="{ background: '#f8f9fa' }">
|
||||||
|
<el-table-column label="商品信息" min-width="280">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="product-cell">
|
||||||
|
<img :src="row.image || '/images/default.png'" :alt="row.name" class="product-thumb" />
|
||||||
|
<div class="product-info">
|
||||||
|
<span class="product-name">{{ row.name }}</span>
|
||||||
|
<span class="product-desc">{{ row.description }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="分类" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag type="info" size="small">{{ getCategoryName(row.categoryId) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="价格" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="price-text">¥{{ row.price.toFixed(2) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="库存" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="{ 'low-stock': row.stock < 10 }">{{ row.stock }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="销量" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ row.sales }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ row.status === 1 ? '上架' : '下架' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="160" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link size="small" @click="handleEdit(row)">
|
||||||
|
<el-icon><Edit /></el-icon>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" link size="small" @click="handleDelete(row)">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="680px" destroy-on-close>
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px" class="product-form">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="商品名称" prop="name">
|
||||||
|
<el-input v-model="form.name" placeholder="请输入商品名称" maxlength="100" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="商品描述" prop="description">
|
||||||
|
<el-input v-model="form.description" type="textarea" rows="3" placeholder="请输入商品描述" maxlength="500" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="商品价格" prop="price">
|
||||||
|
<el-input-number v-model="form.price" :precision="2" :min="0" :max="999999" controls-position="right" style="width: 100%" />
|
||||||
|
<span class="form-tip">元</span>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="商品库存" prop="stock">
|
||||||
|
<el-input-number v-model="form.stock" :min="0" :max="999999" controls-position="right" style="width: 100%" />
|
||||||
|
<span class="form-tip">件</span>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="商品分类" prop="categoryId">
|
||||||
|
<el-select v-model="form.categoryId" placeholder="请选择分类" style="width: 100%">
|
||||||
|
<el-option v-for="cat in categories" :key="cat.id" :label="cat.name" :value="cat.id">
|
||||||
|
<span>{{ cat.name }}</span>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="商品状态" prop="status">
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio :label="1">
|
||||||
|
<el-icon><CircleCheck /></el-icon>
|
||||||
|
上架
|
||||||
|
</el-radio>
|
||||||
|
<el-radio :label="0">
|
||||||
|
<el-icon><CircleClose /></el-icon>
|
||||||
|
下架
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="商品图片" prop="image">
|
||||||
|
<el-upload
|
||||||
|
class="image-uploader"
|
||||||
|
action="#"
|
||||||
|
:show-file-list="false"
|
||||||
|
:auto-upload="true"
|
||||||
|
:http-request="handleImageUpload"
|
||||||
|
>
|
||||||
|
<img v-if="form.image" :src="form.image" class="upload-image" />
|
||||||
|
<div v-else class="upload-placeholder">
|
||||||
|
<el-icon class="upload-icon"><Plus /></el-icon>
|
||||||
|
<span>上传图片</span>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
<div class="upload-tip">支持 jpg、png 格式,大小不超过 2MB</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||||
|
<el-icon v-if="!submitting"><Check /></el-icon>
|
||||||
|
确定保存
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { productApi, categoryApi, uploadApi } from '../../api'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Goods, Plus, Edit, Delete, CircleCheck, CircleClose, Check } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const productList = ref([])
|
||||||
|
const categories = ref([])
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogTitle = ref('添加商品')
|
||||||
|
const formRef = ref(null)
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const uploadLoading = ref(false)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
price: 0,
|
||||||
|
stock: 0,
|
||||||
|
categoryId: null,
|
||||||
|
image: '',
|
||||||
|
status: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [{ required: true, message: '请输入商品名称', trigger: 'blur' }],
|
||||||
|
price: [{ required: true, message: '请输入商品价格', trigger: 'blur' }],
|
||||||
|
stock: [{ required: true, message: '请输入商品库存', trigger: 'blur' }],
|
||||||
|
categoryId: [{ required: true, message: '请选择商品分类', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCategoryName = (categoryId) => {
|
||||||
|
const cat = categories.value.find(c => c.id === categoryId)
|
||||||
|
return cat ? cat.name : '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchProducts = async () => {
|
||||||
|
try {
|
||||||
|
const res = await productApi.getList()
|
||||||
|
if (res.code === 200) {
|
||||||
|
productList.value = res.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchCategories = async () => {
|
||||||
|
try {
|
||||||
|
const res = await categoryApi.getList()
|
||||||
|
if (res.code === 200) {
|
||||||
|
categories.value = res.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageUpload = async (options) => {
|
||||||
|
const file = options.file
|
||||||
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
|
ElMessage.error('图片大小不能超过 2MB')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isImage = file.type === 'image/jpeg' || file.type === 'image/png'
|
||||||
|
if (!isImage) {
|
||||||
|
ElMessage.error('只能上传 JPG/PNG 格式的图片')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await uploadApi.uploadImage(file)
|
||||||
|
if (res.code === 200) {
|
||||||
|
form.image = res.url
|
||||||
|
ElMessage.success('图片上传成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '图片上传失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
ElMessage.error('图片上传失败')
|
||||||
|
} finally {
|
||||||
|
uploadLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.id = null
|
||||||
|
form.name = ''
|
||||||
|
form.description = ''
|
||||||
|
form.price = 0
|
||||||
|
form.stock = 0
|
||||||
|
form.categoryId = null
|
||||||
|
form.image = ''
|
||||||
|
form.status = 1
|
||||||
|
if (formRef.value) {
|
||||||
|
formRef.value.resetFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
isEdit.value = false
|
||||||
|
dialogTitle.value = '添加商品'
|
||||||
|
resetForm()
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
isEdit.value = true
|
||||||
|
dialogTitle.value = '编辑商品'
|
||||||
|
Object.assign(form, row)
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定删除该商品吗?删除后无法恢复!', '确认删除', {
|
||||||
|
confirmButtonText: '确定删除',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
await productApi.delete(row.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchProducts()
|
||||||
|
} catch (e) {
|
||||||
|
if (e !== 'cancel') {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
submitting.value = true
|
||||||
|
if (isEdit.value) {
|
||||||
|
await productApi.update(form)
|
||||||
|
} else {
|
||||||
|
await productApi.add(form)
|
||||||
|
}
|
||||||
|
ElMessage.success(isEdit.value ? '修改成功' : '添加成功')
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchProducts()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchProducts()
|
||||||
|
fetchCategories()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.product-manage {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
color: #e93b3d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.product-thumb {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
max-width: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
max-width: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-text {
|
||||||
|
color: #e93b3d;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.low-stock {
|
||||||
|
color: #f56c6c;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-uploader {
|
||||||
|
width: 120px;
|
||||||
|
|
||||||
|
:deep(.el-upload) {
|
||||||
|
border: 1px dashed #d9d9d9;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #e93b3d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-image {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #8c939d;
|
||||||
|
|
||||||
|
.upload-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-tip {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
110
meiruo-frontend/src/views/admin/UserManage.vue
Normal file
110
meiruo-frontend/src/views/admin/UserManage.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-manage">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>用户管理</span>
|
||||||
|
<el-input
|
||||||
|
v-model="keyword"
|
||||||
|
placeholder="搜索用户名/昵称/手机号"
|
||||||
|
style="width: 250px"
|
||||||
|
@keyup.enter="fetchUsers"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="fetchUsers">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-table :data="userList" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="username" label="用户名" width="150" />
|
||||||
|
<el-table-column prop="nickname" label="昵称" width="150" />
|
||||||
|
<el-table-column prop="phone" label="手机号" width="150" />
|
||||||
|
<el-table-column prop="role" label="角色" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.role === 1 ? 'danger' : 'primary'">
|
||||||
|
{{ row.role === 1 ? '管理员' : '普通用户' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-switch
|
||||||
|
:model-value="row.status === 1"
|
||||||
|
@change="updateStatus(row)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="create_time" label="注册时间" width="180" />
|
||||||
|
<el-table-column label="操作" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { userApi } from '../../api'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
|
const userList = ref([])
|
||||||
|
const keyword = ref('')
|
||||||
|
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
try {
|
||||||
|
const res = await userApi.getList(keyword.value)
|
||||||
|
if (res.code === 200) {
|
||||||
|
userList.value = res.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateStatus = async (user) => {
|
||||||
|
try {
|
||||||
|
const newStatus = user.status === 1 ? 0 : 1
|
||||||
|
await userApi.updateStatus(user.id, newStatus)
|
||||||
|
user.status = newStatus
|
||||||
|
ElMessage.success('操作成功')
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (user) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定删除该用户吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
|
||||||
|
await userApi.delete(user.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchUsers()
|
||||||
|
} catch (e) {
|
||||||
|
if (e !== 'cancel') {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchUsers()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.user-manage {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
152
meiruo-frontend/src/views/user/Cart.vue
Normal file
152
meiruo-frontend/src/views/user/Cart.vue
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<template>
|
||||||
|
<div class="cart-page">
|
||||||
|
<el-container>
|
||||||
|
<el-header>
|
||||||
|
<Header />
|
||||||
|
</el-header>
|
||||||
|
<el-main>
|
||||||
|
<div class="cart-container">
|
||||||
|
<h2>购物车</h2>
|
||||||
|
<div class="cart-list" v-if="cartList.length > 0">
|
||||||
|
<el-table :data="cartList" stripe style="width: 100%">
|
||||||
|
<el-table-column width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-image :src="row.product_image || '/images/default.png'" fit="cover" style="width: 100px; height: 100px" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="product_name" label="商品名称" />
|
||||||
|
<el-table-column label="单价">
|
||||||
|
<template #default="{ row }">
|
||||||
|
¥{{ parseFloat(row.product_price).toFixed(2) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="数量" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input-number v-model="row.quantity" :min="1" @change="updateQuantity(row.id, row.quantity)" size="small" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="小计">
|
||||||
|
<template #default="{ row }">
|
||||||
|
¥{{ (parseFloat(row.product_price) * row.quantity).toFixed(2) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="danger" size="small" @click="deleteItem(row.id)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="cart-footer">
|
||||||
|
<div class="total">
|
||||||
|
总计: <span>¥{{ totalPrice.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" size="large" @click="goToCheckout">去结算</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else description="购物车空空如也" />
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
<el-footer>
|
||||||
|
<Footer />
|
||||||
|
</el-footer>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import Header from '../../components/Header.vue'
|
||||||
|
import Footer from '../../components/Footer.vue'
|
||||||
|
import { cartApi } from '../../api'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const cartList = ref([])
|
||||||
|
|
||||||
|
const totalPrice = computed(() => {
|
||||||
|
return cartList.value.reduce((sum, item) => {
|
||||||
|
return sum + parseFloat(item.product_price) * item.quantity
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchCartList = async () => {
|
||||||
|
try {
|
||||||
|
const res = await cartApi.getList()
|
||||||
|
if (res.code === 200) {
|
||||||
|
cartList.value = res.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateQuantity = async (id, quantity) => {
|
||||||
|
try {
|
||||||
|
await cartApi.updateQuantity(id, quantity)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteItem = async (id) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定删除该商品吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
|
||||||
|
await cartApi.delete(id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchCartList()
|
||||||
|
} catch (e) {
|
||||||
|
if (e !== 'cancel') {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToCheckout = () => {
|
||||||
|
if (cartList.value.length === 0) {
|
||||||
|
ElMessage.warning('请先添加商品到购物车')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const cartIds = cartList.value.map(item => item.id)
|
||||||
|
router.push({ path: '/checkout', query: { cartIds: JSON.stringify(cartIds) } })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchCartList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.cart-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 20px auto;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
|
||||||
|
.total {
|
||||||
|
font-size: 18px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #e93b3d;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
235
meiruo-frontend/src/views/user/Checkout.vue
Normal file
235
meiruo-frontend/src/views/user/Checkout.vue
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
<template>
|
||||||
|
<div class="checkout-page">
|
||||||
|
<el-container>
|
||||||
|
<el-header>
|
||||||
|
<Header />
|
||||||
|
</el-header>
|
||||||
|
<el-main>
|
||||||
|
<div class="checkout-container">
|
||||||
|
<h2>订单结算</h2>
|
||||||
|
<div class="checkout-content">
|
||||||
|
<div class="address-section">
|
||||||
|
<h3>收货信息</h3>
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||||
|
<el-form-item label="收货人" prop="receiverName">
|
||||||
|
<el-input v-model="form.receiverName" placeholder="请输入收货人姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="联系电话" prop="receiverPhone">
|
||||||
|
<el-input v-model="form.receiverPhone" placeholder="请输入联系电话" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="收货地址" prop="receiverAddress">
|
||||||
|
<el-input v-model="form.receiverAddress" placeholder="请输入详细地址" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.remark" type="textarea" placeholder="选填,请输入备注信息" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<div class="order-section">
|
||||||
|
<h3>订单商品</h3>
|
||||||
|
<div class="product-list">
|
||||||
|
<div class="product-item" v-for="item in orderItems" :key="item.id">
|
||||||
|
<img :src="item.product_image || '/images/default.png'" alt="" />
|
||||||
|
<div class="info">
|
||||||
|
<h4>{{ item.product_name }}</h4>
|
||||||
|
<p>¥{{ parseFloat(item.product_price).toFixed(2) }} x {{ item.quantity }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="subtotal">¥{{ (parseFloat(item.product_price) * item.quantity).toFixed(2) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="order-total">
|
||||||
|
<span>订单总额:</span>
|
||||||
|
<span class="amount">¥{{ totalAmount.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="submit-order">
|
||||||
|
<el-button type="primary" size="large" @click="handleSubmit" :loading="submitting">提交订单</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
<el-footer>
|
||||||
|
<Footer />
|
||||||
|
</el-footer>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import Header from '../../components/Header.vue'
|
||||||
|
import Footer from '../../components/Footer.vue'
|
||||||
|
import { cartApi, orderApi, productApi } from '../../api'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const formRef = ref(null)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const orderItems = ref([])
|
||||||
|
const form = ref({
|
||||||
|
receiverName: '',
|
||||||
|
receiverPhone: '',
|
||||||
|
receiverAddress: '',
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
receiverName: [{ required: true, message: '请输入收货人姓名', trigger: 'blur' }],
|
||||||
|
receiverPhone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
|
||||||
|
receiverAddress: [{ required: true, message: '请输入收货地址', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalAmount = computed(() => {
|
||||||
|
return orderItems.value.reduce((sum, item) => {
|
||||||
|
return sum + parseFloat(item.product_price) * item.quantity
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const cartIds = JSON.parse(router.currentRoute.value.query.cartIds || '[]')
|
||||||
|
const productId = router.currentRoute.value.query.productId
|
||||||
|
|
||||||
|
if (productId) {
|
||||||
|
const res = await productApi.getById(productId)
|
||||||
|
if (res.code === 200) {
|
||||||
|
orderItems.value = [{
|
||||||
|
id: 0,
|
||||||
|
product_id: res.data.id,
|
||||||
|
product_name: res.data.name,
|
||||||
|
product_image: res.data.image,
|
||||||
|
product_price: res.data.price,
|
||||||
|
quantity: parseInt(router.currentRoute.value.query.quantity) || 1
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
} else if (cartIds.length > 0) {
|
||||||
|
const res = await cartApi.getList()
|
||||||
|
if (res.code === 200) {
|
||||||
|
orderItems.value = res.data.filter(item => cartIds.includes(item.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
submitting.value = true
|
||||||
|
|
||||||
|
const cartIds = orderItems.value.filter(item => item.id > 0).map(item => item.id)
|
||||||
|
const res = await orderApi.create({
|
||||||
|
cartIds,
|
||||||
|
...form.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('下单成功')
|
||||||
|
router.push('/order')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.checkout-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 20px auto;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-section {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-section {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-list {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtotal {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e93b3d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-total {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #e93b3d;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-order {
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
437
meiruo-frontend/src/views/user/Home.vue
Normal file
437
meiruo-frontend/src/views/user/Home.vue
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home">
|
||||||
|
<el-container>
|
||||||
|
<el-header>
|
||||||
|
<Header />
|
||||||
|
</el-header>
|
||||||
|
<el-main>
|
||||||
|
<div class="banner-section">
|
||||||
|
<el-carousel :interval="5000" height="450px" indicator-position="outside" trigger="click">
|
||||||
|
<el-carousel-item v-for="banner in banners" :key="banner.id">
|
||||||
|
<div class="banner-item" @click="handleBannerClick(banner)">
|
||||||
|
<img :src="banner.image || '/images/default.png'" :alt="banner.title" class="banner-img" />
|
||||||
|
<div class="banner-overlay">
|
||||||
|
<h3>{{ banner.title }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-carousel-item>
|
||||||
|
</el-carousel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="category-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">
|
||||||
|
<el-icon><Grid /></el-icon>
|
||||||
|
商品分类
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="category-grid">
|
||||||
|
<div
|
||||||
|
class="category-card"
|
||||||
|
:class="{ active: activeCategory === null }"
|
||||||
|
@click="selectCategory(null)"
|
||||||
|
>
|
||||||
|
<div class="category-icon">
|
||||||
|
<el-icon><Grid /></el-icon>
|
||||||
|
</div>
|
||||||
|
<span>全部</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="cat in categories"
|
||||||
|
:key="cat.id"
|
||||||
|
class="category-card"
|
||||||
|
:class="{ active: activeCategory === cat.id }"
|
||||||
|
@click="selectCategory(cat.id)"
|
||||||
|
>
|
||||||
|
<div class="category-icon">
|
||||||
|
<el-icon><Goods /></el-icon>
|
||||||
|
</div>
|
||||||
|
<span>{{ cat.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">
|
||||||
|
<el-icon><ShoppingBag /></el-icon>
|
||||||
|
{{ activeCategory ? categories.find(c => c.id === activeCategory)?.name : '全部商品' }}
|
||||||
|
</h2>
|
||||||
|
<div class="product-count">共 {{ productList.length }} 件商品</div>
|
||||||
|
</div>
|
||||||
|
<el-row :gutter="24" v-if="productList.length > 0">
|
||||||
|
<el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="product in productList" :key="product.id">
|
||||||
|
<div class="product-card" @click="goToDetail(product.id)">
|
||||||
|
<div class="product-image-wrapper">
|
||||||
|
<img :src="product.image || '/images/default.png'" :alt="product.name" class="product-img" />
|
||||||
|
<div class="product-badge" v-if="product.sales > 50">热销</div>
|
||||||
|
</div>
|
||||||
|
<div class="product-info">
|
||||||
|
<h4 class="product-name">{{ product.name }}</h4>
|
||||||
|
<p class="product-desc">{{ product.description }}</p>
|
||||||
|
<div class="product-bottom">
|
||||||
|
<div class="product-price">
|
||||||
|
<span class="price-symbol">¥</span>
|
||||||
|
<span class="price-value">{{ product.price.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="product-sales">已售 {{ product.sales }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<div class="empty-state" v-else>
|
||||||
|
<el-empty description="暂无该分类商品">
|
||||||
|
<el-button type="primary" @click="selectCategory(null)">查看全部商品</el-button>
|
||||||
|
</el-empty>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
<el-footer class="home-footer">
|
||||||
|
<Footer />
|
||||||
|
</el-footer>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import Header from '../../components/Header.vue'
|
||||||
|
import Footer from '../../components/Footer.vue'
|
||||||
|
import { productApi, categoryApi, bannerApi } from '../../api'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const banners = ref([])
|
||||||
|
const categories = ref([])
|
||||||
|
const productList = ref([])
|
||||||
|
const activeCategory = ref(null)
|
||||||
|
|
||||||
|
const fetchBanners = async () => {
|
||||||
|
try {
|
||||||
|
const res = await bannerApi.getList(1)
|
||||||
|
if (res.code === 200) {
|
||||||
|
banners.value = res.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchCategories = async () => {
|
||||||
|
try {
|
||||||
|
const res = await categoryApi.getList(1)
|
||||||
|
if (res.code === 200) {
|
||||||
|
categories.value = res.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchProducts = async () => {
|
||||||
|
try {
|
||||||
|
const res = await productApi.getList({
|
||||||
|
categoryId: activeCategory.value,
|
||||||
|
keyword: route.query.keyword
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
productList.value = res.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectCategory = (id) => {
|
||||||
|
activeCategory.value = id
|
||||||
|
fetchProducts()
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToDetail = (id) => {
|
||||||
|
router.push(`/product/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBannerClick = (banner) => {
|
||||||
|
if (banner.link) {
|
||||||
|
router.push(banner.link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => route.query.keyword, () => {
|
||||||
|
fetchProducts()
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchBanners()
|
||||||
|
fetchCategories()
|
||||||
|
fetchProducts()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.home {
|
||||||
|
background: linear-gradient(180deg, #f8f5f3 0%, #ffffff 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-section {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 20px auto 0;
|
||||||
|
padding: 0 20px;
|
||||||
|
|
||||||
|
.banner-item {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.banner-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .banner-img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 40px;
|
||||||
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.6));
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-carousel__item--card) {
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-carousel__button) {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(233, 59, 61, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-carousel__button.is-active) {
|
||||||
|
background: #e93b3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-section {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 40px auto 0;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
color: #e93b3d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-count {
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-grid {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px 32px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: #e93b3d;
|
||||||
|
background: linear-gradient(135deg, #fff5f5 0%, #fff 100%);
|
||||||
|
|
||||||
|
.category-icon {
|
||||||
|
background: linear-gradient(135deg, #e93b3d 0%, #ff6b6b 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: #e93b3d;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-icon {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f8f5f3;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #666;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-section {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
|
.product-img {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 220px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.product-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 16px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: linear-gradient(135deg, #e93b3d 0%, #ff6b6b 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-bottom {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-price {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
.price-symbol {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #e93b3d;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #e93b3d;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sales {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 60px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-footer {
|
||||||
|
background: #2c2c2c;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
100
meiruo-frontend/src/views/user/Login.vue
Normal file
100
meiruo-frontend/src/views/user/Login.vue
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-page">
|
||||||
|
<div class="login-container">
|
||||||
|
<h2>用户登录</h2>
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="0">
|
||||||
|
<el-form-item prop="username">
|
||||||
|
<el-input v-model="form.username" placeholder="用户名" prefix-icon="User" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input v-model="form.password" type="password" placeholder="密码" prefix-icon="Lock" @keyup.enter="handleLogin" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleLogin" :loading="loading" style="width: 100%">登录</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div class="login-footer">
|
||||||
|
<router-link to="/register">还没有账号?去注册</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useUserStore } from '../../stores/user'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const formRef = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const form = reactive({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||||
|
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
const success = await userStore.login(form.username, form.password)
|
||||||
|
if (success) {
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
const redirect = router.currentRoute.value.query.redirect || '/'
|
||||||
|
router.push(redirect)
|
||||||
|
} else {
|
||||||
|
ElMessage.error('用户名或密码错误')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
width: 400px;
|
||||||
|
padding: 40px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #667eea;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
178
meiruo-frontend/src/views/user/Order.vue
Normal file
178
meiruo-frontend/src/views/user/Order.vue
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<template>
|
||||||
|
<div class="order-page">
|
||||||
|
<el-container>
|
||||||
|
<el-header>
|
||||||
|
<Header />
|
||||||
|
</el-header>
|
||||||
|
<el-main>
|
||||||
|
<div class="order-container">
|
||||||
|
<h2>我的订单</h2>
|
||||||
|
<el-tabs v-model="activeTab" @tab-change="fetchOrders">
|
||||||
|
<el-tab-pane label="全部" name="all" />
|
||||||
|
<el-tab-pane label="待付款" name="1" />
|
||||||
|
<el-tab-pane label="已付款" name="2" />
|
||||||
|
<el-tab-pane label="已发货" name="3" />
|
||||||
|
<el-tab-pane label="已完成" name="4" />
|
||||||
|
</el-tabs>
|
||||||
|
<div class="order-list" v-if="orderList.length > 0">
|
||||||
|
<div class="order-item" v-for="order in orderList" :key="order.id">
|
||||||
|
<div class="order-header">
|
||||||
|
<span class="order-no">订单号: {{ order.order_no }}</span>
|
||||||
|
<span class="order-status">{{ getStatusText(order.status) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="order-body">
|
||||||
|
<div class="order-info">
|
||||||
|
<p>收货人: {{ order.receiver_name }}</p>
|
||||||
|
<p>联系电话: {{ order.receiver_phone }}</p>
|
||||||
|
<p>收货地址: {{ order.receiver_address }}</p>
|
||||||
|
<p>下单时间: {{ order.create_time }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="order-total">
|
||||||
|
<span>订单总额:</span>
|
||||||
|
<span class="amount">¥{{ parseFloat(order.total_amount).toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="order-footer">
|
||||||
|
<el-button size="small" @click="viewDetail(order)">查看详情</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="order.status === 1"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="payOrder(order)"
|
||||||
|
>
|
||||||
|
去付款
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else description="暂无订单" />
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
<el-footer>
|
||||||
|
<Footer />
|
||||||
|
</el-footer>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import Header from '../../components/Header.vue'
|
||||||
|
import Footer from '../../components/Footer.vue'
|
||||||
|
import { orderApi } from '../../api'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const activeTab = ref('all')
|
||||||
|
const orderList = ref([])
|
||||||
|
|
||||||
|
const statusMap = {
|
||||||
|
1: '待付款',
|
||||||
|
2: '已付款',
|
||||||
|
3: '已发货',
|
||||||
|
4: '已完成',
|
||||||
|
5: '已取消'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
return statusMap[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchOrders = async () => {
|
||||||
|
try {
|
||||||
|
const status = activeTab.value === 'all' ? null : parseInt(activeTab.value)
|
||||||
|
const res = await orderApi.getList(status)
|
||||||
|
if (res.code === 200) {
|
||||||
|
orderList.value = res.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewDetail = (order) => {
|
||||||
|
console.log('查看订单详情', order)
|
||||||
|
}
|
||||||
|
|
||||||
|
const payOrder = async (order) => {
|
||||||
|
try {
|
||||||
|
await orderApi.updateStatus(order.id, 2)
|
||||||
|
ElMessage.success('付款成功')
|
||||||
|
fetchOrders()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchOrders()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.order-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 20px auto;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-item {
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
|
.order-no {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status {
|
||||||
|
color: #e93b3d;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-info {
|
||||||
|
p {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-total {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #e93b3d;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-footer {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: right;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
157
meiruo-frontend/src/views/user/ProductDetail.vue
Normal file
157
meiruo-frontend/src/views/user/ProductDetail.vue
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<template>
|
||||||
|
<div class="product-detail">
|
||||||
|
<el-container>
|
||||||
|
<el-header>
|
||||||
|
<Header />
|
||||||
|
</el-header>
|
||||||
|
<el-main>
|
||||||
|
<div class="detail-container" v-if="product">
|
||||||
|
<div class="product-gallery">
|
||||||
|
<el-image :src="product.image || '/images/default.png'" :preview-src-list="[product.image]" fit="cover" />
|
||||||
|
</div>
|
||||||
|
<div class="product-info">
|
||||||
|
<h1>{{ product.name }}</h1>
|
||||||
|
<p class="description">{{ product.description }}</p>
|
||||||
|
<div class="price-section">
|
||||||
|
<span class="price">¥{{ product.price.toFixed(2) }}</span>
|
||||||
|
<span class="stock">库存: {{ product.stock }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="quantity-section">
|
||||||
|
<span>数量:</span>
|
||||||
|
<el-input-number v-model="quantity" :min="1" :max="product.stock" size="large" />
|
||||||
|
</div>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<el-button type="primary" size="large" @click="handleAddCart">加入购物车</el-button>
|
||||||
|
<el-button type="warning" size="large" @click="handleBuyNow">立即购买</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else description="商品不存在" />
|
||||||
|
</el-main>
|
||||||
|
<el-footer>
|
||||||
|
<Footer />
|
||||||
|
</el-footer>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useUserStore } from '../../stores/user'
|
||||||
|
import Header from '../../components/Header.vue'
|
||||||
|
import Footer from '../../components/Footer.vue'
|
||||||
|
import { productApi, cartApi } from '../../api'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const product = ref(null)
|
||||||
|
const quantity = ref(1)
|
||||||
|
|
||||||
|
const fetchProduct = async () => {
|
||||||
|
try {
|
||||||
|
const res = await productApi.getById(route.params.id)
|
||||||
|
if (res.code === 200) {
|
||||||
|
product.value = res.data
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddCart = async () => {
|
||||||
|
if (!userStore.isLoggedIn) {
|
||||||
|
router.push('/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await cartApi.add(product.value.id, quantity.value)
|
||||||
|
ElMessage.success('添加成功')
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBuyNow = async () => {
|
||||||
|
if (!userStore.isLoggedIn) {
|
||||||
|
router.push('/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router.push({
|
||||||
|
path: '/checkout',
|
||||||
|
query: { productId: product.value.id, quantity: quantity.value }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchProduct()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.detail-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 20px auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 40px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-gallery {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 400px;
|
||||||
|
|
||||||
|
.el-image {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.price {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #e93b3d;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
127
meiruo-frontend/src/views/user/Register.vue
Normal file
127
meiruo-frontend/src/views/user/Register.vue
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<template>
|
||||||
|
<div class="register-page">
|
||||||
|
<div class="register-container">
|
||||||
|
<h2>用户注册</h2>
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="0">
|
||||||
|
<el-form-item prop="username">
|
||||||
|
<el-input v-model="form.username" placeholder="用户名" prefix-icon="User" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input v-model="form.password" type="password" placeholder="密码" prefix-icon="Lock" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="confirmPassword">
|
||||||
|
<el-input v-model="form.confirmPassword" type="password" placeholder="确认密码" prefix-icon="Lock" @keyup.enter="handleRegister" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="nickname">
|
||||||
|
<el-input v-model="form.nickname" placeholder="昵称" prefix-icon="UserFilled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="phone">
|
||||||
|
<el-input v-model="form.phone" placeholder="手机号" prefix-icon="Phone" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleRegister" :loading="loading" style="width: 100%">注册</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div class="register-footer">
|
||||||
|
<router-link to="/login">已有账号?去登录</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useUserStore } from '../../stores/user'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const formRef = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const form = reactive({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
nickname: '',
|
||||||
|
phone: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const validateConfirmPassword = (rule, value, callback) => {
|
||||||
|
if (value !== form.password) {
|
||||||
|
callback(new Error('两次密码不一致'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||||
|
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||||
|
confirmPassword: [{ required: true, validator: validateConfirmPassword, trigger: 'blur' }],
|
||||||
|
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRegister = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
const success = await userStore.register({
|
||||||
|
username: form.username,
|
||||||
|
password: form.password,
|
||||||
|
nickname: form.nickname,
|
||||||
|
phone: form.phone
|
||||||
|
})
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
ElMessage.success('注册成功,请登录')
|
||||||
|
router.push('/login')
|
||||||
|
} else {
|
||||||
|
ElMessage.error('注册失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.register-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-container {
|
||||||
|
width: 400px;
|
||||||
|
padding: 40px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #667eea;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
161
meiruo-frontend/src/views/user/UserCenter.vue
Normal file
161
meiruo-frontend/src/views/user/UserCenter.vue
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-center">
|
||||||
|
<el-container>
|
||||||
|
<el-header>
|
||||||
|
<Header />
|
||||||
|
</el-header>
|
||||||
|
<el-main>
|
||||||
|
<div class="user-container">
|
||||||
|
<div class="user-sidebar">
|
||||||
|
<div class="user-info">
|
||||||
|
<el-avatar :size="80">{{ userInfo.nickname?.charAt(0) || userInfo.username?.charAt(0) }}</el-avatar>
|
||||||
|
<p>{{ userInfo.nickname || userInfo.username }}</p>
|
||||||
|
</div>
|
||||||
|
<el-menu :default-active="activeMenu" @select="handleMenuSelect">
|
||||||
|
<el-menu-item index="info">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>个人信息</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="orders">
|
||||||
|
<el-icon><List /></el-icon>
|
||||||
|
<span>我的订单</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</div>
|
||||||
|
<div class="user-content">
|
||||||
|
<div v-if="activeMenu === 'info'" class="info-section">
|
||||||
|
<h3>个人信息</h3>
|
||||||
|
<el-form :model="userForm" :rules="formRules" ref="formRef" label-width="100px">
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input v-model="userForm.username" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="昵称" prop="nickname">
|
||||||
|
<el-input v-model="userForm.nickname" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input v-model="userForm.phone" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input v-model="userForm.email" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSave">保存修改</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<div v-if="activeMenu === 'orders'" class="orders-section">
|
||||||
|
<h3>我的订单</h3>
|
||||||
|
<Order />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
<el-footer>
|
||||||
|
<Footer />
|
||||||
|
</el-footer>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import Header from '../../components/Header.vue'
|
||||||
|
import Footer from '../../components/Footer.vue'
|
||||||
|
import Order from './Order.vue'
|
||||||
|
import { useUserStore } from '../../stores/user'
|
||||||
|
import { userApi } from '../../api'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const formRef = ref(null)
|
||||||
|
const activeMenu = ref('info')
|
||||||
|
const userForm = reactive({
|
||||||
|
username: '',
|
||||||
|
nickname: '',
|
||||||
|
phone: '',
|
||||||
|
email: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const userInfo = computed(() => userStore.userInfo)
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
nickname: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
|
||||||
|
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMenuSelect = (index) => {
|
||||||
|
activeMenu.value = index
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
await userApi.updateInfo(userForm)
|
||||||
|
userStore.updateUserInfo(userForm)
|
||||||
|
ElMessage.success('保存成功')
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
userForm.username = userInfo.value.username
|
||||||
|
userForm.nickname = userInfo.value.nickname
|
||||||
|
userForm.phone = userInfo.value.phone
|
||||||
|
userForm.email = userInfo.value.email
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.user-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 20px auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-sidebar {
|
||||||
|
width: 250px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
text-align: center;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.el-avatar {
|
||||||
|
background: #e93b3d;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-content {
|
||||||
|
flex: 1;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section,
|
||||||
|
.orders-section {
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
21
meiruo-frontend/vite.config.js
Normal file
21
meiruo-frontend/vite.config.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, 'src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
36
美若彩妆销售平台功能清单.md
Normal file
36
美若彩妆销售平台功能清单.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# 美若彩妆销售平台功能清单
|
||||||
|
|
||||||
|
## 一、顾客端功能
|
||||||
|
|
||||||
|
| 序号 | 功能模块 | 功能描述 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 1 | 登录注册 | 账号的登录与注册,获得系统使用权限 |
|
||||||
|
| 2 | 浏览商品 | 查看商品列表,了解商品详情 |
|
||||||
|
| 3 | 分类查询 | 按商品分类查找特定类型商品 |
|
||||||
|
| 4 | 购物车管理 | 添加商品、删除商品、修改数量 |
|
||||||
|
| 5 | 购买商品 | 完成商品选购并进行支付 |
|
||||||
|
| 6 | 订单管理 | 查看订单状态、历史订单、管理订单 |
|
||||||
|
| 7 | 个人信息管理 | 修改联系方式、地址等基本信息 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、管理端功能
|
||||||
|
|
||||||
|
| 序号 | 功能模块 | 功能描述 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 1 | 系统登录 | 管理员登录系统 |
|
||||||
|
| 2 | 用户信息审核 | 用户信息的增删改查审核 |
|
||||||
|
| 3 | 商品管理 | 商品增删改查、商品图片上传/展示/删除、商品状态管理 |
|
||||||
|
| 4 | 订单管理 | 订单查询、创建、删除 |
|
||||||
|
| 5 | 轮播图管理 | 轮播图增加、修改、删除 |
|
||||||
|
| 6 | 营收管理 | 收入柱状图、收入饼图、本周/本月收入统计、销售量排行榜 |
|
||||||
|
| 7 | 个人信息修改 | 修改管理员个人信息 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、技术要求
|
||||||
|
|
||||||
|
- **前端框架**: Vue.js
|
||||||
|
- **后端框架**: Spring Boot
|
||||||
|
- **架构**: 前后端分离
|
||||||
|
- **数据库**: MySQL
|
||||||
208
胡宝月-美若彩妆销售平台-开题报告.md
Normal file
208
胡宝月-美若彩妆销售平台-开题报告.md
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
# 大连科技学院
|
||||||
|
|
||||||
|
# 毕业设计(论文)开题报告
|
||||||
|
|
||||||
|
学 院 信息科学与技术学院
|
||||||
|
|
||||||
|
专业班级 网络工程(专升本)24-1
|
||||||
|
|
||||||
|
学生姓名 胡宝月
|
||||||
|
|
||||||
|
学生学号 2406490128
|
||||||
|
|
||||||
|
指导教师 郭永伟
|
||||||
|
|
||||||
|
导师职称 副教授
|
||||||
|
|
||||||
|
# 1 选题的意义和研究现状
|
||||||
|
|
||||||
|
# 1.1 选题的意义
|
||||||
|
|
||||||
|
在数字化浪潮席卷全球的当下,线上购物凭借便捷性、多样性等优势,逐渐成为消费者日常消费的主流方式。美妆作为兼具实用性与时尚性的消费品类,其线上市场需求持续攀升,消费者对美妆产品的购买渠道、选择空间及购物体验提出了更高要求。
|
||||||
|
|
||||||
|
在美妆消费升级与竞争加剧的背景下,传统销售模式正面临多重挑战:线下门店覆盖有限,消费者选择受限;品牌与用户间信息传递不畅,影响购买决策;同时,繁琐的购物流程与滞后的售后服务也持续拉低消费体验。
|
||||||
|
|
||||||
|
美若彩妆平台的建设,为消费者打造一个突破时空限制、信息透明且流程便捷的美妆购物新体验。其意义不仅在于提升个体消费满意度,更在于推动整个美妆行业拓宽市场、实现数字化转型,从而全面提升运营效率与服务品质。
|
||||||
|
|
||||||
|
# 1.2 本课题所涉及问题在国内外设计或研究的现状
|
||||||
|
|
||||||
|
国内美妆电商生态已趋于成熟,天猫、京东等头部平台凭借庞大的用户基础和完善的供应链体系,实现了美妆全品类覆盖,同时具备快速物流配送和基础的商品分类展示功能,满足消费者日常购买需求。不过多数综合电商平台在美妆垂直领域的针对性服务
|
||||||
|
|
||||||
|
仍有欠缺,用户寻找精准美妆产品的效率有待提升,且对美妆产品的细节展示、使用场景介绍等专项服务不够完善。
|
||||||
|
|
||||||
|
国外以 Sephora、Ulta Beauty 为代表的美妆网络销售平台,核心优势在于线下门店与线上平台的渠道联动,能为消费者提供线下体验、线上复购的连贯服务,线上平台则侧重美妆产品的系列化展示和基础购买功能。但此类平台在跨区域适配、针对不同地区消费者的基础需求响应等方面存在不足,且部分平台的商品筛选逻辑较为复杂,普通消费者难以快速找到适配自身需求的产品。
|
||||||
|
|
||||||
|
综合来看,国内外美妆电商的共有特点的是:均以商品展示、在线购买、订单查询为核心基础功能,聚焦满足消费者美妆产品的线上交易需求,但在垂直领域的精准服务、购买流程简化、用户查找效率等方面仍存在改进空间。
|
||||||
|
|
||||||
|
# 2 课题设计或研究的内容、预期目标和实施计划
|
||||||
|
|
||||||
|
# 2.1 要设计或研究的主要内容方案论证分析
|
||||||
|
|
||||||
|
# 2.1.1 需求分析
|
||||||
|
|
||||||
|
对彩妆平台的消费者、管理人员等进行需求调研,了解管理平台的功能需求和使用习惯,要求本系统有以下功能。
|
||||||
|
|
||||||
|
# (1)顾客
|
||||||
|
|
||||||
|
登录注册:顾客可进行账号的登录与注册操作,以获得系统的使用权限。
|
||||||
|
|
||||||
|
浏览商品:消费者可随意查看商品,了解商品。
|
||||||
|
|
||||||
|
分类查询商品:可以按照商品的分类来查找特定类型的商品,方便快速定位到所需商品类别。
|
||||||
|
|
||||||
|
购物车管理:对选中的商品进行添加、删除、修改数量等管理操作,便于后续统一结算。
|
||||||
|
|
||||||
|
购买商品:完成商品的选购并进行支付,达成交易。
|
||||||
|
|
||||||
|
订单管理:可查看订单的状态、历史订单等信息,对订单进行相关管理。
|
||||||
|
|
||||||
|
更改信息:能够修改个人的基本信息,如联系方式、地址等。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
图 2.1 顾客功能
|
||||||
|
|
||||||
|
|
||||||
|
# (2)管理员
|
||||||
|
|
||||||
|
登录:登录系统
|
||||||
|
|
||||||
|
用户信息审核:负责用户信息的增删改查审核。
|
||||||
|
|
||||||
|
商品管理:负责对商品进行增删改查等基本操作,包括商品信息的录入、维护和展示,
|
||||||
|
|
||||||
|
商品图片的上传、展示和删除,商品状态的管理等功能
|
||||||
|
|
||||||
|
订单管理:负责订单相关的基础操作,包括订单的查询、创建、删除等功能。
|
||||||
|
|
||||||
|
轮播图管理:轮播图的增加,修改,删除等。
|
||||||
|
|
||||||
|
营收管理:各类收入柱状图,各类收入饼图,本周收入统计,本月收入统计以及销售量排行榜。
|
||||||
|
|
||||||
|
个人信息修改:修改个人信息。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
图2.2管理员功能
|
||||||
|
|
||||||
|
|
||||||
|
# 2.1.2 可行性分析
|
||||||
|
|
||||||
|
# (1)技术可行性
|
||||||
|
|
||||||
|
本课题采用成熟的Spring Boot + Vue前后端分离架构,契合电商系统开发的主流技术选型。该架构具备核心优势,可支撑商品、用户、订单等核心业务模块的独立开发与协同运作,技术体系已在各类网络销售平台场景中得到广泛验证,技术路线成熟可靠。同时,依托架构自带的高效接口处理、组件化渲染及缓存机制,能够满足系统响应速度、数据传输效率等性能要求,技术层面无核心障碍。
|
||||||
|
|
||||||
|
# (2)经济可行性
|
||||||
|
|
||||||
|
开发阶段无需为核心技术框架支付商业授权费用,有效降低初始投入成本。功能模
|
||||||
|
|
||||||
|
块按业务逻辑拆分后,可采用迭代开发模式推进,减少重复开发工作,控制开发周期与人力成本。平台上线后,通过线上商品销售、品牌合作等模式可形成明确盈利路径,同时标准化的接口设计与清晰的模块划分,能降低后续功能迭代与Bug修复的维护成本,长期运营的经济成本可控、收益可期。
|
||||||
|
|
||||||
|
# (3)操作可行性
|
||||||
|
|
||||||
|
用户端遵循大众普遍的线上购物操作逻辑,简化登录注册流程、优化商品查找与订单管理路径,无需用户额外学习即可上手。管理员端采用清晰的导航栏与内容区布局,核心操作流程直观易懂,经简单培训即可开展日常管理工作。此外,前端技术的响应式特性可适配多种终端设备,进一步提升不同场景下的操作便利性,无论是用户还是管理员都能高效使用系统。
|
||||||
|
|
||||||
|
综合技术、经济、操作三个维度分析,本课题的技术选型成熟可行,开发与运营的经济成本可控且具备盈利潜力,操作流程符合用户使用习惯,无关键实施障碍。课题的实施不仅能落地实用的网络销售平台功能,还能在技术应用、业务落地等方面形成有价值的实践成果,具备明确的实施意义与可推进性。
|
||||||
|
|
||||||
|
# 2.2 本课题选题特色及预期的目标
|
||||||
|
|
||||||
|
本课题选题特色在于聚焦彩妆垂直领域,基于 Spring Boot $^ +$ Vue 技术栈构建前后端分离架构,实现功能模块化设计,同时结合轮播图管理、营收统计等特色功能,兼顾用户购物体验与管理员运营需求,区别于综合网络销售平台的泛化服务。预期目标为完成平台开发,实现商品管理、订单处理等核心功能稳定运行,达成用户便捷购物、管理员高效管理的双向目标,为美妆线上销售提供功能完整、操作友好的垂直解决方案。
|
||||||
|
|
||||||
|
# 2.3 本课题实施计划
|
||||||
|
|
||||||
|
<table><tr><td>周数</td><td>进度计划</td></tr><tr><td>第1周</td><td>确定毕业设计题目,在网络上对“彩妆销售平台”进行调研</td></tr><tr><td>第2周</td><td>根据前期的调研情况,查阅相关资料完成开题报告撰写</td></tr><tr><td>第3周</td><td>选择与课题相关的外文文献,完成外文翻译。进行前期资料自查,进行系统可行性分析和需求分析</td></tr><tr><td>第4周</td><td>完成毕设前期检查。依据系统功能需求和业务流程分析,完成用例图和用例说明</td></tr><tr><td>第5周</td><td>进行系统分析,以用例图为基础进行类图、活动图和顺序图的绘制,确保系统的一致性和可维护性</td></tr><tr><td>第6周</td><td>完成数据库设计、界面设计,根据反馈意见进行修改</td></tr><tr><td>第7周</td><td>系统实现,按功能模块进行编码</td></tr><tr><td>第8周</td><td>完成毕设中期检查。系统实现,按功能模块进行编码</td></tr><tr><td>第9周</td><td>系统测试,测试本系统各业务功能运行是否正常,验证功能需求是否都符合规范要求。完成论文主体部分</td></tr><tr><td>第10周</td><td>按照系统测试结果修改代码完善功能,并完成论文剩余相关部分内容编写</td></tr><tr><td>第11周</td><td>提交论文初稿,根据反馈意见修改论文</td></tr><tr><td>第12周</td><td>继续修改论文,完成论文查重稿定稿。检查系统功能,为软件验收做好准备</td></tr><tr><td>第13周</td><td>进行软件验收,参加校级论文查重,根据论文内容制作答辩PPT</td></tr><tr><td>第14周</td><td>进行毕业设计答辩,并按照答辩组意见修改论文定稿,完成毕设资料存档</td></tr></table>
|
||||||
|
|
||||||
|
# 3 主要参考文献
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[1] 吴怡婷,高斐,张华辉.应用型本科下新工 Java 课程的师生互动与协同学习研究[J].办公自动化,2025,30(07):42-44+48.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[2] 张倬维,王晗霖,张韬祥,等.基于SSM框架的高速公路实验室管理系统设计[J].中国交通信息化,2025,(S1):355-357.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[3] 黄婧.“互联网+”背景下Java程序设计课程教学改革策略[J].信息与电脑,2025,37(05):233-235.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[4] 王培培.基于SpringBoot的网上商城管理系统设计与实现[J].现代计算机,2024,30(07):117-120.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[5] 付世军,卢淞岩,李梦,等.基于B/S架构的智慧农业管理系统的设计与实现[J].湖北农业科学,2025,64(01):154-161.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[6] 向育程,段元梅.基于 Java 的网上商城系统[J].电脑编程技巧与维护,2024,(08):32-34.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[7] Balasubramanian A ,Elangeswaran J V S . A novel power aware smart agriculture management system based on RNN-LSTM[J].Electrical Engineering,2024,107(2):1-22.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[8] 徐家喜,王小正,朱杰.Java EE 框架技术与案例教程[M].南京大学出版社:202310:312.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[9] 任建新,王一鸣,李鑫,等.基于Java Web的智慧商城购物系统设计[J].信息技术与信息化,2022,(07):23-27.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[10]杨晟,罗奇.基于 Spring Boot 的在线商城系统设计[J].科技创新与应用,2022,12(19):58-61.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[11]李宜镓.基于 SpringBoot 的电商秒杀系统的设计与实现[D].西安电子科技大学,2022.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[12]吕善雨.多元化信息融合的电商推荐系统设计与实现[D].北京邮电大学,2022.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[13]张曰花.基于JavaWeb的山东地方特色产品销售网站设计[J].现代信息科技,2025,9(04): 118-123.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[14]凌洋.基于JavaWeb技术的医院人事管理系统的设计与实现[J].现代计算机,2024,30(19):117-120.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[15]柳小刚.基于 B/S 架构的档案管理系统的设计与实现[J].电子技术,2024,53(08):332-333.
|
||||||
|
|
||||||
Reference in New Issue
Block a user