初始化v1

This commit is contained in:
2026-01-11 18:14:42 +08:00
commit 641b03e8e2
297 changed files with 28656 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
spring:
datasource:
url: jdbc:mysql://localhost:3306/nl_shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root

View File

@@ -0,0 +1,47 @@
server:
port: 14001
spring:
application:
name: nl-shop
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/z_wb_nc_shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
redis:
host: 127.0.0.1
port: 6379
password: redis_pBjaRs
timeout: 3000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 10MB
thymeleaf:
prefix: classpath:/templates/
suffix: .html
mode: HTML
encoding: UTF-8
cache: false
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.nlshop.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
file:
upload-path: ${user.dir}/src/main/resources/static/upload

View File

@@ -0,0 +1,248 @@
-- 奶茶店管理系统数据库表结构
-- 用户表
CREATE TABLE IF NOT EXISTS `user` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(50) UNIQUE NOT NULL COMMENT '用户名',
`password` VARCHAR(255) NOT NULL COMMENT '密码(加密)',
`name` VARCHAR(50) COMMENT '姓名',
`gender` TINYINT COMMENT '性别0-女1-男',
`phone` VARCHAR(20) COMMENT '手机号',
`email` VARCHAR(100) COMMENT '邮箱',
`avatar` VARCHAR(255) COMMENT '头像路径',
`address` VARCHAR(255) COMMENT '默认地址',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 用户地址表
CREATE TABLE IF NOT EXISTS `user_address` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`address` VARCHAR(255) NOT NULL COMMENT '详细地址',
`contact_name` VARCHAR(50) COMMENT '联系人姓名',
`contact_phone` VARCHAR(20) COMMENT '联系人电话',
`is_default` TINYINT DEFAULT 0 COMMENT '是否默认0-否1-是',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户地址表';
-- 商家表
CREATE TABLE IF NOT EXISTS `merchant` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(50) UNIQUE NOT NULL COMMENT '用户名',
`password` VARCHAR(255) NOT NULL COMMENT '密码(加密)',
`store_name` VARCHAR(100) NOT NULL COMMENT '门店名称',
`address` VARCHAR(255) COMMENT '门店地址',
`phone` VARCHAR(20) COMMENT '联系电话',
`email` VARCHAR(100) COMMENT '邮箱',
`manager_name` VARCHAR(50) COMMENT '负责人姓名',
`avatar` VARCHAR(255) COMMENT '头像路径',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商家表';
-- 商品表
CREATE TABLE IF NOT EXISTS `product` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`merchant_id` BIGINT NOT NULL COMMENT '商家ID',
`name` VARCHAR(100) NOT NULL COMMENT '商品名称',
`category` VARCHAR(50) COMMENT '分类',
`description` TEXT COMMENT '商品描述',
`base_price` DECIMAL(10,2) NOT NULL COMMENT '基础价格',
`image` VARCHAR(255) COMMENT '商品图片',
`status` TINYINT DEFAULT 1 COMMENT '状态0-下架1-上架',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`merchant_id`) REFERENCES `merchant`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
-- 商品规格表
CREATE TABLE IF NOT EXISTS `product_spec` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`product_id` BIGINT NOT NULL COMMENT '商品ID',
`spec_name` VARCHAR(50) NOT NULL COMMENT '规格名称(大杯/中杯/小杯)',
`price_adjust` DECIMAL(10,2) DEFAULT 0 COMMENT '价格调整(相对于基础价格)',
FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品规格表';
-- 配料表
CREATE TABLE IF NOT EXISTS `product_topping` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`product_id` BIGINT NOT NULL COMMENT '商品ID',
`topping_name` VARCHAR(50) NOT NULL COMMENT '配料名称',
`price` DECIMAL(10,2) DEFAULT 0 COMMENT '配料价格',
FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配料表';
-- 商品定制选项表
CREATE TABLE IF NOT EXISTS `product_custom` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`product_id` BIGINT NOT NULL COMMENT '商品ID',
`option_type` VARCHAR(20) NOT NULL COMMENT '选项类型sweetness-甜度ice-冰度',
`option_value` VARCHAR(50) NOT NULL COMMENT '选项值',
`price_adjust` DECIMAL(10,2) DEFAULT 0 COMMENT '价格调整',
FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品定制选项表';
-- 购物车表
CREATE TABLE IF NOT EXISTS `cart` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`product_id` BIGINT NOT NULL COMMENT '商品ID',
`spec_id` BIGINT COMMENT '规格ID',
`quantity` INT DEFAULT 1 COMMENT '数量',
`custom_sweetness` VARCHAR(20) COMMENT '甜度定制',
`custom_ice` VARCHAR(20) COMMENT '冰度定制',
`toppings` TEXT COMMENT '配料JSON',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车表';
-- 订单表
CREATE TABLE IF NOT EXISTS `order` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`order_no` VARCHAR(50) UNIQUE NOT NULL COMMENT '订单号',
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`merchant_id` BIGINT NOT NULL COMMENT '商家ID',
`total_amount` DECIMAL(10,2) NOT NULL COMMENT '订单总金额',
`payment_method` VARCHAR(20) COMMENT '支付方式wechat/alipay',
`payment_status` TINYINT DEFAULT 0 COMMENT '支付状态0-未支付1-已支付',
`order_status` VARCHAR(20) DEFAULT 'PENDING_PAY' COMMENT '订单状态PENDING_PAY-待支付MAKING-制作中READY-待取餐COMPLETED-已完成CANCELLED-已取消',
`address` VARCHAR(255) NOT NULL COMMENT '收货地址',
`contact_phone` VARCHAR(20) NOT NULL COMMENT '联系电话',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`pay_time` DATETIME COMMENT '支付时间',
`complete_time` DATETIME COMMENT '完成时间',
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`),
FOREIGN KEY (`merchant_id`) REFERENCES `merchant`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
-- 订单明细表
CREATE TABLE IF NOT EXISTS `order_item` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`order_id` BIGINT NOT NULL COMMENT '订单ID',
`product_id` BIGINT NOT NULL COMMENT '商品ID',
`product_name` VARCHAR(100) NOT NULL COMMENT '商品名称',
`spec_name` VARCHAR(50) COMMENT '规格名称',
`quantity` INT NOT NULL COMMENT '数量',
`price` DECIMAL(10,2) NOT NULL COMMENT '单价',
`custom_sweetness` VARCHAR(20) COMMENT '甜度',
`custom_ice` VARCHAR(20) COMMENT '冰度',
`toppings` TEXT COMMENT '配料JSON',
`subtotal` DECIMAL(10,2) NOT NULL COMMENT '小计',
FOREIGN KEY (`order_id`) REFERENCES `order`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`product_id`) REFERENCES `product`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';
-- 订单跟踪表
CREATE TABLE IF NOT EXISTS `order_track` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`order_id` BIGINT NOT NULL COMMENT '订单ID',
`status` VARCHAR(20) NOT NULL COMMENT '状态',
`description` VARCHAR(255) COMMENT '描述',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`order_id`) REFERENCES `order`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单跟踪表';
-- 公告表
CREATE TABLE IF NOT EXISTS `announcement` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`merchant_id` BIGINT NOT NULL COMMENT '商家ID',
`title` VARCHAR(200) NOT NULL COMMENT '标题',
`content` TEXT COMMENT '内容',
`type` VARCHAR(20) DEFAULT 'NOTICE' COMMENT '类型ACTIVITY-活动NOTICE-通知',
`status` TINYINT DEFAULT 1 COMMENT '状态0-下架1-发布',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`merchant_id`) REFERENCES `merchant`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公告表';
-- 留言反馈表
CREATE TABLE IF NOT EXISTS `message` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`merchant_id` BIGINT NOT NULL COMMENT '商家ID',
`order_id` BIGINT COMMENT '订单ID',
`content` TEXT NOT NULL COMMENT '留言内容',
`reply` TEXT COMMENT '回复内容',
`status` TINYINT DEFAULT 0 COMMENT '状态0-未回复1-已回复',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`reply_time` DATETIME COMMENT '回复时间',
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`),
FOREIGN KEY (`merchant_id`) REFERENCES `merchant`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='留言反馈表';
-- 评价表
CREATE TABLE IF NOT EXISTS `review` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`order_id` BIGINT NOT NULL COMMENT '订单ID',
`product_id` BIGINT NOT NULL COMMENT '商品ID',
`rating` TINYINT NOT NULL COMMENT '评分1-5',
`content` TEXT COMMENT '评价内容',
`reply` TEXT COMMENT '商家回复',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`),
FOREIGN KEY (`order_id`) REFERENCES `order`(`id`),
FOREIGN KEY (`product_id`) REFERENCES `product`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评价表';
-- 库存表
CREATE TABLE IF NOT EXISTS `inventory` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`merchant_id` BIGINT NOT NULL COMMENT '商家ID',
`material_name` VARCHAR(100) NOT NULL COMMENT '原料名称',
`quantity` DECIMAL(10,2) NOT NULL COMMENT '数量',
`unit` VARCHAR(20) COMMENT '单位',
`min_threshold` DECIMAL(10,2) DEFAULT 0 COMMENT '最低库存阈值',
`last_in_time` DATETIME COMMENT '最后入库时间',
`last_out_time` DATETIME COMMENT '最后出库时间',
FOREIGN KEY (`merchant_id`) REFERENCES `merchant`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';
-- 库存操作日志表
CREATE TABLE IF NOT EXISTS `inventory_log` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`inventory_id` BIGINT NOT NULL COMMENT '库存ID',
`operation_type` VARCHAR(20) NOT NULL COMMENT '操作类型IN-入库OUT-出库',
`quantity` DECIMAL(10,2) NOT NULL COMMENT '操作数量',
`operator` VARCHAR(50) COMMENT '操作人',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`inventory_id`) REFERENCES `inventory`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存操作日志表';
-- 用户行为表(用于协同过滤)
CREATE TABLE IF NOT EXISTS `user_behavior` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`product_id` BIGINT NOT NULL COMMENT '商品ID',
`behavior_type` VARCHAR(20) NOT NULL COMMENT '行为类型VIEW-浏览PURCHASE-购买RATING-评分',
`score` DECIMAL(5,2) DEFAULT 0 COMMENT '评分0-5',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户行为表';
-- 用户相似度缓存表(可选)
CREATE TABLE IF NOT EXISTS `user_similarity` (
`user1_id` BIGINT NOT NULL COMMENT '用户1ID',
`user2_id` BIGINT NOT NULL COMMENT '用户2ID',
`similarity_score` DECIMAL(10,6) NOT NULL COMMENT '相似度分数',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`user1_id`, `user2_id`),
FOREIGN KEY (`user1_id`) REFERENCES `user`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`user2_id`) REFERENCES `user`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户相似度缓存表';
-- 创建索引
CREATE INDEX idx_user_username ON `user`(`username`);
CREATE INDEX idx_merchant_username ON `merchant`(`username`);
CREATE INDEX idx_product_merchant ON `product`(`merchant_id`);
CREATE INDEX idx_product_status ON `product`(`status`);
CREATE INDEX idx_cart_user ON `cart`(`user_id`);
CREATE INDEX idx_order_user ON `order`(`user_id`);
CREATE INDEX idx_order_merchant ON `order`(`merchant_id`);
CREATE INDEX idx_order_status ON `order`(`order_status`);
CREATE INDEX idx_order_no ON `order`(`order_no`);
CREATE INDEX idx_user_behavior_user ON `user_behavior`(`user_id`);
CREATE INDEX idx_user_behavior_product ON `user_behavior`(`product_id`);

View File

@@ -0,0 +1,45 @@
<?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.nlshop.mapper.AnnouncementMapper">
<resultMap id="BaseResultMap" type="com.nlshop.entity.Announcement">
<id column="id" property="id"/>
<result column="merchant_id" property="merchantId"/>
<result column="title" property="title"/>
<result column="content" property="content"/>
<result column="type" property="type"/>
<result column="status" property="status"/>
<result column="create_time" property="createTime"/>
</resultMap>
<insert id="insert" parameterType="com.nlshop.entity.Announcement" useGeneratedKeys="true" keyProperty="id">
INSERT INTO announcement (merchant_id, title, content, type, status)
VALUES (#{merchantId}, #{title}, #{content}, #{type}, #{status})
</insert>
<select id="selectByMerchantId" resultMap="BaseResultMap">
SELECT * FROM announcement WHERE merchant_id = #{merchantId} ORDER BY create_time DESC
</select>
<select id="selectPublished" resultMap="BaseResultMap">
SELECT * FROM announcement WHERE status = 1 ORDER BY create_time DESC
</select>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM announcement WHERE id = #{id}
</select>
<update id="update" parameterType="com.nlshop.entity.Announcement">
UPDATE announcement
<set>
<if test="title != null">title = #{title},</if>
<if test="content != null">content = #{content},</if>
<if test="type != null">type = #{type},</if>
<if test="status != null">status = #{status},</if>
</set>
WHERE id = #{id}
</update>
<delete id="delete">
DELETE FROM announcement WHERE id = #{id}
</delete>
</mapper>

View File

@@ -0,0 +1,95 @@
<?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.nlshop.mapper.CartMapper">
<resultMap id="BaseResultMap" type="com.nlshop.entity.CartItem">
<id column="id" property="id"/>
<result column="user_id" property="userId"/>
<result column="product_id" property="productId"/>
<result column="spec_id" property="specId"/>
<result column="quantity" property="quantity"/>
<result column="custom_sweetness" property="customSweetness"/>
<result column="custom_ice" property="customIce"/>
<result column="toppings" property="toppings"/>
<result column="create_time" property="createTime"/>
<association property="product" javaType="com.nlshop.entity.Product">
<id column="p_id" property="id"/>
<result column="p_name" property="name"/>
<result column="p_image" property="image"/>
<result column="p_base_price" property="basePrice"/>
<result column="p_merchant_id" property="merchantId"/>
</association>
<association property="spec" javaType="com.nlshop.entity.ProductSpec">
<id column="s_id" property="id"/>
<result column="s_spec_name" property="specName"/>
<result column="s_price_adjust" property="priceAdjust"/>
</association>
</resultMap>
<insert id="insert" parameterType="com.nlshop.entity.CartItem" useGeneratedKeys="true" keyProperty="id">
INSERT INTO cart (user_id, product_id, spec_id, quantity, custom_sweetness, custom_ice, toppings)
VALUES (#{userId}, #{productId}, #{specId}, #{quantity}, #{customSweetness}, #{customIce}, #{toppings})
</insert>
<select id="selectByUserId" resultMap="BaseResultMap">
SELECT c.*,
p.id as p_id, p.name as p_name, p.image as p_image, p.base_price as p_base_price, p.merchant_id as p_merchant_id,
s.id as s_id, s.spec_name as s_spec_name, s.price_adjust as s_price_adjust
FROM cart c
LEFT JOIN product p ON c.product_id = p.id
LEFT JOIN product_spec s ON c.spec_id = s.id
WHERE c.user_id = #{userId}
ORDER BY c.create_time DESC
</select>
<select id="selectById" resultMap="BaseResultMap">
SELECT c.*,
p.id as p_id, p.name as p_name, p.image as p_image, p.base_price as p_base_price, p.merchant_id as p_merchant_id,
s.id as s_id, s.spec_name as s_spec_name, s.price_adjust as s_price_adjust
FROM cart c
LEFT JOIN product p ON c.product_id = p.id
LEFT JOIN product_spec s ON c.spec_id = s.id
WHERE c.id = #{id}
</select>
<select id="selectByUserAndProduct" resultMap="BaseResultMap">
SELECT * FROM cart
WHERE user_id = #{userId} AND product_id = #{productId}
AND (spec_id = #{specId} OR (#{specId} IS NULL AND spec_id IS NULL))
AND (custom_sweetness = #{customSweetness} OR (#{customSweetness} IS NULL AND custom_sweetness IS NULL))
AND (custom_ice = #{customIce} OR (#{customIce} IS NULL AND custom_ice IS NULL))
AND (toppings = #{toppings} OR (#{toppings} IS NULL AND toppings IS NULL))
LIMIT 1
</select>
<update id="update" parameterType="com.nlshop.entity.CartItem">
UPDATE cart
<set>
<if test="quantity != null">quantity = #{quantity},</if>
<if test="customSweetness != null">custom_sweetness = #{customSweetness},</if>
<if test="customIce != null">custom_ice = #{customIce},</if>
<if test="toppings != null">toppings = #{toppings},</if>
</set>
WHERE id = #{id}
</update>
<update id="updateQuantity">
UPDATE cart SET quantity = #{quantity} 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>
<delete id="deleteBatch">
DELETE FROM cart
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
AND user_id = #{userId}
</delete>
</mapper>

View File

@@ -0,0 +1,21 @@
<?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.nlshop.mapper.InventoryLogMapper">
<resultMap id="BaseResultMap" type="com.nlshop.entity.InventoryLog">
<id column="id" property="id"/>
<result column="inventory_id" property="inventoryId"/>
<result column="operation_type" property="operationType"/>
<result column="quantity" property="quantity"/>
<result column="operator" property="operator"/>
<result column="create_time" property="createTime"/>
</resultMap>
<insert id="insert" parameterType="com.nlshop.entity.InventoryLog" useGeneratedKeys="true" keyProperty="id">
INSERT INTO inventory_log (inventory_id, operation_type, quantity, operator)
VALUES (#{inventoryId}, #{operationType}, #{quantity}, #{operator})
</insert>
<select id="selectByInventoryId" resultMap="BaseResultMap">
SELECT * FROM inventory_log WHERE inventory_id = #{inventoryId} ORDER BY create_time DESC
</select>
</mapper>

View File

@@ -0,0 +1,55 @@
<?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.nlshop.mapper.InventoryMapper">
<resultMap id="BaseResultMap" type="com.nlshop.entity.Inventory">
<id column="id" property="id"/>
<result column="merchant_id" property="merchantId"/>
<result column="material_name" property="materialName"/>
<result column="quantity" property="quantity"/>
<result column="unit" property="unit"/>
<result column="min_threshold" property="minThreshold"/>
<result column="last_in_time" property="lastInTime"/>
<result column="last_out_time" property="lastOutTime"/>
</resultMap>
<insert id="insert" parameterType="com.nlshop.entity.Inventory" useGeneratedKeys="true" keyProperty="id">
INSERT INTO inventory (merchant_id, material_name, quantity, unit, min_threshold)
VALUES (#{merchantId}, #{materialName}, #{quantity}, #{unit}, #{minThreshold})
</insert>
<select id="selectByMerchantId" resultMap="BaseResultMap">
SELECT * FROM inventory WHERE merchant_id = #{merchantId} ORDER BY material_name
</select>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM inventory WHERE id = #{id}
</select>
<select id="selectByMerchantAndMaterial" resultMap="BaseResultMap">
SELECT * FROM inventory
WHERE merchant_id = #{merchantId} AND material_name = #{materialName}
LIMIT 1
</select>
<select id="selectLowStock" resultMap="BaseResultMap">
SELECT * FROM inventory
WHERE merchant_id = #{merchantId} AND quantity &lt;= min_threshold
ORDER BY quantity ASC
</select>
<update id="update" parameterType="com.nlshop.entity.Inventory">
UPDATE inventory
<set>
<if test="quantity != null">quantity = #{quantity},</if>
<if test="unit != null">unit = #{unit},</if>
<if test="minThreshold != null">min_threshold = #{minThreshold},</if>
<if test="lastInTime != null">last_in_time = #{lastInTime},</if>
<if test="lastOutTime != null">last_out_time = #{lastOutTime},</if>
</set>
WHERE id = #{id}
</update>
<delete id="delete">
DELETE FROM inventory WHERE id = #{id}
</delete>
</mapper>

View File

@@ -0,0 +1,43 @@
<?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.nlshop.mapper.MerchantMapper">
<resultMap id="BaseResultMap" type="com.nlshop.entity.Merchant">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="store_name" property="storeName"/>
<result column="address" property="address"/>
<result column="phone" property="phone"/>
<result column="email" property="email"/>
<result column="manager_name" property="managerName"/>
<result column="avatar" property="avatar"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<insert id="insert" parameterType="com.nlshop.entity.Merchant" useGeneratedKeys="true" keyProperty="id">
INSERT INTO merchant (username, password, store_name, address, phone, email, manager_name, avatar)
VALUES (#{username}, #{password}, #{storeName}, #{address}, #{phone}, #{email}, #{managerName}, #{avatar})
</insert>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM merchant WHERE id = #{id}
</select>
<select id="selectByUsername" resultMap="BaseResultMap">
SELECT * FROM merchant WHERE username = #{username}
</select>
<update id="update" parameterType="com.nlshop.entity.Merchant">
UPDATE merchant
<set>
<if test="storeName != null">store_name = #{storeName},</if>
<if test="address != null">address = #{address},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="email != null">email = #{email},</if>
<if test="managerName != null">manager_name = #{managerName},</if>
<if test="avatar != null">avatar = #{avatar},</if>
</set>
WHERE id = #{id}
</update>
</mapper>

View File

@@ -0,0 +1,42 @@
<?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.nlshop.mapper.MessageMapper">
<resultMap id="BaseResultMap" type="com.nlshop.entity.Message">
<id column="id" property="id"/>
<result column="user_id" property="userId"/>
<result column="merchant_id" property="merchantId"/>
<result column="order_id" property="orderId"/>
<result column="content" property="content"/>
<result column="reply" property="reply"/>
<result column="status" property="status"/>
<result column="create_time" property="createTime"/>
<result column="reply_time" property="replyTime"/>
</resultMap>
<insert id="insert" parameterType="com.nlshop.entity.Message" useGeneratedKeys="true" keyProperty="id">
INSERT INTO message (user_id, merchant_id, order_id, content, status)
VALUES (#{userId}, #{merchantId}, #{orderId}, #{content}, #{status})
</insert>
<select id="selectByUserId" resultMap="BaseResultMap">
SELECT * FROM message WHERE user_id = #{userId} ORDER BY create_time DESC
</select>
<select id="selectByMerchantId" resultMap="BaseResultMap">
SELECT * FROM message WHERE merchant_id = #{merchantId} ORDER BY create_time DESC
</select>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM message WHERE id = #{id}
</select>
<update id="update" parameterType="com.nlshop.entity.Message">
UPDATE message
<set>
<if test="reply != null">reply = #{reply},</if>
<if test="status != null">status = #{status},</if>
<if test="replyTime != null">reply_time = #{replyTime},</if>
</set>
WHERE id = #{id}
</update>
</mapper>

View File

@@ -0,0 +1,32 @@
<?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.nlshop.mapper.OrderItemMapper">
<resultMap id="BaseResultMap" type="com.nlshop.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="spec_name" property="specName"/>
<result column="quantity" property="quantity"/>
<result column="price" property="price"/>
<result column="custom_sweetness" property="customSweetness"/>
<result column="custom_ice" property="customIce"/>
<result column="toppings" property="toppings"/>
<result column="subtotal" property="subtotal"/>
</resultMap>
<insert id="insert" parameterType="com.nlshop.entity.OrderItem" useGeneratedKeys="true" keyProperty="id">
INSERT INTO order_item (order_id, product_id, product_name, spec_name, quantity, price,
custom_sweetness, custom_ice, toppings, subtotal)
VALUES (#{orderId}, #{productId}, #{productName}, #{specName}, #{quantity}, #{price},
#{customSweetness}, #{customIce}, #{toppings}, #{subtotal})
</insert>
<select id="selectByOrderId" resultMap="BaseResultMap">
SELECT * FROM order_item WHERE order_id = #{orderId}
</select>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM order_item WHERE id = #{id}
</select>
</mapper>

View File

@@ -0,0 +1,107 @@
<?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.nlshop.mapper.OrderMapper">
<resultMap id="BaseResultMap" type="com.nlshop.entity.Order">
<id column="id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="user_id" property="userId"/>
<result column="merchant_id" property="merchantId"/>
<result column="total_amount" property="totalAmount"/>
<result column="payment_method" property="paymentMethod"/>
<result column="payment_status" property="paymentStatus"/>
<result column="order_status" property="orderStatus"/>
<result column="address" property="address"/>
<result column="contact_phone" property="contactPhone"/>
<result column="create_time" property="createTime"/>
<result column="pay_time" property="payTime"/>
<result column="complete_time" property="completeTime"/>
<association property="merchant" javaType="com.nlshop.entity.Merchant">
<id column="m_id" property="id"/>
<result column="m_store_name" property="storeName"/>
</association>
</resultMap>
<insert id="insert" parameterType="com.nlshop.entity.Order" useGeneratedKeys="true" keyProperty="id">
INSERT INTO `order` (order_no, user_id, merchant_id, total_amount, payment_method,
payment_status, order_status, address, contact_phone, pay_time, complete_time)
VALUES (#{orderNo}, #{userId}, #{merchantId}, #{totalAmount}, #{paymentMethod},
#{paymentStatus}, #{orderStatus}, #{address}, #{contactPhone}, #{payTime}, #{completeTime})
</insert>
<select id="selectById" resultMap="BaseResultMap">
SELECT o.*, m.id as m_id, m.store_name as m_store_name
FROM `order` o
LEFT JOIN merchant m ON o.merchant_id = m.id
WHERE o.id = #{id}
</select>
<select id="selectByOrderNo" resultMap="BaseResultMap">
SELECT * FROM `order` WHERE order_no = #{orderNo}
</select>
<select id="selectByUserId" resultMap="BaseResultMap">
SELECT o.*, m.id as m_id, m.store_name as m_store_name
FROM `order` o
LEFT JOIN merchant m ON o.merchant_id = m.id
WHERE o.user_id = #{userId}
<if test="status != null and status != ''">
AND o.order_status = #{status}
</if>
ORDER BY o.create_time DESC
</select>
<select id="selectByMerchantId" resultMap="BaseResultMap">
SELECT o.*, m.id as m_id, m.store_name as m_store_name
FROM `order` o
LEFT JOIN merchant m ON o.merchant_id = m.id
WHERE o.merchant_id = #{merchantId}
<if test="status != null and status != ''">
AND o.order_status = #{status}
</if>
ORDER BY o.create_time DESC
</select>
<update id="update" parameterType="com.nlshop.entity.Order">
UPDATE `order`
<set>
<if test="totalAmount != null">total_amount = #{totalAmount},</if>
<if test="paymentMethod != null">payment_method = #{paymentMethod},</if>
<if test="paymentStatus != null">payment_status = #{paymentStatus},</if>
<if test="orderStatus != null">order_status = #{orderStatus},</if>
<if test="address != null">address = #{address},</if>
<if test="contactPhone != null">contact_phone = #{contactPhone},</if>
<if test="payTime != null">pay_time = #{payTime},</if>
<if test="completeTime != null">complete_time = #{completeTime},</if>
</set>
WHERE id = #{id}
</update>
<update id="updateStatus">
UPDATE `order` SET order_status = #{status} WHERE id = #{id}
</update>
<update id="updatePayment">
UPDATE `order`
SET payment_status = #{paymentStatus},
payment_method = #{paymentMethod},
pay_time = NOW()
WHERE id = #{id}
</update>
<select id="selectStickyUsers" resultType="java.util.HashMap">
SELECT
u.id as userId,
u.username,
u.name,
COUNT(o.id) as orderCount,
SUM(o.total_amount) as totalAmount
FROM `order` o
INNER JOIN user u ON o.user_id = u.id
WHERE o.merchant_id = #{merchantId}
AND o.order_status = 'COMPLETED'
AND o.create_time >= DATE_SUB(NOW(), INTERVAL #{days} DAY)
GROUP BY u.id, u.username, u.name
HAVING COUNT(o.id) >= #{minOrders}
ORDER BY orderCount DESC, totalAmount DESC
</select>
</mapper>

View File

@@ -0,0 +1,20 @@
<?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.nlshop.mapper.OrderTrackMapper">
<resultMap id="BaseResultMap" type="com.nlshop.entity.OrderTrack">
<id column="id" property="id"/>
<result column="order_id" property="orderId"/>
<result column="status" property="status"/>
<result column="description" property="description"/>
<result column="create_time" property="createTime"/>
</resultMap>
<insert id="insert" parameterType="com.nlshop.entity.OrderTrack" useGeneratedKeys="true" keyProperty="id">
INSERT INTO order_track (order_id, status, description)
VALUES (#{orderId}, #{status}, #{description})
</insert>
<select id="selectByOrderId" resultMap="BaseResultMap">
SELECT * FROM order_track WHERE order_id = #{orderId} ORDER BY create_time ASC
</select>
</mapper>

View File

@@ -0,0 +1,38 @@
<?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.nlshop.mapper.ProductCustomMapper">
<resultMap id="BaseResultMap" type="com.nlshop.entity.ProductCustom">
<id column="id" property="id"/>
<result column="product_id" property="productId"/>
<result column="option_type" property="optionType"/>
<result column="option_value" property="optionValue"/>
<result column="price_adjust" property="priceAdjust"/>
</resultMap>
<insert id="insert" parameterType="com.nlshop.entity.ProductCustom" useGeneratedKeys="true" keyProperty="id">
INSERT INTO product_custom (product_id, option_type, option_value, price_adjust)
VALUES (#{productId}, #{optionType}, #{optionValue}, #{priceAdjust})
</insert>
<select id="selectByProductId" resultMap="BaseResultMap">
SELECT * FROM product_custom WHERE product_id = #{productId}
</select>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM product_custom WHERE id = #{id}
</select>
<update id="update" parameterType="com.nlshop.entity.ProductCustom">
UPDATE product_custom
SET option_type = #{optionType}, option_value = #{optionValue}, price_adjust = #{priceAdjust}
WHERE id = #{id}
</update>
<delete id="delete">
DELETE FROM product_custom WHERE id = #{id}
</delete>
<delete id="deleteByProductId">
DELETE FROM product_custom WHERE product_id = #{productId}
</delete>
</mapper>

View File

@@ -0,0 +1,71 @@
<?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.nlshop.mapper.ProductMapper">
<resultMap id="BaseResultMap" type="com.nlshop.entity.Product">
<id column="id" property="id"/>
<result column="merchant_id" property="merchantId"/>
<result column="name" property="name"/>
<result column="category" property="category"/>
<result column="description" property="description"/>
<result column="base_price" property="basePrice"/>
<result column="image" property="image"/>
<result column="status" property="status"/>
<result column="create_time" property="createTime"/>
</resultMap>
<insert id="insert" parameterType="com.nlshop.entity.Product" useGeneratedKeys="true" keyProperty="id">
INSERT INTO product (merchant_id, name, category, description, base_price, image, status)
VALUES (#{merchantId}, #{name}, #{category}, #{description}, #{basePrice}, #{image}, #{status})
</insert>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM product WHERE id = #{id}
</select>
<select id="selectByMerchantId" resultMap="BaseResultMap">
SELECT * FROM product WHERE merchant_id = #{merchantId}
<if test="status != null">
AND status = #{status}
</if>
ORDER BY create_time DESC
</select>
<select id="selectAll" resultMap="BaseResultMap">
SELECT * FROM product WHERE 1=1
<if test="category != null and category != ''">
AND category = #{category}
</if>
<if test="status != null">
AND status = #{status}
</if>
ORDER BY create_time DESC
</select>
<select id="selectHotProducts" resultMap="BaseResultMap">
SELECT p.*, SUM(oi.quantity) as sales
FROM product p
LEFT JOIN order_item oi ON p.id = oi.product_id
LEFT JOIN `order` o ON oi.order_id = o.id
WHERE o.order_status = 'COMPLETED' AND p.status = 1
GROUP BY p.id
ORDER BY sales DESC
LIMIT #{limit}
</select>
<update id="update" parameterType="com.nlshop.entity.Product">
UPDATE product
<set>
<if test="name != null">name = #{name},</if>
<if test="category != null">category = #{category},</if>
<if test="description != null">description = #{description},</if>
<if test="basePrice != null">base_price = #{basePrice},</if>
<if test="image != null">image = #{image},</if>
<if test="status != null">status = #{status},</if>
</set>
WHERE id = #{id}
</update>
<update id="updateStatus">
UPDATE product SET status = #{status} WHERE id = #{id}
</update>
</mapper>

View File

@@ -0,0 +1,37 @@
<?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.nlshop.mapper.ProductSpecMapper">
<resultMap id="BaseResultMap" type="com.nlshop.entity.ProductSpec">
<id column="id" property="id"/>
<result column="product_id" property="productId"/>
<result column="spec_name" property="specName"/>
<result column="price_adjust" property="priceAdjust"/>
</resultMap>
<insert id="insert" parameterType="com.nlshop.entity.ProductSpec" useGeneratedKeys="true" keyProperty="id">
INSERT INTO product_spec (product_id, spec_name, price_adjust)
VALUES (#{productId}, #{specName}, #{priceAdjust})
</insert>
<select id="selectByProductId" resultMap="BaseResultMap">
SELECT * FROM product_spec WHERE product_id = #{productId}
</select>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM product_spec WHERE id = #{id}
</select>
<update id="update" parameterType="com.nlshop.entity.ProductSpec">
UPDATE product_spec
SET spec_name = #{specName}, price_adjust = #{priceAdjust}
WHERE id = #{id}
</update>
<delete id="delete">
DELETE FROM product_spec WHERE id = #{id}
</delete>
<delete id="deleteByProductId">
DELETE FROM product_spec WHERE product_id = #{productId}
</delete>
</mapper>

View File

@@ -0,0 +1,37 @@
<?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.nlshop.mapper.ProductToppingMapper">
<resultMap id="BaseResultMap" type="com.nlshop.entity.ProductTopping">
<id column="id" property="id"/>
<result column="product_id" property="productId"/>
<result column="topping_name" property="toppingName"/>
<result column="price" property="price"/>
</resultMap>
<insert id="insert" parameterType="com.nlshop.entity.ProductTopping" useGeneratedKeys="true" keyProperty="id">
INSERT INTO product_topping (product_id, topping_name, price)
VALUES (#{productId}, #{toppingName}, #{price})
</insert>
<select id="selectByProductId" resultMap="BaseResultMap">
SELECT * FROM product_topping WHERE product_id = #{productId}
</select>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM product_topping WHERE id = #{id}
</select>
<update id="update" parameterType="com.nlshop.entity.ProductTopping">
UPDATE product_topping
SET topping_name = #{toppingName}, price = #{price}
WHERE id = #{id}
</update>
<delete id="delete">
DELETE FROM product_topping WHERE id = #{id}
</delete>
<delete id="deleteByProductId">
DELETE FROM product_topping WHERE product_id = #{productId}
</delete>
</mapper>

View File

@@ -0,0 +1,53 @@
<?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.nlshop.mapper.ReviewMapper">
<resultMap id="BaseResultMap" type="com.nlshop.entity.Review">
<id column="id" property="id"/>
<result column="user_id" property="userId"/>
<result column="order_id" property="orderId"/>
<result column="product_id" property="productId"/>
<result column="rating" property="rating"/>
<result column="content" property="content"/>
<result column="reply" property="reply"/>
<result column="create_time" property="createTime"/>
</resultMap>
<insert id="insert" parameterType="com.nlshop.entity.Review" useGeneratedKeys="true" keyProperty="id">
INSERT INTO review (user_id, order_id, product_id, rating, content)
VALUES (#{userId}, #{orderId}, #{productId}, #{rating}, #{content})
</insert>
<select id="selectByMerchantId" resultMap="BaseResultMap">
SELECT r.* FROM review r
INNER JOIN product p ON r.product_id = p.id
WHERE p.merchant_id = #{merchantId}
ORDER BY r.create_time DESC
</select>
<select id="selectByProductId" resultMap="BaseResultMap">
SELECT * FROM review WHERE product_id = #{productId} ORDER BY create_time DESC
</select>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM review WHERE id = #{id}
</select>
<select id="selectByOrderId" resultMap="BaseResultMap">
SELECT * FROM review WHERE order_id = #{orderId} LIMIT 1
</select>
<select id="selectByOrderIdList" resultMap="BaseResultMap">
SELECT * FROM review WHERE order_id IN
<foreach collection="orderIds" item="orderId" open="(" separator="," close=")">
#{orderId}
</foreach>
</select>
<update id="update" parameterType="com.nlshop.entity.Review">
UPDATE review
<set>
<if test="reply != null">reply = #{reply},</if>
</set>
WHERE id = #{id}
</update>
</mapper>

View File

@@ -0,0 +1,45 @@
<?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.nlshop.mapper.UserAddressMapper">
<resultMap id="BaseResultMap" type="com.nlshop.entity.UserAddress">
<id column="id" property="id"/>
<result column="user_id" property="userId"/>
<result column="address" property="address"/>
<result column="contact_name" property="contactName"/>
<result column="contact_phone" property="contactPhone"/>
<result column="is_default" property="isDefault"/>
<result column="create_time" property="createTime"/>
</resultMap>
<insert id="insert" parameterType="com.nlshop.entity.UserAddress" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user_address (user_id, address, contact_name, contact_phone, is_default)
VALUES (#{userId}, #{address}, #{contactName}, #{contactPhone}, #{isDefault})
</insert>
<select id="selectByUserId" resultMap="BaseResultMap">
SELECT * FROM user_address WHERE user_id = #{userId} ORDER BY is_default DESC, create_time DESC
</select>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM user_address WHERE id = #{id}
</select>
<select id="selectDefaultByUserId" resultMap="BaseResultMap">
SELECT * FROM user_address WHERE user_id = #{userId} AND is_default = 1 LIMIT 1
</select>
<update id="update" parameterType="com.nlshop.entity.UserAddress">
UPDATE user_address
<set>
<if test="address != null">address = #{address},</if>
<if test="contactName != null">contact_name = #{contactName},</if>
<if test="contactPhone != null">contact_phone = #{contactPhone},</if>
<if test="isDefault != null">is_default = #{isDefault},</if>
</set>
WHERE id = #{id}
</update>
<delete id="delete">
DELETE FROM user_address WHERE id = #{id}
</delete>
</mapper>

View 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.nlshop.mapper.UserBehaviorMapper">
<resultMap id="BaseResultMap" type="com.nlshop.entity.UserBehavior">
<id column="id" property="id"/>
<result column="user_id" property="userId"/>
<result column="product_id" property="productId"/>
<result column="behavior_type" property="behaviorType"/>
<result column="score" property="score"/>
<result column="create_time" property="createTime"/>
</resultMap>
<insert id="insert" parameterType="com.nlshop.entity.UserBehavior" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user_behavior (user_id, product_id, behavior_type, score)
VALUES (#{userId}, #{productId}, #{behaviorType}, #{score})
</insert>
<select id="selectByUserId" resultMap="BaseResultMap">
SELECT * FROM user_behavior WHERE user_id = #{userId} ORDER BY create_time DESC
</select>
<select id="selectByProductId" resultMap="BaseResultMap">
SELECT * FROM user_behavior WHERE product_id = #{productId} ORDER BY create_time DESC
</select>
<select id="selectByUserAndProduct" resultMap="BaseResultMap">
SELECT * FROM user_behavior
WHERE user_id = #{userId} AND product_id = #{productId} AND behavior_type = #{behaviorType}
LIMIT 1
</select>
<update id="update" parameterType="com.nlshop.entity.UserBehavior">
UPDATE user_behavior
SET score = #{score}, create_time = NOW()
WHERE id = #{id}
</update>
<select id="selectProductIdsByUserId" resultType="java.lang.Long">
SELECT DISTINCT product_id FROM user_behavior WHERE user_id = #{userId}
</select>
<select id="selectUserIdsByProductId" resultType="java.lang.Long">
SELECT DISTINCT user_id FROM user_behavior WHERE product_id = #{productId}
</select>
<select id="selectActiveUsers" resultType="java.util.HashMap">
SELECT
u.id as userId,
u.username,
u.name,
COUNT(ub.id) as viewCount,
MAX(ub.create_time) as lastViewTime
FROM user_behavior ub
INNER JOIN user u ON ub.user_id = u.id
INNER JOIN product p ON ub.product_id = p.id
WHERE ub.behavior_type = 'VIEW'
AND p.merchant_id = #{merchantId}
AND ub.create_time >= DATE_SUB(NOW(), INTERVAL #{days} DAY)
GROUP BY u.id, u.username, u.name
ORDER BY lastViewTime DESC
</select>
</mapper>

View File

@@ -0,0 +1,51 @@
<?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.nlshop.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.nlshop.entity.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="name" property="name"/>
<result column="gender" property="gender"/>
<result column="phone" property="phone"/>
<result column="email" property="email"/>
<result column="avatar" property="avatar"/>
<result column="address" property="address"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<insert id="insert" parameterType="com.nlshop.entity.User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (username, password, name, gender, phone, email, avatar, address)
VALUES (#{username}, #{password}, #{name}, #{gender}, #{phone}, #{email}, #{avatar}, #{address})
</insert>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM user WHERE id = #{id}
</select>
<select id="selectByUsername" resultMap="BaseResultMap">
SELECT * FROM user WHERE username = #{username}
</select>
<update id="update" parameterType="com.nlshop.entity.User">
UPDATE user
<set>
<if test="name != null">name = #{name},</if>
<if test="gender != null">gender = #{gender},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="email != null">email = #{email},</if>
<if test="avatar != null">avatar = #{avatar},</if>
<if test="address != null">address = #{address},</if>
</set>
WHERE id = #{id}
</update>
<update id="updatePassword">
UPDATE user SET password = #{password} WHERE id = #{id}
</update>
<select id="selectAll" resultMap="BaseResultMap">
SELECT * FROM user ORDER BY create_time DESC
</select>
</mapper>

View File

@@ -0,0 +1,31 @@
<?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.nlshop.mapper.UserSimilarityMapper">
<resultMap id="BaseResultMap" type="com.nlshop.entity.UserSimilarity">
<result column="user1_id" property="user1Id"/>
<result column="user2_id" property="user2Id"/>
<result column="similarity_score" property="similarityScore"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<insert id="insertOrUpdate" parameterType="com.nlshop.entity.UserSimilarity">
INSERT INTO user_similarity (user1_id, user2_id, similarity_score)
VALUES (#{user1Id}, #{user2Id}, #{similarityScore})
ON DUPLICATE KEY UPDATE
similarity_score = #{similarityScore}, update_time = NOW()
</insert>
<select id="selectByUserIds" resultMap="BaseResultMap">
SELECT * FROM user_similarity
WHERE (user1_id = #{user1Id} AND user2_id = #{user2Id})
OR (user1_id = #{user2Id} AND user2_id = #{user1Id})
LIMIT 1
</select>
<select id="selectSimilarUsers" resultMap="BaseResultMap">
SELECT * FROM user_similarity
WHERE user1_id = #{userId} OR user2_id = #{userId}
ORDER BY similarity_score DESC
LIMIT #{limit}
</select>
</mapper>

View File

@@ -0,0 +1,70 @@
// Cookie工具类
const CookieUtils = {
/**
* 设置Cookie
* @param {string} name Cookie名称
* @param {string} value Cookie值
* @param {number} days 过期天数默认7天
*/
set(name, value, days = 7) {
const expires = new Date();
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
document.cookie = `${name}=${encodeURIComponent(value)};expires=${expires.toUTCString()};path=/`;
},
/**
* 获取Cookie
* @param {string} name Cookie名称
* @returns {string|null} Cookie值不存在返回null
*/
get(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) {
return decodeURIComponent(c.substring(nameEQ.length, c.length));
}
}
return null;
},
/**
* 删除Cookie
* @param {string} name Cookie名称
*/
remove(name) {
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;`;
},
/**
* 获取JSON格式的Cookie
* @param {string} name Cookie名称
* @returns {any|null} 解析后的JSON对象不存在或解析失败返回null
*/
getJSON(name) {
const value = this.get(name);
if (!value) return null;
try {
return JSON.parse(value);
} catch (e) {
console.error(`Failed to parse cookie ${name}:`, e);
return null;
}
},
/**
* 设置JSON格式的Cookie
* @param {string} name Cookie名称
* @param {any} value 要存储的对象
* @param {number} days 过期天数默认7天
*/
setJSON(name, value, days = 7) {
try {
this.set(name, JSON.stringify(value), days);
} catch (e) {
console.error(`Failed to stringify cookie ${name}:`, e);
}
}
};

View File

@@ -0,0 +1,356 @@
/**
* 自定义模态框组件
* 符合项目UI设计规范具有平滑过渡动画和响应式显示
*/
class CustomModal {
constructor(options = {}) {
this.id = options.id || 'customModal_' + Date.now();
this.title = options.title || '';
this.content = options.content || '';
this.size = options.size || 'medium'; // small, medium, large
this.showClose = options.showClose !== false;
this.onClose = options.onClose || null;
this.onConfirm = options.onConfirm || null;
this.confirmText = options.confirmText || '确定';
this.cancelText = options.cancelText || '取消';
this.showFooter = options.showFooter !== false;
this.backdrop = options.backdrop !== false;
this.modal = null;
}
show() {
// 创建模态框HTML
const modalHtml = this._createModalHtml();
// 移除已存在的模态框
const existing = document.getElementById(this.id);
if (existing) {
existing.remove();
}
// 添加到body
document.body.insertAdjacentHTML('beforeend', modalHtml);
this.modal = document.getElementById(this.id);
this.backdropEl = document.getElementById(this.id + '_backdrop');
// 添加显示动画
requestAnimationFrame(() => {
this.modal.classList.add('show');
if (this.backdropEl) {
this.backdropEl.classList.add('show');
}
document.body.style.overflow = 'hidden';
});
// 绑定事件
this._bindEvents();
}
hide() {
if (!this.modal) return;
this.modal.classList.remove('show');
if (this.backdropEl) {
this.backdropEl.classList.remove('show');
}
document.body.style.overflow = '';
// 动画结束后移除
setTimeout(() => {
if (this.modal && this.modal.parentNode) {
this.modal.remove();
}
if (this.backdropEl && this.backdropEl.parentNode) {
this.backdropEl.remove();
}
if (this.onClose) {
this.onClose();
}
}, 300);
}
_createModalHtml() {
const sizeClass = {
small: 'modal-sm',
medium: '',
large: 'modal-lg'
}[this.size];
const backdropHtml = this.backdrop ?
`<div class="custom-modal-backdrop" id="${this.id}_backdrop"></div>` : '';
return backdropHtml + `
<div class="custom-modal" id="${this.id}">
<div class="custom-modal-dialog ${sizeClass}">
<div class="custom-modal-content">
${this.title || this.showClose ? `
<div class="custom-modal-header">
${this.title ? `<h5 class="custom-modal-title">${this.title}</h5>` : ''}
${this.showClose ? `<button type="button" class="custom-modal-close" data-dismiss="modal">
<i class="bi bi-x-lg"></i>
</button>` : ''}
</div>
` : ''}
<div class="custom-modal-body">
${this.content}
</div>
${this.showFooter ? `
<div class="custom-modal-footer">
<button type="button" class="custom-modal-btn custom-modal-btn-cancel" data-dismiss="modal">
${this.cancelText}
</button>
${this.onConfirm ? `
<button type="button" class="custom-modal-btn custom-modal-btn-confirm">
${this.confirmText}
</button>
` : ''}
</div>
` : ''}
</div>
</div>
</div>
`;
}
_bindEvents() {
// 关闭按钮
const closeBtn = this.modal.querySelector('[data-dismiss="modal"]');
if (closeBtn) {
closeBtn.addEventListener('click', () => this.hide());
}
// 确认按钮
const confirmBtn = this.modal.querySelector('.custom-modal-btn-confirm');
if (confirmBtn && this.onConfirm) {
confirmBtn.addEventListener('click', () => {
if (this.onConfirm() !== false) {
this.hide();
}
});
}
// 点击背景关闭
if (this.backdropEl) {
this.backdropEl.addEventListener('click', () => this.hide());
}
// 点击内容区域不关闭
const content = this.modal.querySelector('.custom-modal-content');
if (content) {
content.addEventListener('click', (e) => e.stopPropagation());
}
}
updateContent(content) {
const body = this.modal?.querySelector('.custom-modal-body');
if (body) {
body.innerHTML = content;
}
}
}
// CSS样式通过JavaScript注入或单独引入CSS文件
const customModalStyles = `
<style id="custom-modal-styles">
.custom-modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1040;
opacity: 0;
transition: opacity 0.3s ease;
backdrop-filter: blur(4px);
}
.custom-modal-backdrop.show {
opacity: 1;
}
.custom-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1050;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.custom-modal.show {
opacity: 1;
pointer-events: auto;
}
.custom-modal-dialog {
width: 100%;
max-width: 500px;
margin: auto;
transform: scale(0.9) translateY(-20px);
transition: transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
}
.custom-modal.show .custom-modal-dialog {
transform: scale(1) translateY(0);
}
.custom-modal-dialog.modal-sm {
max-width: 400px;
}
.custom-modal-dialog.modal-lg {
max-width: 800px;
}
.custom-modal-content {
background: #fff;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
max-height: 90vh;
display: flex;
flex-direction: column;
}
.custom-modal-header {
padding: 20px 24px;
border-bottom: 1px solid #f1f5f9;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.custom-modal-title {
font-size: 18px;
font-weight: 700;
color: #0f172a;
margin: 0;
}
.custom-modal-close {
background: none;
border: none;
font-size: 20px;
color: #94a3b8;
cursor: pointer;
padding: 4px;
line-height: 1;
transition: color 0.2s;
}
.custom-modal-close:hover {
color: #64748b;
}
.custom-modal-body {
padding: 24px;
overflow-y: auto;
flex: 1;
color: #334155;
font-size: 14px;
line-height: 1.6;
}
.custom-modal-footer {
padding: 16px 24px;
border-top: 1px solid #f1f5f9;
display: flex;
justify-content: flex-end;
gap: 12px;
flex-shrink: 0;
}
.custom-modal-btn {
padding: 10px 24px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.custom-modal-btn-cancel {
background: #f8fafc;
color: #64748b;
}
.custom-modal-btn-cancel:hover {
background: #f1f5f9;
}
.custom-modal-btn-confirm {
background: #0f172a;
color: #fff;
}
.custom-modal-btn-confirm:hover {
background: #1e293b;
transform: translateY(-1px);
}
.custom-modal-btn-confirm:active {
transform: translateY(0);
}
/* 响应式 */
@media (max-width: 576px) {
.custom-modal-dialog {
max-width: 100%;
margin: 0;
}
.custom-modal {
padding: 0;
align-items: flex-end;
}
.custom-modal-dialog {
transform: translateY(100%);
}
.custom-modal.show .custom-modal-dialog {
transform: translateY(0);
}
.custom-modal-content {
border-radius: 20px 20px 0 0;
max-height: 80vh;
}
}
</style>
`;
// 注入样式
if (!document.getElementById('custom-modal-styles')) {
document.head.insertAdjacentHTML('beforeend', customModalStyles);
}
// 导出到全局
window.CustomModal = CustomModal;
// 便捷方法
window.showCustomModal = function(options) {
const modal = new CustomModal(options);
modal.show();
return modal;
};
window.showCustomConfirm = function(message, title = '确认操作', onConfirm = null) {
return new CustomModal({
title: title,
content: `<p style="margin: 0;">${message}</p>`,
onConfirm: onConfirm,
showFooter: true
});
};

View File

@@ -0,0 +1,338 @@
/**
* 商家后台通用脚本
* 提供侧边栏、加载状态、交互反馈等功能
*/
// 侧边栏HTML模板
function getSidebarHTML(activePage) {
const menuItems = [
{ href: '/merchant/dashboard.html', icon: 'bi-speedometer2', text: '首页', page: 'dashboard' },
{ href: '/merchant/products.html', icon: 'bi-box-seam', text: '商品管理', page: 'products' },
{ href: '/merchant/orders.html', icon: 'bi-receipt-cutoff', text: '订单管理', page: 'orders' },
{ href: '/merchant/inventory.html', icon: 'bi-archive', text: '库存管理', page: 'inventory' },
{ href: '/merchant/statistics.html', icon: 'bi-bar-chart', text: '数据统计', page: 'statistics' },
{ href: '/merchant/announcements.html', icon: 'bi-megaphone', text: '公告管理', page: 'announcements' },
{ href: '/merchant/messages.html', icon: 'bi-chat-dots', text: '留言管理', page: 'messages' },
{ href: '/merchant/reviews.html', icon: 'bi-star', text: '评价管理', page: 'reviews' },
{ href: '/merchant/users.html', icon: 'bi-people', text: '用户管理', page: 'users' }
];
const menuHTML = menuItems.map(item => {
const activeClass = item.page === activePage ? 'active' : '';
return `
<li class="nav-item">
<a class="nav-link ${activeClass}" href="${item.href}" data-page="${item.page}">
<i class="bi ${item.icon}"></i>
<span class="nav-text">${item.text}</span>
</a>
</li>
`;
}).join('');
return `
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="d-flex align-items-center">
<i class="bi bi-shop fs-4 me-2"></i>
<span class="sidebar-brand">商家后台</span>
</div>
<button class="btn btn-link text-white p-0 d-md-none" id="sidebarCloseBtn">
<i class="bi bi-x-lg"></i>
</button>
</div>
<nav class="sidebar-nav">
<ul class="nav flex-column">
${menuHTML}
</ul>
</nav>
<div class="sidebar-footer">
<a href="/merchant/logout" class="btn btn-outline-light w-100">
<i class="bi bi-box-arrow-right"></i>
<span class="ms-2">退出登录</span>
</a>
</div>
</div>
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<nav class="top-navbar d-md-none">
<div class="container-fluid d-flex align-items-center">
<button class="btn btn-link text-white p-0" id="sidebarToggleBtn">
<i class="bi bi-list fs-4"></i>
</button>
<span class="ms-3 text-white fw-bold">商家后台</span>
</div>
</nav>
`;
}
// 侧边栏CSS样式
const sidebarCSS = `
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 260px;
height: 100vh;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
color: #fff;
z-index: 1050;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 600;
}
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 1rem 0;
}
.sidebar-nav .nav-link {
color: rgba(255, 255, 255, 0.8);
padding: 0.75rem 1.5rem;
transition: all 0.3s;
border-left: 3px solid transparent;
display: flex;
align-items: center;
text-decoration: none;
}
.sidebar-nav .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
border-left-color: #0d6efd;
}
.sidebar-nav .nav-link.active {
background-color: rgba(13, 110, 253, 0.2);
color: #fff;
border-left-color: #0d6efd;
font-weight: 600;
}
.sidebar-nav .nav-link i {
width: 24px;
font-size: 1.1rem;
margin-right: 0.75rem;
}
.sidebar-nav .nav-text {
flex: 1;
}
.sidebar-footer {
padding: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
@media (max-width: 767.98px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.show {
transform: translateX(0);
}
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1049;
display: none;
transition: opacity 0.3s;
}
.sidebar-overlay.show {
display: block;
}
.top-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
z-index: 1048;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
body {
padding-top: 56px;
}
}
@media (min-width: 768px) {
.main-content {
margin-left: 260px;
min-height: 100vh;
}
}
.sidebar-nav::-webkit-scrollbar {
width: 6px;
}
.sidebar-nav::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.sidebar-nav::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.sidebar-nav::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
`;
// 初始化侧边栏
function initSidebar(activePage) {
// 添加CSS样式
if (!document.getElementById('merchant-sidebar-styles')) {
const style = document.createElement('style');
style.id = 'merchant-sidebar-styles';
style.textContent = sidebarCSS;
document.head.appendChild(style);
}
// 替换导航栏
const oldNav = document.querySelector('nav.navbar');
if (oldNav) {
oldNav.outerHTML = getSidebarHTML(activePage);
// 初始化交互
const sidebar = document.getElementById('sidebar');
const sidebarOverlay = document.getElementById('sidebarOverlay');
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
const sidebarCloseBtn = document.getElementById('sidebarCloseBtn');
if (sidebarToggleBtn) {
sidebarToggleBtn.addEventListener('click', function() {
sidebar.classList.add('show');
sidebarOverlay.classList.add('show');
document.body.style.overflow = 'hidden';
});
}
function closeSidebar() {
sidebar.classList.remove('show');
sidebarOverlay.classList.remove('show');
document.body.style.overflow = '';
}
if (sidebarCloseBtn) {
sidebarCloseBtn.addEventListener('click', closeSidebar);
}
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', closeSidebar);
}
const navLinks = document.querySelectorAll('.sidebar-nav .nav-link');
navLinks.forEach(link => {
link.addEventListener('click', function() {
if (window.innerWidth < 768) {
closeSidebar();
}
});
});
}
// 包装主内容区域
const container = document.querySelector('.container, .container-fluid');
if (container && !container.closest('.main-content')) {
const mainContent = document.createElement('div');
mainContent.className = 'main-content';
container.parentNode.insertBefore(mainContent, container);
mainContent.appendChild(container);
}
}
// 显示加载状态
function showLoading(elementId, message = '加载中...') {
const element = document.getElementById(elementId);
if (element) {
element.innerHTML = `
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">${message}</span>
</div>
<p class="text-muted mt-3">${message}</p>
</div>
`;
}
}
// 显示成功提示
function showSuccess(message, duration = 3000) {
const toast = document.createElement('div');
toast.className = 'toast align-items-center text-white bg-success border-0';
toast.setAttribute('role', 'alert');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
const toastContainer = document.getElementById('toastContainer') || createToastContainer();
toastContainer.appendChild(toast);
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
toast.addEventListener('hidden.bs.toast', () => toast.remove());
}
// 显示错误提示
function showError(message, duration = 3000) {
const toast = document.createElement('div');
toast.className = 'toast align-items-center text-white bg-danger border-0';
toast.setAttribute('role', 'alert');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
const toastContainer = document.getElementById('toastContainer') || createToastContainer();
toastContainer.appendChild(toast);
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
toast.addEventListener('hidden.bs.toast', () => toast.remove());
}
// 创建Toast容器
function createToastContainer() {
const container = document.createElement('div');
container.id = 'toastContainer';
container.className = 'toast-container position-fixed top-0 end-0 p-3';
container.style.zIndex = '1060';
document.body.appendChild(container);
return container;
}
// 检测当前页面
function detectCurrentPage() {
const path = window.location.pathname;
if (path.includes('products')) return 'products';
if (path.includes('orders')) return 'orders';
if (path.includes('inventory')) return 'inventory';
if (path.includes('statistics')) return 'statistics';
if (path.includes('announcements')) return 'announcements';
if (path.includes('messages')) return 'messages';
if (path.includes('reviews')) return 'reviews';
if (path.includes('users')) return 'users';
return 'dashboard';
}
// 自动初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
initSidebar(detectCurrentPage());
});
} else {
initSidebar(detectCurrentPage());
}

View File

@@ -0,0 +1,297 @@
/**
* 商家后台侧边栏通用脚本
* 提供侧边栏的HTML结构和交互逻辑
*/
// 生成侧边栏HTML
function generateSidebarHTML(currentPage) {
const menuItems = [
{ href: '/merchant/dashboard.html', icon: 'bi-speedometer2', text: '首页', page: 'dashboard' },
{ href: '/merchant/products.html', icon: 'bi-box-seam', text: '商品管理', page: 'products' },
{ href: '/merchant/orders.html', icon: 'bi-receipt-cutoff', text: '订单管理', page: 'orders' },
{ href: '/merchant/inventory.html', icon: 'bi-archive', text: '库存管理', page: 'inventory' },
{ href: '/merchant/statistics.html', icon: 'bi-bar-chart', text: '数据统计', page: 'statistics' },
{ href: '/merchant/announcements.html', icon: 'bi-megaphone', text: '公告管理', page: 'announcements' },
{ href: '/merchant/messages.html', icon: 'bi-chat-dots', text: '留言管理', page: 'messages' },
{ href: '/merchant/reviews.html', icon: 'bi-star', text: '评价管理', page: 'reviews' },
{ href: '/merchant/users.html', icon: 'bi-people', text: '用户管理', page: 'users' }
];
const menuHTML = menuItems.map(item => {
const activeClass = item.page === currentPage ? 'active' : '';
return `
<li class="nav-item">
<a class="nav-link ${activeClass}" href="${item.href}" data-page="${item.page}">
<i class="bi ${item.icon}"></i>
<span class="nav-text">${item.text}</span>
</a>
</li>
`;
}).join('');
return `
<!-- 侧边栏导航 -->
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="d-flex align-items-center">
<i class="bi bi-shop fs-4 me-2"></i>
<span class="sidebar-brand">商家后台</span>
</div>
<button class="btn btn-link text-white p-0 d-md-none" id="sidebarCloseBtn">
<i class="bi bi-x-lg"></i>
</button>
</div>
<nav class="sidebar-nav">
<ul class="nav flex-column">
${menuHTML}
</ul>
</nav>
<div class="sidebar-footer">
<a href="/merchant/logout" class="btn btn-outline-light w-100">
<i class="bi bi-box-arrow-right"></i>
<span class="ms-2">退出登录</span>
</a>
</div>
</div>
<!-- 侧边栏遮罩层(移动端) -->
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<!-- 顶部导航栏(移动端) -->
<nav class="top-navbar d-md-none">
<div class="container-fluid d-flex align-items-center">
<button class="btn btn-link text-white p-0" id="sidebarToggleBtn">
<i class="bi bi-list fs-4"></i>
</button>
<span class="ms-3 text-white fw-bold">商家后台</span>
</div>
</nav>
`;
}
// 添加侧边栏CSS样式
function addSidebarStyles() {
if (document.getElementById('sidebar-styles')) return;
const style = document.createElement('style');
style.id = 'sidebar-styles';
style.textContent = `
/* 侧边栏样式 */
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 260px;
height: 100vh;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
color: #fff;
z-index: 1050;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 600;
}
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 1rem 0;
}
.sidebar-nav .nav-link {
color: rgba(255, 255, 255, 0.8);
padding: 0.75rem 1.5rem;
transition: all 0.3s;
border-left: 3px solid transparent;
display: flex;
align-items: center;
text-decoration: none;
}
.sidebar-nav .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
border-left-color: #0d6efd;
}
.sidebar-nav .nav-link.active {
background-color: rgba(13, 110, 253, 0.2);
color: #fff;
border-left-color: #0d6efd;
font-weight: 600;
}
.sidebar-nav .nav-link i {
width: 24px;
font-size: 1.1rem;
margin-right: 0.75rem;
}
.sidebar-nav .nav-text {
flex: 1;
}
.sidebar-footer {
padding: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
/* 移动端样式 */
@media (max-width: 767.98px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.show {
transform: translateX(0);
}
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1049;
display: none;
transition: opacity 0.3s;
}
.sidebar-overlay.show {
display: block;
}
.top-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
z-index: 1048;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
body {
padding-top: 56px;
}
}
/* 桌面端主内容区域 */
@media (min-width: 768px) {
.main-content {
margin-left: 260px;
min-height: 100vh;
}
}
/* 滚动条样式 */
.sidebar-nav::-webkit-scrollbar {
width: 6px;
}
.sidebar-nav::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.sidebar-nav::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.sidebar-nav::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
`;
document.head.appendChild(style);
}
// 初始化侧边栏交互
function initSidebar() {
const sidebar = document.getElementById('sidebar');
const sidebarOverlay = document.getElementById('sidebarOverlay');
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
const sidebarCloseBtn = document.getElementById('sidebarCloseBtn');
if (!sidebar) return;
// 移动端切换侧边栏
if (sidebarToggleBtn) {
sidebarToggleBtn.addEventListener('click', function() {
sidebar.classList.add('show');
if (sidebarOverlay) sidebarOverlay.classList.add('show');
document.body.style.overflow = 'hidden';
});
}
// 关闭侧边栏
function closeSidebar() {
sidebar.classList.remove('show');
if (sidebarOverlay) sidebarOverlay.classList.remove('show');
document.body.style.overflow = '';
}
if (sidebarCloseBtn) {
sidebarCloseBtn.addEventListener('click', closeSidebar);
}
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', closeSidebar);
}
// 点击导航项时,移动端自动关闭侧边栏
const navLinks = document.querySelectorAll('.sidebar-nav .nav-link');
navLinks.forEach(link => {
link.addEventListener('click', function() {
if (window.innerWidth < 768) {
closeSidebar();
}
});
});
}
// 初始化侧边栏(自动检测当前页面)
function initMerchantSidebar() {
addSidebarStyles();
// 检测当前页面
const path = window.location.pathname;
let currentPage = 'dashboard';
if (path.includes('products')) currentPage = 'products';
else if (path.includes('orders')) currentPage = 'orders';
else if (path.includes('inventory')) currentPage = 'inventory';
else if (path.includes('statistics')) currentPage = 'statistics';
else if (path.includes('announcements')) currentPage = 'announcements';
else if (path.includes('messages')) currentPage = 'messages';
else if (path.includes('reviews')) currentPage = 'reviews';
else if (path.includes('users')) currentPage = 'users';
// 查找并替换旧的导航栏
const oldNav = document.querySelector('nav.navbar');
if (oldNav) {
oldNav.outerHTML = generateSidebarHTML(currentPage);
initSidebar();
}
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initMerchantSidebar);
} else {
initMerchantSidebar();
}

View File

@@ -0,0 +1,300 @@
/**
* 侧边栏自动替换脚本
* 自动检测并替换旧的导航栏为侧边栏
*/
(function() {
'use strict';
// 侧边栏CSS
const sidebarCSS = `
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 260px;
height: 100vh;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
color: #fff;
z-index: 1000;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 600;
}
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 1rem 0;
}
.sidebar-nav .nav-link {
color: rgba(255, 255, 255, 0.8);
padding: 0.75rem 1.5rem;
transition: all 0.3s;
border-left: 3px solid transparent;
display: flex;
align-items: center;
text-decoration: none;
}
.sidebar-nav .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
border-left-color: #0d6efd;
}
.sidebar-nav .nav-link.active {
background-color: rgba(13, 110, 253, 0.2);
color: #fff;
border-left-color: #0d6efd;
font-weight: 600;
}
.sidebar-nav .nav-link i {
width: 24px;
font-size: 1.1rem;
margin-right: 0.75rem;
}
.sidebar-nav .nav-text {
flex: 1;
}
.sidebar-footer {
padding: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
@media (max-width: 767.98px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.show {
transform: translateX(0);
}
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1049;
display: none;
transition: opacity 0.3s;
}
.sidebar-overlay.show {
display: block;
}
.top-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
z-index: 1048;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
body {
padding-top: 56px;
}
}
@media (min-width: 768px) {
.main-content {
margin-left: 260px;
min-height: 100vh;
position: relative;
z-index: 1;
width: calc(100% - 260px);
}
}
.sidebar-nav::-webkit-scrollbar {
width: 6px;
}
.sidebar-nav::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.sidebar-nav::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.sidebar-nav::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
`;
// 菜单配置
const menuConfig = [
{ href: '/merchant/dashboard.html', icon: 'bi-speedometer2', text: '首页', page: 'dashboard' },
{ href: '/merchant/products.html', icon: 'bi-box-seam', text: '商品管理', page: 'products' },
{ href: '/merchant/orders.html', icon: 'bi-receipt-cutoff', text: '订单管理', page: 'orders' },
{ href: '/merchant/inventory.html', icon: 'bi-archive', text: '库存管理', page: 'inventory' },
{ href: '/merchant/statistics.html', icon: 'bi-bar-chart-line', text: '数据统计', page: 'statistics' },
{ href: '/merchant/announcements.html', icon: 'bi-megaphone', text: '公告管理', page: 'announcements' },
{ href: '/merchant/messages.html', icon: 'bi-chat-dots', text: '留言管理', page: 'messages' },
{ href: '/merchant/reviews.html', icon: 'bi-star', text: '评价管理', page: 'reviews' },
{ href: '/merchant/users.html', icon: 'bi-people', text: '用户管理', page: 'users' }
];
// 检测当前页面
function detectCurrentPage() {
const path = window.location.pathname;
for (const item of menuConfig) {
if (path.includes(item.page)) {
return item.page;
}
}
return 'dashboard';
}
// 生成侧边栏HTML
function generateSidebar(activePage) {
const menuHTML = menuConfig.map(item => {
const activeClass = item.page === activePage ? 'active' : '';
return `
<li class="nav-item">
<a class="nav-link ${activeClass}" href="${item.href}" data-page="${item.page}">
<i class="bi ${item.icon}"></i>
<span class="nav-text">${item.text}</span>
</a>
</li>
`;
}).join('');
return `
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="d-flex align-items-center">
<i class="bi bi-shop fs-4 me-2"></i>
<span class="sidebar-brand">商家后台</span>
</div>
<button class="btn btn-link text-white p-0 d-md-none" id="sidebarCloseBtn">
<i class="bi bi-x-lg"></i>
</button>
</div>
<nav class="sidebar-nav">
<ul class="nav flex-column">
${menuHTML}
</ul>
</nav>
<div class="sidebar-footer">
<a href="/merchant/logout" class="btn btn-outline-light w-100">
<i class="bi bi-box-arrow-right"></i>
<span class="ms-2">退出登录</span>
</a>
</div>
</div>
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<nav class="top-navbar d-md-none">
<div class="container-fluid d-flex align-items-center">
<button class="btn btn-link text-white p-0" id="sidebarToggleBtn">
<i class="bi bi-list fs-4"></i>
</button>
<span class="ms-3 text-white fw-bold">商家后台</span>
</div>
</nav>
`;
}
// 初始化侧边栏
function initSidebar() {
// 添加CSS
if (!document.getElementById('merchant-sidebar-styles')) {
const style = document.createElement('style');
style.id = 'merchant-sidebar-styles';
style.textContent = sidebarCSS;
document.head.appendChild(style);
}
// 替换导航栏
const oldNav = document.querySelector('nav.navbar');
if (oldNav && !document.getElementById('sidebar')) {
const activePage = detectCurrentPage();
oldNav.outerHTML = generateSidebar(activePage);
// 包装主内容 - 确保不重复包装
let mainContent = document.querySelector('.main-content');
if (!mainContent) {
// 查找body的直接子元素中的container
const bodyChildren = Array.from(document.body.children);
let container = null;
// 优先查找container-fluid然后是container
for (const child of bodyChildren) {
if (child.classList.contains('container-fluid') || child.classList.contains('container')) {
if (!child.closest('.main-content') && !child.closest('.sidebar') && !child.closest('.sidebar-overlay') && !child.closest('.top-navbar')) {
container = child;
break;
}
}
}
if (container) {
mainContent = document.createElement('div');
mainContent.className = 'main-content';
container.parentNode.insertBefore(mainContent, container);
mainContent.appendChild(container);
}
}
// 初始化交互
setupSidebarInteractions();
}
}
// 设置侧边栏交互
function setupSidebarInteractions() {
const sidebar = document.getElementById('sidebar');
const sidebarOverlay = document.getElementById('sidebarOverlay');
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
const sidebarCloseBtn = document.getElementById('sidebarCloseBtn');
if (!sidebar) return;
function openSidebar() {
sidebar.classList.add('show');
if (sidebarOverlay) sidebarOverlay.classList.add('show');
document.body.style.overflow = 'hidden';
}
function closeSidebar() {
sidebar.classList.remove('show');
if (sidebarOverlay) sidebarOverlay.classList.remove('show');
document.body.style.overflow = '';
}
if (sidebarToggleBtn) {
sidebarToggleBtn.addEventListener('click', openSidebar);
}
if (sidebarCloseBtn) {
sidebarCloseBtn.addEventListener('click', closeSidebar);
}
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', closeSidebar);
}
// 点击导航项时,移动端自动关闭
const navLinks = document.querySelectorAll('.sidebar-nav .nav-link');
navLinks.forEach(link => {
link.addEventListener('click', function() {
if (window.innerWidth < 768) {
closeSidebar();
}
});
});
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSidebar);
} else {
initSidebar();
}
})();

View File

@@ -0,0 +1,289 @@
/**
* UI交互增强脚本
* 提供加载状态、视觉反馈、操作提示等功能
*/
// 创建Toast容器
function ensureToastContainer() {
let container = document.getElementById('toastContainer');
if (!container) {
container = document.createElement('div');
container.id = 'toastContainer';
container.className = 'toast-container position-fixed top-0 end-0 p-3';
container.style.zIndex = '1060';
document.body.appendChild(container);
}
return container;
}
// 显示成功提示
function showSuccess(message, duration = 3000) {
const container = ensureToastContainer();
const toast = document.createElement('div');
toast.className = 'toast align-items-center text-white bg-success border-0';
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
<i class="bi bi-check-circle me-2"></i>${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
container.appendChild(toast);
const bsToast = new bootstrap.Toast(toast, { delay: duration });
bsToast.show();
toast.addEventListener('hidden.bs.toast', () => toast.remove());
}
// 显示错误提示
function showError(message, duration = 4000) {
const container = ensureToastContainer();
const toast = document.createElement('div');
toast.className = 'toast align-items-center text-white bg-danger border-0';
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
<i class="bi bi-exclamation-circle me-2"></i>${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
container.appendChild(toast);
const bsToast = new bootstrap.Toast(toast, { delay: duration });
bsToast.show();
toast.addEventListener('hidden.bs.toast', () => toast.remove());
}
// 显示警告提示
function showWarning(message, duration = 3000) {
const container = ensureToastContainer();
const toast = document.createElement('div');
toast.className = 'toast align-items-center text-white bg-warning border-0';
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
<i class="bi bi-exclamation-triangle me-2"></i>${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
container.appendChild(toast);
const bsToast = new bootstrap.Toast(toast, { delay: duration });
bsToast.show();
toast.addEventListener('hidden.bs.toast', () => toast.remove());
}
// 显示信息提示
function showInfo(message, duration = 3000) {
const container = ensureToastContainer();
const toast = document.createElement('div');
toast.className = 'toast align-items-center text-white bg-info border-0';
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
<i class="bi bi-info-circle me-2"></i>${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
container.appendChild(toast);
const bsToast = new bootstrap.Toast(toast, { delay: duration });
bsToast.show();
toast.addEventListener('hidden.bs.toast', () => toast.remove());
}
// 显示加载状态
function showLoading(elementId, message = '加载中...') {
const element = document.getElementById(elementId);
if (element) {
element.innerHTML = `
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">${message}</span>
</div>
<p class="text-muted mt-3">${message}</p>
</div>
`;
}
}
// 按钮加载状态
function setButtonLoading(button, loading = true, originalText = null) {
if (loading) {
if (!button.dataset.originalText) {
button.dataset.originalText = button.innerHTML;
}
button.disabled = true;
button.innerHTML = `
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
处理中...
`;
} else {
button.disabled = false;
button.innerHTML = button.dataset.originalText || originalText || '提交';
delete button.dataset.originalText;
}
}
// 增强的fetch函数带加载状态和错误处理
async function enhancedFetch(url, options = {}) {
const { showLoading: showLoadingId, button: loadingButton, ...fetchOptions } = options;
// 显示加载状态
if (showLoadingId) {
showLoading(showLoadingId);
}
// 按钮加载状态
if (loadingButton) {
setButtonLoading(loadingButton, true);
}
try {
const response = await fetch(url, fetchOptions);
const data = await response.json();
// 恢复按钮状态
if (loadingButton) {
setButtonLoading(loadingButton, false);
}
return { response, data };
} catch (error) {
// 恢复按钮状态
if (loadingButton) {
setButtonLoading(loadingButton, false);
}
showError('网络请求失败,请检查网络连接');
throw error;
}
}
// 确认对话框(使用自定义模态框)
function confirmAction(message, title = '确认操作', confirmText = '确定', cancelText = '取消') {
return new Promise((resolve) => {
// 确保自定义模态框已加载
if (typeof CustomModal === 'undefined') {
console.error('CustomModal未加载请先引入custom-modal.js');
// 降级到原生confirm
resolve(confirm(message));
return;
}
const modal = new CustomModal({
title: title,
content: `<p style="margin: 0;">${message}</p>`,
confirmText: confirmText,
cancelText: cancelText,
onConfirm: () => {
resolve(true);
return true;
},
onClose: () => {
resolve(false);
}
});
modal.show();
});
}
// 表单验证增强
function validateForm(formId) {
const form = document.getElementById(formId);
if (!form) return false;
const requiredFields = form.querySelectorAll('[required]');
let isValid = true;
requiredFields.forEach(field => {
if (!field.value.trim()) {
field.classList.add('is-invalid');
isValid = false;
} else {
field.classList.remove('is-invalid');
field.classList.add('is-valid');
}
});
return isValid;
}
// 数字格式化
function formatNumber(num, decimals = 2) {
return parseFloat(num || 0).toFixed(decimals);
}
// 日期格式化
function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
if (!date) return '-';
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
const seconds = String(d.getSeconds()).padStart(2, '0');
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds);
}
// 防抖函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 节流函数
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 导出到全局
window.UIEnhancements = {
showSuccess,
showError,
showWarning,
showInfo,
showLoading,
setButtonLoading,
enhancedFetch,
confirmAction,
validateForm,
formatNumber,
formatDate,
debounce,
throttle
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View File

@@ -0,0 +1,291 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>公告管理 - 商家后台</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/merchant/dashboard.html">
<i class="bi bi-shop"></i> 商家后台
</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/merchant/dashboard.html">首页</a>
<a class="nav-link" href="/merchant/products.html">商品管理</a>
<a class="nav-link" href="/merchant/orders.html">订单管理</a>
<a class="nav-link" href="/merchant/inventory.html">库存管理</a>
<a class="nav-link" href="/merchant/statistics.html">数据统计</a>
<a class="nav-link active" href="/merchant/announcements.html">公告管理</a>
<a class="nav-link" href="/merchant/messages.html">留言管理</a>
<a class="nav-link" href="/merchant/reviews.html">评价管理</a>
<a class="nav-link" href="/merchant/users.html">用户管理</a>
</div>
</div>
</nav>
<div class="container mt-4 mb-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-megaphone"></i> 公告管理</h2>
<button class="btn btn-primary" onclick="showAddAnnouncementModal()">
<i class="bi bi-plus-circle"></i> 新增公告
</button>
</div>
<div class="card shadow-sm">
<div class="card-body">
<div id="announcementsList"></div>
</div>
</div>
</div>
<!-- 添加/编辑公告模态框 -->
<div class="modal fade" id="announcementModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="announcementModalTitle">新增公告</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="announcementForm">
<input type="hidden" id="announcementId">
<div class="mb-3">
<label for="title" class="form-label">标题 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="title" required>
</div>
<div class="mb-3">
<label for="type" class="form-label">类型</label>
<select class="form-control" id="type">
<option value="ACTIVITY">活动</option>
<option value="NOTICE">通知</option>
</select>
</div>
<div class="mb-3">
<label for="content" class="form-label">内容 <span class="text-danger">*</span></label>
<textarea class="form-control" id="content" rows="5" required></textarea>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="status" checked>
<label class="form-check-label" for="status">
发布
</label>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveAnnouncement()">保存</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
// 加载公告列表
async function loadAnnouncements() {
try {
const response = await fetch('/merchant/announcements', {
credentials: 'include'
});
const data = await response.json();
const container = document.getElementById('announcementsList');
if (data.code === 200 && data.data) {
const announcements = data.data || [];
if (announcements.length === 0) {
container.innerHTML = '<div class="alert alert-info">暂无公告,请点击"新增公告"按钮添加</div>';
return;
}
container.innerHTML = `
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>标题</th>
<th>类型</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${announcements.map(announcement => `
<tr>
<td><strong>${announcement.title || ''}</strong></td>
<td><span class="badge ${announcement.type === 'ACTIVITY' ? 'bg-warning' : 'bg-info'}">${announcement.type === 'ACTIVITY' ? '活动' : '通知'}</span></td>
<td>
<span class="badge ${announcement.status === 1 ? 'bg-success' : 'bg-secondary'}">
${announcement.status === 1 ? '已发布' : '已下架'}
</span>
</td>
<td>${announcement.createTime ? new Date(announcement.createTime).toLocaleString('zh-CN') : '-'}</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="editAnnouncement(${announcement.id})" title="编辑">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-outline-danger" onclick="deleteAnnouncement(${announcement.id})" title="删除">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
} else {
container.innerHTML = '<div class="alert alert-warning">加载公告列表失败:' + (data.message || '未知错误') + '</div>';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('announcementsList').innerHTML = '<div class="alert alert-danger">加载公告列表失败,请刷新页面重试</div>';
}
}
// 显示添加公告模态框
function showAddAnnouncementModal() {
document.getElementById('announcementModalTitle').textContent = '新增公告';
document.getElementById('announcementId').value = '';
document.getElementById('announcementForm').reset();
document.getElementById('status').checked = true;
const modal = new bootstrap.Modal(document.getElementById('announcementModal'));
modal.show();
}
// 编辑公告
async function editAnnouncement(announcementId) {
try {
const response = await fetch(`/merchant/announcements/${announcementId}`, {
credentials: 'include'
});
const data = await response.json();
if (data.code === 200 && data.data) {
const announcement = data.data;
document.getElementById('announcementModalTitle').textContent = '编辑公告';
document.getElementById('announcementId').value = announcement.id;
document.getElementById('title').value = announcement.title || '';
document.getElementById('type').value = announcement.type || 'NOTICE';
document.getElementById('content').value = announcement.content || '';
document.getElementById('status').checked = announcement.status === 1;
const modal = new bootstrap.Modal(document.getElementById('announcementModal'));
modal.show();
} else {
alert('加载公告信息失败');
}
} catch (error) {
console.error('Error:', error);
alert('加载公告信息失败');
}
}
// 保存公告
async function saveAnnouncement() {
const announcementId = document.getElementById('announcementId').value;
const announcementData = {
title: document.getElementById('title').value,
type: document.getElementById('type').value,
content: document.getElementById('content').value,
status: document.getElementById('status').checked ? 1 : 0
};
if (!announcementData.title || !announcementData.content) {
UIEnhancements.showWarning('请填写完整信息');
return;
}
const saveBtn = document.querySelector('#announcementModal .btn-primary');
try {
const url = announcementId ? `/merchant/announcements/${announcementId}` : '/merchant/announcements';
const method = announcementId ? 'PUT' : 'POST';
const { data } = await UIEnhancements.enhancedFetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(announcementData),
button: saveBtn
});
if (data.code === 200) {
UIEnhancements.showSuccess(announcementId ? '公告已更新' : '公告已发布');
const modal = bootstrap.Modal.getInstance(document.getElementById('announcementModal'));
modal.hide();
loadAnnouncements();
} else {
UIEnhancements.showError(data.message || '保存失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('保存失败,请稍后重试');
}
}
// 删除公告
async function deleteAnnouncement(announcementId) {
const confirmed = await UIEnhancements.confirmAction('确定要删除该公告吗?', '确认删除');
if (!confirmed) return;
try {
const { data } = await UIEnhancements.enhancedFetch(`/merchant/announcements/${announcementId}`, {
method: 'DELETE',
credentials: 'include'
});
if (data.code === 200) {
UIEnhancements.showSuccess('公告已删除');
loadAnnouncements();
} else {
UIEnhancements.showError(data.message || '删除失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('删除失败,请稍后重试');
}
}
// 页面加载
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
loadAnnouncements();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,283 @@
<!-- 侧边栏导航 -->
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="d-flex align-items-center">
<i class="bi bi-shop fs-4 me-2"></i>
<span class="sidebar-brand">商家后台</span>
</div>
<button class="btn btn-link text-white p-0 d-md-none" id="sidebarCloseBtn">
<i class="bi bi-x-lg"></i>
</button>
</div>
<nav class="sidebar-nav">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="/merchant/dashboard.html" data-page="dashboard">
<i class="bi bi-speedometer2"></i>
<span class="nav-text">首页</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/products.html" data-page="products">
<i class="bi bi-box-seam"></i>
<span class="nav-text">商品管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/orders.html" data-page="orders">
<i class="bi bi-receipt-cutoff"></i>
<span class="nav-text">订单管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/inventory.html" data-page="inventory">
<i class="bi bi-archive"></i>
<span class="nav-text">库存管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/statistics.html" data-page="statistics">
<i class="bi bi-bar-chart"></i>
<span class="nav-text">数据统计</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/announcements.html" data-page="announcements">
<i class="bi bi-megaphone"></i>
<span class="nav-text">公告管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/messages.html" data-page="messages">
<i class="bi bi-chat-dots"></i>
<span class="nav-text">留言管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/reviews.html" data-page="reviews">
<i class="bi bi-star"></i>
<span class="nav-text">评价管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/users.html" data-page="users">
<i class="bi bi-people"></i>
<span class="nav-text">用户管理</span>
</a>
</li>
</ul>
</nav>
<div class="sidebar-footer">
<a href="/merchant/logout" class="btn btn-outline-light w-100">
<i class="bi bi-box-arrow-right"></i>
<span class="ms-2">退出登录</span>
</a>
</div>
</div>
<!-- 侧边栏遮罩层(移动端) -->
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<!-- 顶部导航栏(移动端) -->
<nav class="top-navbar d-md-none">
<div class="container-fluid d-flex align-items-center">
<button class="btn btn-link text-white p-0" id="sidebarToggleBtn">
<i class="bi bi-list fs-4"></i>
</button>
<span class="ms-3 text-white fw-bold">商家后台</span>
</div>
</nav>
<style>
/* 侧边栏样式 */
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 260px;
height: 100vh;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
color: #fff;
z-index: 1050;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 600;
}
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 1rem 0;
}
.sidebar-nav .nav-link {
color: rgba(255, 255, 255, 0.8);
padding: 0.75rem 1.5rem;
transition: all 0.3s;
border-left: 3px solid transparent;
display: flex;
align-items: center;
}
.sidebar-nav .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
border-left-color: #0d6efd;
}
.sidebar-nav .nav-link.active {
background-color: rgba(13, 110, 253, 0.2);
color: #fff;
border-left-color: #0d6efd;
font-weight: 600;
}
.sidebar-nav .nav-link i {
width: 24px;
font-size: 1.1rem;
margin-right: 0.75rem;
}
.sidebar-nav .nav-text {
flex: 1;
}
.sidebar-footer {
padding: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
/* 移动端样式 */
@media (max-width: 767.98px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.show {
transform: translateX(0);
}
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1049;
display: none;
transition: opacity 0.3s;
}
.sidebar-overlay.show {
display: block;
}
.top-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
z-index: 1048;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
body {
padding-top: 56px;
}
}
/* 桌面端主内容区域 */
@media (min-width: 768px) {
.main-content {
margin-left: 260px;
min-height: 100vh;
}
}
/* 滚动条样式 */
.sidebar-nav::-webkit-scrollbar {
width: 6px;
}
.sidebar-nav::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.sidebar-nav::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.sidebar-nav::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>
<script>
// 侧边栏交互逻辑
(function() {
const sidebar = document.getElementById('sidebar');
const sidebarOverlay = document.getElementById('sidebarOverlay');
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
const sidebarCloseBtn = document.getElementById('sidebarCloseBtn');
// 移动端切换侧边栏
if (sidebarToggleBtn) {
sidebarToggleBtn.addEventListener('click', function() {
sidebar.classList.add('show');
sidebarOverlay.classList.add('show');
document.body.style.overflow = 'hidden';
});
}
// 关闭侧边栏
function closeSidebar() {
sidebar.classList.remove('show');
sidebarOverlay.classList.remove('show');
document.body.style.overflow = '';
}
if (sidebarCloseBtn) {
sidebarCloseBtn.addEventListener('click', closeSidebar);
}
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', closeSidebar);
}
// 设置当前页面的导航项为激活状态
const currentPath = window.location.pathname;
const navLinks = document.querySelectorAll('.sidebar-nav .nav-link');
navLinks.forEach(link => {
if (link.getAttribute('href') === currentPath) {
link.classList.add('active');
}
});
// 点击导航项时,移动端自动关闭侧边栏
navLinks.forEach(link => {
link.addEventListener('click', function() {
if (window.innerWidth < 768) {
closeSidebar();
}
});
});
})();
</script>

View File

@@ -0,0 +1,413 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商家后台 - 奶茶店管理系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
.stat-card {
transition: transform 0.3s, box-shadow 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.stat-icon {
font-size: 3rem;
opacity: 0.3;
}
/* 侧边栏样式 */
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 260px;
height: 100vh;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
color: #fff;
z-index: 1050;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 600;
}
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 1rem 0;
}
.sidebar-nav .nav-link {
color: rgba(255, 255, 255, 0.8);
padding: 0.75rem 1.5rem;
transition: all 0.3s;
border-left: 3px solid transparent;
display: flex;
align-items: center;
}
.sidebar-nav .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
border-left-color: #0d6efd;
}
.sidebar-nav .nav-link.active {
background-color: rgba(13, 110, 253, 0.2);
color: #fff;
border-left-color: #0d6efd;
font-weight: 600;
}
.sidebar-nav .nav-link i {
width: 24px;
font-size: 1.1rem;
margin-right: 0.75rem;
}
.sidebar-nav .nav-text {
flex: 1;
}
.sidebar-footer {
padding: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
/* 移动端样式 */
@media (max-width: 767.98px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.show {
transform: translateX(0);
}
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1049;
display: none;
transition: opacity 0.3s;
}
.sidebar-overlay.show {
display: block;
}
.top-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
z-index: 1048;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
body {
padding-top: 56px;
}
}
/* 桌面端主内容区域 */
@media (min-width: 768px) {
.main-content {
margin-left: 260px;
min-height: 100vh;
position: relative;
z-index: 1;
width: calc(100% - 260px);
}
}
/* 滚动条样式 */
.sidebar-nav::-webkit-scrollbar {
width: 6px;
}
.sidebar-nav::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.sidebar-nav::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.sidebar-nav::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>
</head>
<body>
<!-- 侧边栏导航 -->
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="d-flex align-items-center">
<i class="bi bi-shop fs-4 me-2"></i>
<span class="sidebar-brand">商家后台</span>
</div>
<button class="btn btn-link text-white p-0 d-md-none" id="sidebarCloseBtn">
<i class="bi bi-x-lg"></i>
</button>
</div>
<nav class="sidebar-nav">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link active" href="/merchant/dashboard.html" data-page="dashboard">
<i class="bi bi-speedometer2"></i>
<span class="nav-text">首页</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/products.html" data-page="products">
<i class="bi bi-box-seam"></i>
<span class="nav-text">商品管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/orders.html" data-page="orders">
<i class="bi bi-receipt-cutoff"></i>
<span class="nav-text">订单管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/inventory.html" data-page="inventory">
<i class="bi bi-archive"></i>
<span class="nav-text">库存管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/statistics.html" data-page="statistics">
<i class="bi bi-bar-chart"></i>
<span class="nav-text">数据统计</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/announcements.html" data-page="announcements">
<i class="bi bi-megaphone"></i>
<span class="nav-text">公告管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/messages.html" data-page="messages">
<i class="bi bi-chat-dots"></i>
<span class="nav-text">留言管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/reviews.html" data-page="reviews">
<i class="bi bi-star"></i>
<span class="nav-text">评价管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/users.html" data-page="users">
<i class="bi bi-people"></i>
<span class="nav-text">用户管理</span>
</a>
</li>
</ul>
</nav>
<div class="sidebar-footer">
<a href="/merchant/logout" class="btn btn-outline-light w-100">
<i class="bi bi-box-arrow-right"></i>
<span class="ms-2">退出登录</span>
</a>
</div>
</div>
<!-- 侧边栏遮罩层(移动端) -->
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<!-- 顶部导航栏(移动端) -->
<nav class="top-navbar d-md-none">
<div class="container-fluid d-flex align-items-center">
<button class="btn btn-link text-white p-0" id="sidebarToggleBtn">
<i class="bi bi-list fs-4"></i>
</button>
<span class="ms-3 text-white fw-bold">商家后台</span>
</div>
</nav>
<div class="main-content">
<div class="container-fluid mt-4 mb-5">
<h2 class="mb-4"><i class="bi bi-speedometer2"></i> 数据概览</h2>
<div class="row">
<div class="col-md-4 mb-4">
<div class="card stat-card shadow-sm border-primary">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">今日订单</h6>
<h3 class="mb-0" id="todayOrders">-</h3>
</div>
<i class="bi bi-receipt-cutoff stat-icon text-primary"></i>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card stat-card shadow-sm border-success">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">今日营收</h6>
<h3 class="mb-0 text-success" id="todayRevenue">-</h3>
</div>
<i class="bi bi-currency-yen stat-icon text-success"></i>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card stat-card shadow-sm border-warning">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">待处理订单</h6>
<h3 class="mb-0 text-warning" id="pendingOrders">-</h3>
</div>
<i class="bi bi-clock-history stat-icon text-warning"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 使用Session浏览器自动发送Cookie
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
async function loadStatistics() {
try {
const { data } = await UIEnhancements.enhancedFetch('/merchant/statistics/sales?period=day', {
credentials: 'include'
});
if (data.code === 200 && data.data) {
document.getElementById('todayOrders').textContent = data.data.totalOrders || 0;
document.getElementById('todayRevenue').textContent = '¥' + UIEnhancements.formatNumber(data.data.totalAmount || 0);
} else {
document.getElementById('todayOrders').textContent = '-';
document.getElementById('todayRevenue').textContent = '-';
}
// 加载待处理订单数
try {
const { data: ordersData } = await UIEnhancements.enhancedFetch('/merchant/orders?status=MAKING&pageNum=1&pageSize=1', {
credentials: 'include'
});
if (ordersData.code === 200 && ordersData.data) {
document.getElementById('pendingOrders').textContent = ordersData.data.total || 0;
} else {
document.getElementById('pendingOrders').textContent = '-';
}
} catch (e) {
document.getElementById('pendingOrders').textContent = '-';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('todayOrders').textContent = '-';
document.getElementById('todayRevenue').textContent = '-';
document.getElementById('pendingOrders').textContent = '-';
}
}
// 侧边栏交互逻辑
(function() {
const sidebar = document.getElementById('sidebar');
const sidebarOverlay = document.getElementById('sidebarOverlay');
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
const sidebarCloseBtn = document.getElementById('sidebarCloseBtn');
// 移动端切换侧边栏
if (sidebarToggleBtn) {
sidebarToggleBtn.addEventListener('click', function() {
sidebar.classList.add('show');
sidebarOverlay.classList.add('show');
document.body.style.overflow = 'hidden';
});
}
// 关闭侧边栏
function closeSidebar() {
sidebar.classList.remove('show');
sidebarOverlay.classList.remove('show');
document.body.style.overflow = '';
}
if (sidebarCloseBtn) {
sidebarCloseBtn.addEventListener('click', closeSidebar);
}
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', closeSidebar);
}
// 点击导航项时,移动端自动关闭侧边栏
const navLinks = document.querySelectorAll('.sidebar-nav .nav-link');
navLinks.forEach(link => {
link.addEventListener('click', function() {
if (window.innerWidth < 768) {
closeSidebar();
}
});
});
})();
// 页面加载时检查登录
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
loadStatistics();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,466 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>库存管理 - 商家后台</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
/* 侧边栏样式 */
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 260px;
height: 100vh;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
color: #fff;
z-index: 1050;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 600;
}
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 1rem 0;
}
.sidebar-nav .nav-link {
color: rgba(255, 255, 255, 0.8);
padding: 0.75rem 1.5rem;
display: flex;
align-items: center;
transition: all 0.2s;
text-decoration: none;
}
.sidebar-nav .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
}
.sidebar-nav .nav-link.active {
background-color: rgba(255, 255, 255, 0.15);
color: #fff;
border-left: 3px solid #3498db;
}
.sidebar-nav .nav-link i {
width: 20px;
margin-right: 0.75rem;
font-size: 1.1rem;
}
.main-content {
margin-left: 260px;
padding: 2rem;
min-height: 100vh;
}
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.show {
transform: translateX(0);
}
.main-content {
margin-left: 0;
}
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/merchant/dashboard.html">商家后台</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/merchant/dashboard.html">首页</a>
<a class="nav-link" href="/merchant/products.html">商品管理</a>
<a class="nav-link" href="/merchant/orders.html">订单管理</a>
<a class="nav-link active" href="/merchant/inventory.html">库存管理</a>
<a class="nav-link" href="/merchant/statistics.html">数据统计</a>
<a class="nav-link" href="/merchant/announcements.html">公告管理</a>
<a class="nav-link" href="/merchant/messages.html">留言管理</a>
<a class="nav-link" href="/merchant/reviews.html">评价管理</a>
<a class="nav-link" href="/merchant/users.html">用户管理</a>
</div>
</div>
</nav>
<div class="container mt-4">
<div class="d-flex justify-content-between mb-3">
<h2>库存管理</h2>
<button class="btn btn-primary" onclick="showAddInventoryModal()">新增库存</button>
</div>
<div class="mb-3">
<button class="btn btn-outline-warning" onclick="loadLowStockAlerts()">低库存预警</button>
</div>
<div id="inventoryList"></div>
</div>
<!-- 添加/编辑库存模态框 -->
<div class="modal fade" id="inventoryModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="inventoryModalTitle">新增库存</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="inventoryForm">
<input type="hidden" id="inventoryId">
<div class="mb-3">
<label for="materialName" class="form-label">原料名称 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="materialName" required>
</div>
<div class="mb-3">
<label for="quantity" class="form-label">数量 <span class="text-danger">*</span></label>
<input type="number" class="form-control" id="quantity" step="0.01" min="0" required>
</div>
<div class="mb-3">
<label for="unit" class="form-label">单位</label>
<input type="text" class="form-control" id="unit" placeholder="例如kg、L、个">
</div>
<div class="mb-3">
<label for="minThreshold" class="form-label">最低库存阈值</label>
<input type="number" class="form-control" id="minThreshold" step="0.01" min="0" value="0">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveInventory()">保存</button>
</div>
</div>
</div>
</div>
<!-- 入库/出库模态框 -->
<div class="modal fade" id="stockModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="stockModalTitle">入库</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="stockForm">
<input type="hidden" id="stockInventoryId">
<div class="mb-3">
<label for="stockQuantity" class="form-label">数量 <span class="text-danger">*</span></label>
<input type="number" class="form-control" id="stockQuantity" step="0.01" min="0" required>
</div>
<div class="mb-3">
<label for="operator" class="form-label">操作人</label>
<input type="text" class="form-control" id="operator">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveStock()">确认</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 使用Session浏览器自动发送Cookie
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
let stockOperationType = 'in'; // 'in' 或 'out'
// 页面加载
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
loadInventory();
}
});
// 加载库存列表
async function loadInventory() {
try {
const response = await fetch('/merchant/inventory', {
credentials: 'include'
});
const data = await response.json();
const container = document.getElementById('inventoryList');
if (data.code === 200 && data.data) {
const inventory = data.data || [];
if (inventory.length === 0) {
container.innerHTML = '<div class="alert alert-info">暂无库存记录,请点击"新增库存"按钮添加</div>';
return;
}
container.innerHTML = `
<table class="table table-hover">
<thead>
<tr>
<th>原料名称</th>
<th>数量</th>
<th>单位</th>
<th>最低阈值</th>
<th>库存状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${inventory.map(item => {
const isLowStock = parseFloat(item.quantity || 0) <= parseFloat(item.minThreshold || 0);
return `
<tr class="${isLowStock ? 'table-warning' : ''}">
<td>${item.materialName || ''}</td>
<td>${item.quantity || '0'}</td>
<td>${item.unit || '-'}</td>
<td>${item.minThreshold || '0'}</td>
<td>
${isLowStock ?
'<span class="badge bg-warning">库存不足</span>' :
'<span class="badge bg-success">正常</span>'
}
</td>
<td>
<button class="btn btn-sm btn-success" onclick="showStockModal(${item.id}, 'in')">入库</button>
<button class="btn btn-sm btn-warning" onclick="showStockModal(${item.id}, 'out')">出库</button>
<button class="btn btn-sm btn-primary" onclick="editInventory(${item.id})">编辑</button>
<button class="btn btn-sm btn-danger" onclick="deleteInventory(${item.id})">删除</button>
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
} else {
container.innerHTML = '<div class="alert alert-warning">加载库存列表失败:' + (data.message || '未知错误') + '</div>';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('inventoryList').innerHTML = '<div class="alert alert-danger">加载库存列表失败,请刷新页面重试</div>';
}
}
// 加载低库存预警
async function loadLowStockAlerts() {
try {
const response = await fetch('/merchant/inventory/alerts', {
credentials: 'include'
});
const data = await response.json();
if (data.code === 200 && data.data) {
const alerts = data.data || [];
if (alerts.length === 0) {
alert('暂无低库存预警');
} else {
alert(`${alerts.length} 个原料库存不足:\n${alerts.map(a => `${a.materialName} (当前: ${a.quantity}, 阈值: ${a.minThreshold})`).join('\n')}`);
}
}
} catch (error) {
console.error('Error:', error);
alert('加载低库存预警失败');
}
}
// 显示添加库存模态框
function showAddInventoryModal() {
document.getElementById('inventoryModalTitle').textContent = '新增库存';
document.getElementById('inventoryId').value = '';
document.getElementById('inventoryForm').reset();
const modal = new bootstrap.Modal(document.getElementById('inventoryModal'));
modal.show();
}
// 编辑库存
async function editInventory(inventoryId) {
try {
const response = await fetch('/merchant/inventory', {
credentials: 'include'
});
const data = await response.json();
if (data.code === 200 && data.data) {
const inventory = data.data.find(i => i.id === inventoryId);
if (inventory) {
document.getElementById('inventoryModalTitle').textContent = '编辑库存';
document.getElementById('inventoryId').value = inventory.id;
document.getElementById('materialName').value = inventory.materialName || '';
document.getElementById('quantity').value = inventory.quantity || '';
document.getElementById('unit').value = inventory.unit || '';
document.getElementById('minThreshold').value = inventory.minThreshold || '0';
const modal = new bootstrap.Modal(document.getElementById('inventoryModal'));
modal.show();
}
}
} catch (error) {
console.error('Error:', error);
}
}
// 保存库存
async function saveInventory() {
const inventoryId = document.getElementById('inventoryId').value;
const inventoryData = {
materialName: document.getElementById('materialName').value,
quantity: parseFloat(document.getElementById('quantity').value) || 0,
unit: document.getElementById('unit').value,
minThreshold: parseFloat(document.getElementById('minThreshold').value) || 0
};
if (!inventoryData.materialName) {
UIEnhancements.showWarning('请填写材料名称');
return;
}
const saveBtn = document.querySelector('#inventoryModal .btn-primary');
try {
const url = inventoryId ? `/merchant/inventory/${inventoryId}` : '/merchant/inventory';
const method = inventoryId ? 'PUT' : 'POST';
const { data } = await UIEnhancements.enhancedFetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(inventoryData),
button: saveBtn
});
if (data.code === 200) {
UIEnhancements.showSuccess(inventoryId ? '库存信息已更新' : '库存已添加');
const modal = bootstrap.Modal.getInstance(document.getElementById('inventoryModal'));
modal.hide();
loadInventory();
} else {
UIEnhancements.showError(data.message || '保存失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('保存失败,请稍后重试');
}
}
// 显示入库/出库模态框
function showStockModal(inventoryId, type) {
stockOperationType = type;
document.getElementById('stockModalTitle').textContent = type === 'in' ? '入库' : '出库';
document.getElementById('stockInventoryId').value = inventoryId;
document.getElementById('stockForm').reset();
const modal = new bootstrap.Modal(document.getElementById('stockModal'));
modal.show();
}
// 保存入库/出库
async function saveStock() {
const inventoryId = document.getElementById('stockInventoryId').value;
const quantity = parseFloat(document.getElementById('stockQuantity').value);
const operator = document.getElementById('operator').value;
if (!quantity || quantity <= 0) {
UIEnhancements.showWarning('请输入有效的数量');
return;
}
const saveBtn = document.querySelector('#stockModal .btn-primary');
try {
const url = `/merchant/inventory/${inventoryId}/${stockOperationType}`;
const { data } = await UIEnhancements.enhancedFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
credentials: 'include',
body: `quantity=${quantity}&operator=${encodeURIComponent(operator || '')}`,
button: saveBtn
});
if (data.code === 200) {
UIEnhancements.showSuccess(stockOperationType === 'in' ? '入库成功' : '出库成功');
const modal = bootstrap.Modal.getInstance(document.getElementById('stockModal'));
modal.hide();
loadInventory();
} else {
UIEnhancements.showError(data.message || '操作失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('操作失败,请稍后重试');
}
}
// 删除库存
async function deleteInventory(inventoryId) {
const confirmed = await UIEnhancements.confirmAction('确定要删除这个库存记录吗?', '确认删除');
if (!confirmed) return;
try {
const { data } = await UIEnhancements.enhancedFetch(`/merchant/inventory/${inventoryId}`, {
method: 'DELETE',
credentials: 'include'
});
if (data.code === 200) {
UIEnhancements.showSuccess('库存记录已删除');
loadInventory();
} else {
UIEnhancements.showError(data.message || '删除失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('删除失败,请稍后重试');
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商家登录 - 奶茶店管理系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
body {
background-color: #f5f5f5;
min-height: 100vh;
display: flex;
align-items: center;
}
.login-card {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card login-card shadow-lg">
<div class="card-header bg-dark text-white text-center py-4">
<h3 class="mb-0">
<i class="bi bi-shop"></i> 商家后台
</h3>
<p class="mb-0 mt-2">商家登录</p>
</div>
<div class="card-body p-4">
<form id="loginForm">
<div class="mb-3">
<label for="username" class="form-label">
<i class="bi bi-person"></i> 用户名
</label>
<input type="text" class="form-control form-control-lg" id="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">
<i class="bi bi-lock"></i> 密码
</label>
<input type="password" class="form-control form-control-lg" id="password" required>
</div>
<button type="submit" class="btn btn-dark btn-lg w-100 mt-3">
<i class="bi bi-box-arrow-in-right"></i> 登录
</button>
</form>
<div class="mt-4 text-center">
<a href="/merchant/register.html" class="text-decoration-none">还没有账号?立即注册</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.getElementById('loginForm').addEventListener('submit', async function(e) {
e.preventDefault();
const response = await fetch('/merchant/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
credentials: 'include', // 确保发送Cookie
body: JSON.stringify({
username: document.getElementById('username').value,
password: document.getElementById('password').value
})
});
const data = await response.json();
if (data.code === 200) {
// 使用Session不需要存储token
window.location.href = '/merchant/dashboard.html';
} else {
alert(data.message);
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,209 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>留言管理 - 商家后台</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/merchant/dashboard.html">
<i class="bi bi-shop"></i> 商家后台
</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/merchant/dashboard.html">首页</a>
<a class="nav-link" href="/merchant/products.html">商品管理</a>
<a class="nav-link" href="/merchant/orders.html">订单管理</a>
<a class="nav-link" href="/merchant/inventory.html">库存管理</a>
<a class="nav-link" href="/merchant/statistics.html">数据统计</a>
<a class="nav-link" href="/merchant/announcements.html">公告管理</a>
<a class="nav-link active" href="/merchant/messages.html">留言管理</a>
<a class="nav-link" href="/merchant/reviews.html">评价管理</a>
<a class="nav-link" href="/merchant/users.html">用户管理</a>
</div>
</div>
</nav>
<div class="container mt-4 mb-5">
<h2 class="mb-4"><i class="bi bi-chat-dots"></i> 留言管理</h2>
<div class="card shadow-sm">
<div class="card-body">
<div id="messagesList"></div>
</div>
</div>
</div>
<!-- 回复留言模态框 -->
<div class="modal fade" id="replyModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">回复留言</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">留言内容</label>
<div class="form-control" id="messageContent" style="min-height: 60px; background-color: #f8f9fa;"></div>
</div>
<div class="mb-3">
<label for="replyContent" class="form-label">回复内容 <span class="text-danger">*</span></label>
<textarea class="form-control" id="replyContent" rows="4" required></textarea>
</div>
<input type="hidden" id="replyMessageId">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveReply()">保存回复</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
// 加载留言列表
async function loadMessages() {
try {
const response = await fetch('/merchant/messages', {
credentials: 'include'
});
const data = await response.json();
const container = document.getElementById('messagesList');
if (data.code === 200 && data.data) {
const messages = data.data || [];
if (messages.length === 0) {
container.innerHTML = '<div class="alert alert-info">暂无留言</div>';
return;
}
container.innerHTML = `
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>留言内容</th>
<th>订单号</th>
<th>状态</th>
<th>留言时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${messages.map(message => `
<tr>
<td>${message.content || ''}</td>
<td>${message.orderId ? '订单#' + message.orderId : '-'}</td>
<td>
<span class="badge ${message.status === 1 ? 'bg-success' : 'bg-warning'}">
${message.status === 1 ? '已回复' : '未回复'}
</span>
</td>
<td>${message.createTime ? new Date(message.createTime).toLocaleString('zh-CN') : '-'}</td>
<td>
${message.status === 0 ?
`<button class="btn btn-sm btn-primary" onclick="showReplyModal(${message.id}, '${(message.content || '').replace(/'/g, "\\'")}')">
<i class="bi bi-reply"></i> 回复
</button>` :
`<button class="btn btn-sm btn-outline-primary" onclick="showReplyModal(${message.id}, '${(message.content || '').replace(/'/g, "\\'")}', '${(message.reply || '').replace(/'/g, "\\'")}')">
<i class="bi bi-pencil"></i> 查看/编辑
</button>`
}
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
} else {
container.innerHTML = '<div class="alert alert-warning">加载留言列表失败:' + (data.message || '未知错误') + '</div>';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('messagesList').innerHTML = '<div class="alert alert-danger">加载留言列表失败,请刷新页面重试</div>';
}
}
// 显示回复模态框
function showReplyModal(messageId, content, reply = '') {
document.getElementById('replyMessageId').value = messageId;
document.getElementById('messageContent').textContent = content;
document.getElementById('replyContent').value = reply;
const modal = new bootstrap.Modal(document.getElementById('replyModal'));
modal.show();
}
// 保存回复
async function saveReply() {
const messageId = document.getElementById('replyMessageId').value;
const reply = document.getElementById('replyContent').value;
if (!reply.trim()) {
UIEnhancements.showWarning('请输入回复内容');
return;
}
const saveBtn = document.querySelector('#replyModal .btn-primary');
try {
const { data } = await UIEnhancements.enhancedFetch(`/merchant/messages/${messageId}/reply`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
credentials: 'include',
body: `reply=${encodeURIComponent(reply)}`,
button: saveBtn
});
if (data.code === 200) {
UIEnhancements.showSuccess('回复已保存');
const modal = bootstrap.Modal.getInstance(document.getElementById('replyModal'));
modal.hide();
loadMessages();
} else {
UIEnhancements.showError(data.message || '保存失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('保存失败,请稍后重试');
}
}
// 页面加载
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
loadMessages();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,503 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>订单管理 - 商家后台</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
:root {
--primary-bg: #f8f9fa;
--card-radius: 12px;
--modal-radius: 16px;
}
body {
background-color: #f3f4f6;
}
.filter-btn.active {
background-color: #0d6efd;
color: white;
border-color: #0d6efd;
box-shadow: 0 4px 10px rgba(13, 110, 253, 0.2);
}
.order-card {
border: none;
border-radius: var(--card-radius);
transition: all 0.2s ease;
background: #fff;
}
.order-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
}
/* --- 模态框优化样式 --- */
.modal-content {
border: none;
border-radius: var(--modal-radius);
box-shadow: 0 15px 40px rgba(0,0,0,0.1);
}
.modal-header {
border-bottom: 1px solid #f1f5f9;
padding: 20px 24px;
background-color: #fff;
border-radius: var(--modal-radius) var(--modal-radius) 0 0;
}
.modal-title {
font-weight: 700;
color: #1e293b;
}
.modal-body {
padding: 24px;
background-color: #f8fafc; /* 浅灰底色突出卡片 */
}
.modal-footer {
border-top: none;
padding: 16px 24px;
background: #fff;
border-radius: 0 0 var(--modal-radius) var(--modal-radius);
}
/* 详情卡片块 */
.detail-section {
background: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 6px rgba(0,0,0,0.02);
}
.section-title {
font-size: 14px;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 16px;
border-left: 4px solid #3b82f6;
padding-left: 10px;
}
/* 关键数据胶囊 */
.info-capsule {
background: #f1f5f9;
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
}
.info-capsule .label {
font-size: 11px;
color: #64748b;
margin-bottom: 4px;
}
.info-capsule .value {
font-size: 15px;
font-weight: 600;
color: #1e293b;
}
/* 商品表格美化 */
.custom-table thead th {
border-bottom: none;
color: #64748b;
font-weight: 600;
font-size: 13px;
background-color: #f8fafc;
}
.custom-table td {
vertical-align: middle;
color: #334155;
border-bottom: 1px solid #f1f5f9;
padding: 12px 8px;
}
/* --- 时间轴样式 (Timeline) --- */
.timeline {
position: relative;
padding-left: 24px;
border-left: 2px solid #e2e8f0;
margin-left: 8px;
}
.timeline-item {
position: relative;
margin-bottom: 20px;
}
.timeline-item:last-child {
margin-bottom: 0;
}
.timeline-dot {
position: absolute;
left: -31px; /* Line width 2px + padding 24px + dot radius approx */
top: 4px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #cbd5e1;
border: 2px solid #fff;
box-shadow: 0 0 0 2px #f1f5f9;
}
.timeline-dot.active {
background: #3b82f6;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2);
}
.timeline-time {
font-size: 12px;
color: #94a3b8;
margin-bottom: 2px;
}
.timeline-content {
font-size: 14px;
color: #334155;
font-weight: 500;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/merchant/dashboard.html">
<i class="bi bi-shop"></i> 商家后台
</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/merchant/dashboard.html">首页</a>
<a class="nav-link" href="/merchant/products.html">商品管理</a>
<a class="nav-link active" href="/merchant/orders.html">
<i class="bi bi-receipt-cutoff"></i> 订单管理
</a>
<a class="nav-link" href="/merchant/inventory.html">库存管理</a>
<a class="nav-link" href="/merchant/statistics.html">数据统计</a>
<a class="nav-link" href="/merchant/announcements.html">公告管理</a>
<a class="nav-link" href="/merchant/messages.html">留言管理</a>
<a class="nav-link" href="/merchant/reviews.html">评价管理</a>
<a class="nav-link" href="/merchant/users.html">用户管理</a>
</div>
</div>
</nav>
<div class="container mt-4 mb-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0 fw-bold text-dark"><i class="bi bi-receipt-cutoff text-primary"></i> 订单管理</h2>
<div class="btn-group shadow-sm" role="group">
<button class="btn btn-outline-primary filter-btn" onclick="filterOrders('')" id="filter-all">全部</button>
<button class="btn btn-outline-primary filter-btn" onclick="filterOrders('MAKING')" id="filter-making">制作中</button>
<button class="btn btn-outline-primary filter-btn" onclick="filterOrders('READY')" id="filter-ready">待取餐</button>
<button class="btn btn-outline-primary filter-btn" onclick="filterOrders('COMPLETED')" id="filter-completed">已完成</button>
</div>
</div>
<div id="ordersList"></div>
</div>
<!-- 优化后的订单详情模态框 -->
<div class="modal fade" id="orderDetailModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 text-primary rounded p-2 me-3">
<i class="bi bi-receipt fs-4"></i>
</div>
<div>
<h5 class="modal-title" id="modalOrderNo">订单详情</h5>
<small class="text-muted" id="modalOrderTime">加载中...</small>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="orderDetailContent">
<!-- 内容将由JS动态加载 -->
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status"></div>
</div>
</div>
<div class="modal-footer justify-content-between bg-white">
<span class="text-muted small">系统自动生成</span>
<button type="button" class="btn btn-secondary px-4 rounded-pill" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', { credentials: 'include' });
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
let currentStatus = '';
async function filterOrders(status) {
currentStatus = status;
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
const filterMap = { '': 'filter-all', 'MAKING': 'filter-making', 'READY': 'filter-ready', 'COMPLETED': 'filter-completed' };
const activeBtn = document.getElementById(filterMap[status] || 'filter-all');
if (activeBtn) activeBtn.classList.add('active');
await loadOrders();
}
async function loadOrders() {
const url = currentStatus ?
`/merchant/orders?status=${currentStatus}&pageNum=1&pageSize=20` :
'/merchant/orders?pageNum=1&pageSize=20';
try {
const response = await fetch(url, { credentials: 'include' });
const data = await response.json();
const container = document.getElementById('ordersList');
if (data.code === 200 && data.data && data.data.list) {
const orders = data.data.list;
if (orders.length === 0) {
container.innerHTML = '<div class="alert alert-light text-center py-4 text-muted border">暂无订单数据</div>';
return;
}
container.innerHTML = orders.map(order => `
<div class="card order-card mb-3">
<div class="card-body py-3">
<div class="row align-items-center g-3">
<div class="col-md-2">
<div class="text-muted small mb-1">订单号</div>
<div class="fw-bold font-monospace">${order.orderNo || ''}</div>
</div>
<div class="col-md-3">
<div class="d-flex align-items-center text-secondary">
<i class="bi bi-person-circle me-2"></i>
<div class="text-truncate" style="max-width: 150px;">${order.address || '自提'}</div>
</div>
<div class="text-muted small mt-1 ps-4">${order.contactPhone || '-'}</div>
</div>
<div class="col-md-2 text-center">
<span class="badge ${getStatusBadgeClass(order.orderStatus)} px-3 py-2 rounded-pill">
${getStatusText(order.orderStatus)}
</span>
</div>
<div class="col-md-2 text-center">
<div class="text-muted small">金额</div>
<strong class="text-primary fs-5">¥${order.totalAmount || '0.00'}</strong>
</div>
<div class="col-md-3 text-end">
<small class="text-muted d-block mb-2">
${order.createTime ? new Date(order.createTime).toLocaleTimeString('zh-CN', {hour:'2-digit', minute:'2-digit'}) : '-'}
</small>
<div class="btn-group">
${getActionButtons(order)}
<button class="btn btn-light text-primary border" onclick="viewDetail(${order.id})" title="详情">
详情
</button>
</div>
</div>
</div>
</div>
</div>
`).join('');
}
} catch (e) { console.error(e); }
}
function getActionButtons(order) {
if (order.orderStatus === 'MAKING') {
return `<button class="btn btn-success text-white" onclick="updateStatus(${order.id}, 'READY')">制作完成</button>`;
} else if (order.orderStatus === 'READY') {
return `<button class="btn btn-primary" onclick="updateStatus(${order.id}, 'COMPLETED')">完成订单</button>`;
}
return '';
}
function getStatusText(status) {
const map = { 'PENDING_PAY': '待支付', 'MAKING': '制作中', 'READY': '待取餐', 'COMPLETED': '已完成', 'CANCELLED': '已取消' };
return map[status] || status;
}
function getStatusBadgeClass(status) {
const map = { 'PENDING_PAY': 'bg-warning', 'MAKING': 'bg-info', 'READY': 'bg-primary', 'COMPLETED': 'bg-success', 'CANCELLED': 'bg-secondary' };
return map[status] || 'bg-secondary';
}
async function updateStatus(orderId, status) {
if(!confirm(`确定将订单更新为 ${getStatusText(status)} 吗?`)) return;
try {
const { data } = await UIEnhancements.enhancedFetch(`/merchant/orders/${orderId}/status?status=${status}`, {
method: 'PUT', credentials: 'include'
});
if (data.code === 200) {
UIEnhancements.showSuccess('状态已更新');
loadOrders();
} else {
UIEnhancements.showError(data.message || '操作失败');
}
} catch (error) { UIEnhancements.showError('网络错误'); }
}
async function viewDetail(orderId) {
const modal = new bootstrap.Modal(document.getElementById('orderDetailModal'));
modal.show();
try {
const response = await fetch(`/merchant/orders/${orderId}`, { credentials: 'include' });
const data = await response.json();
if (data.code === 200) {
renderModalContent(data.data);
} else {
document.getElementById('orderDetailContent').innerHTML = `<div class="alert alert-danger">${data.message}</div>`;
}
} catch (error) {
document.getElementById('orderDetailContent').innerHTML = '<div class="alert alert-danger">网络错误,无法加载详情</div>';
}
}
async function renderModalContent(detail) {
const order = detail.order;
const items = detail.items || [];
const tracks = detail.tracks || [];
// 设置标题栏信息
document.getElementById('modalOrderNo').innerText = `订单号:${order.orderNo}`;
document.getElementById('modalOrderTime').innerText = `下单时间:${new Date(order.createTime).toLocaleString()}`;
// 1. 处理商品明细 (Toppings逻辑保持不变)
let itemsHtml = '';
// (此处简化了异步获取配料名的逻辑以保证UI响应速度实际项目中可保留之前的Promise.all)
// 为了演示UI这里假设toppings已经处理好或者直接显示简单的文本
itemsHtml = items.map(item => {
let spec = item.specName ? `<span class="badge bg-light text-dark border ms-1">${item.specName}</span>` : '';
let custom = [];
if(item.customSweetness) custom.push(item.customSweetness);
if(item.customIce) custom.push(item.customIce);
// 简化的配料显示
let toppingText = item.toppings && item.toppings !== '[]' ? '<i class="bi bi-plus-circle-dotted ms-1"></i> 加料' : '';
return `
<tr>
<td style="width: 40%">
<div class="fw-bold">${item.productName}</div>
<div class="small text-muted">${custom.join(' / ')} ${toppingText}</div>
</td>
<td class="text-center">${spec}</td>
<td class="text-center">x${item.quantity}</td>
<td class="text-end fw-bold">¥${item.subtotal}</td>
</tr>
`;
}).join('');
// 2. 生成时间轴HTML
const tracksHtml = tracks.map((track, index) => `
<div class="timeline-item">
<div class="timeline-dot ${index === 0 ? 'active' : ''}"></div>
<div class="timeline-time">${new Date(track.createTime).toLocaleString('zh-CN', {hour:'2-digit', minute:'2-digit', month:'2-digit', day:'2-digit'})}</div>
<div class="timeline-content">
<span class="fw-bold text-dark">${getStatusText(track.status)}</span>
${track.description ? `<div class="small text-muted mt-1">${track.description}</div>` : ''}
</div>
</div>
`).join('');
// 3. 组合最终HTML
const content = `
<div class="row g-3">
<!-- 左侧:订单信息与商品 -->
<div class="col-lg-8">
<!-- 状态概览卡片 -->
<div class="detail-section d-flex justify-content-between align-items-center bg-primary bg-opacity-10 mb-3 border-0">
<div>
<span class="text-primary small fw-bold text-uppercase">当前状态</span>
<h3 class="text-primary fw-bold mb-0 mt-1">${getStatusText(order.orderStatus)}</h3>
</div>
<div class="text-end">
<span class="text-muted small">实付金额</span>
<h2 class="text-dark fw-bold mb-0">¥${order.totalAmount}</h2>
</div>
</div>
<!-- 客户信息 -->
<div class="detail-section">
<div class="section-title">配送信息</div>
<div class="row g-3">
<div class="col-md-6">
<div class="info-capsule">
<span class="label">联系人</span>
<span class="value"><i class="bi bi-telephone me-1"></i> ${order.contactPhone}</span>
</div>
</div>
<div class="col-md-6">
<div class="info-capsule">
<span class="label">支付方式</span>
<span class="value">
${order.paymentMethod === 'wechat' ? '<i class="bi bi-wechat text-success"></i> 微信支付' : '在线支付'}
</span>
</div>
</div>
<div class="col-12">
<div class="info-capsule">
<span class="label">收货地址</span>
<span class="value text-break"><i class="bi bi-geo-alt me-1"></i> ${order.address}</span>
</div>
</div>
</div>
</div>
<!-- 商品列表 -->
<div class="detail-section">
<div class="section-title">商品明细</div>
<div class="table-responsive">
<table class="table custom-table table-hover mb-0">
<thead>
<tr>
<th>品名/规格</th>
<th class="text-center">属性</th>
<th class="text-center">数量</th>
<th class="text-end">小计</th>
</tr>
</thead>
<tbody>${itemsHtml}</tbody>
</table>
</div>
<div class="border-top mt-3 pt-3 text-end">
<span class="text-muted me-2">共 ${items.reduce((sum, i) => sum + i.quantity, 0)} 件商品</span>
<span class="fs-5 fw-bold text-dark">合计: ¥${order.totalAmount}</span>
</div>
</div>
</div>
<!-- 右侧:时间轴 -->
<div class="col-lg-4">
<div class="detail-section h-100">
<div class="section-title">订单追踪</div>
<div class="timeline mt-4">
${tracksHtml || '<div class="text-muted small">暂无追踪信息</div>'}
</div>
</div>
</div>
</div>
`;
document.getElementById('orderDetailContent').innerHTML = content;
}
// 页面初始化
checkLogin().then(isLoggedIn => {
if (isLoggedIn) filterOrders('');
});
</script>
</body>
</html>

View File

@@ -0,0 +1,651 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商品编辑 - 商家后台</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/merchant/dashboard.html">商家后台</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/merchant/dashboard.html">首页</a>
<a class="nav-link active" href="/merchant/products.html">商品管理</a>
<a class="nav-link" href="/merchant/orders.html">订单管理</a>
<a class="nav-link" href="/merchant/inventory.html">库存管理</a>
<a class="nav-link" href="/merchant/statistics.html">数据统计</a>
<a class="nav-link" href="/merchant/announcements.html">公告管理</a>
<a class="nav-link" href="/merchant/messages.html">留言管理</a>
<a class="nav-link" href="/merchant/reviews.html">评价管理</a>
<a class="nav-link" href="/merchant/users.html">用户管理</a>
</div>
</div>
</nav>
<div class="container mt-4">
<h2 id="pageTitle">新增商品</h2>
<form id="productForm" enctype="multipart/form-data">
<input type="hidden" id="productId">
<div class="card mb-3">
<div class="card-header">基本信息</div>
<div class="card-body">
<div class="mb-3">
<label for="name" class="form-label">商品名称 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="name" required>
</div>
<div class="mb-3">
<label for="category" class="form-label">分类</label>
<input type="text" class="form-control" id="category" placeholder="例如:奶茶、果茶">
</div>
<div class="mb-3">
<label for="description" class="form-label">商品描述</label>
<textarea class="form-control" id="description" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="basePrice" class="form-label">基础价格 <span class="text-danger">*</span></label>
<input type="number" class="form-control" id="basePrice" step="0.01" min="0" required>
</div>
<div class="mb-3">
<label for="image" class="form-label">商品图片</label>
<input type="file" class="form-control" id="image" accept="image/*">
<div id="imagePreview" class="mt-2"></div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<span>规格管理</span>
<button type="button" class="btn btn-sm btn-primary" onclick="addSpec()">添加规格</button>
</div>
<div class="card-body">
<div id="specsList"></div>
</div>
</div>
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<span>配料管理</span>
<button type="button" class="btn btn-sm btn-primary" onclick="addTopping()">添加配料</button>
</div>
<div class="card-body">
<div id="toppingsList"></div>
</div>
</div>
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<span>定制选项管理(甜度、冰度)</span>
<div>
<button type="button" class="btn btn-sm btn-primary" onclick="addCustom('sweetness')">添加甜度</button>
<button type="button" class="btn btn-sm btn-primary" onclick="addCustom('ice')">添加冰度</button>
</div>
</div>
<div class="card-body">
<div class="mb-3">
<h6>甜度选项</h6>
<div id="sweetnessList"></div>
</div>
<div class="mb-3">
<h6>冰度选项</h6>
<div id="iceList"></div>
</div>
</div>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary">保存</button>
<a href="/merchant/products.html" class="btn btn-secondary">取消</a>
</div>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 使用Session浏览器自动发送Cookie
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
const urlParams = new URLSearchParams(window.location.search);
const productId = urlParams.get('id');
let specs = [];
let toppings = [];
let customs = {
sweetness: [],
ice: []
};
// 页面加载
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
if (productId) {
document.getElementById('pageTitle').textContent = '编辑商品';
loadProduct();
} else {
// 新商品时初始化默认选项
if (customs.sweetness.length === 0) {
customs.sweetness = [
{ optionType: 'sweetness', optionValue: '无糖', priceAdjust: 0 },
{ optionType: 'sweetness', optionValue: '少糖', priceAdjust: 0 },
{ optionType: 'sweetness', optionValue: '正常', priceAdjust: 0 },
{ optionType: 'sweetness', optionValue: '多糖', priceAdjust: 0 }
];
}
if (customs.ice.length === 0) {
customs.ice = [
{ optionType: 'ice', optionValue: '去冰', priceAdjust: 0 },
{ optionType: 'ice', optionValue: '少冰', priceAdjust: 0 },
{ optionType: 'ice', optionValue: '正常', priceAdjust: 0 },
{ optionType: 'ice', optionValue: '多冰', priceAdjust: 0 }
];
}
renderCustoms();
}
}
});
// 图片预览
document.getElementById('image').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('imagePreview').innerHTML =
`<img src="${e.target.result}" class="img-thumbnail" style="max-width: 200px;">`;
};
reader.readAsDataURL(file);
}
});
// 加载商品信息
async function loadProduct() {
try {
const response = await fetch(`/merchant/products/${productId}`, {
credentials: 'include'
});
const data = await response.json();
if (data.code === 200) {
const product = data.data.product;
document.getElementById('productId').value = product.id;
document.getElementById('name').value = product.name || '';
document.getElementById('category').value = product.category || '';
document.getElementById('description').value = product.description || '';
document.getElementById('basePrice').value = product.basePrice || '';
if (product.image) {
document.getElementById('imagePreview').innerHTML =
`<img src="${product.image}" class="img-thumbnail" style="max-width: 200px;">`;
}
specs = data.data.specs || [];
toppings = data.data.toppings || [];
const allCustoms = data.data.customs || [];
// 分离甜度和冰度选项
customs.sweetness = allCustoms.filter(c => c.optionType === 'sweetness');
customs.ice = allCustoms.filter(c => c.optionType === 'ice');
// 如果没有甜度选项,创建默认选项
if (customs.sweetness.length === 0) {
customs.sweetness = [
{ optionType: 'sweetness', optionValue: '无糖', priceAdjust: 0 },
{ optionType: 'sweetness', optionValue: '少糖', priceAdjust: 0 },
{ optionType: 'sweetness', optionValue: '正常', priceAdjust: 0 },
{ optionType: 'sweetness', optionValue: '多糖', priceAdjust: 0 }
];
}
// 如果没有冰度选项,创建默认选项
if (customs.ice.length === 0) {
customs.ice = [
{ optionType: 'ice', optionValue: '去冰', priceAdjust: 0 },
{ optionType: 'ice', optionValue: '少冰', priceAdjust: 0 },
{ optionType: 'ice', optionValue: '正常', priceAdjust: 0 },
{ optionType: 'ice', optionValue: '多冰', priceAdjust: 0 }
];
}
renderSpecs();
renderToppings();
renderCustoms();
} else {
if (data.message && data.message.includes('登录')) {
alert('请先登录');
window.location.href = '/merchant/login.html';
} else {
alert(data.message || '加载商品信息失败');
window.location.href = '/merchant/products.html';
}
}
} catch (error) {
console.error('Error:', error);
alert('加载商品信息失败');
window.location.href = '/merchant/products.html';
}
}
// 渲染规格列表
function renderSpecs() {
const container = document.getElementById('specsList');
if (specs.length === 0) {
container.innerHTML = '<p class="text-muted">暂无规格,点击"添加规格"按钮添加</p>';
return;
}
container.innerHTML = `
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>规格名称</th>
<th>价格调整</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${specs.map((spec, index) => `
<tr>
<td>
<input type="text" class="form-control form-control-sm"
value="${spec.specName || ''}"
onchange="updateSpec(${index}, 'specName', this.value)"
placeholder="例如:大杯、中杯、小杯">
</td>
<td>
<input type="number" class="form-control form-control-sm"
value="${spec.priceAdjust || 0}"
step="0.01"
onchange="updateSpec(${index}, 'priceAdjust', this.value)"
placeholder="价格调整">
</td>
<td>
<button type="button" class="btn btn-sm btn-danger" onclick="removeSpec(${index})">删除</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
// 渲染配料列表
function renderToppings() {
const container = document.getElementById('toppingsList');
if (toppings.length === 0) {
container.innerHTML = '<p class="text-muted">暂无配料,点击"添加配料"按钮添加</p>';
return;
}
container.innerHTML = `
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>配料名称</th>
<th>价格</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${toppings.map((topping, index) => `
<tr>
<td>
<input type="text" class="form-control form-control-sm"
value="${topping.toppingName || ''}"
onchange="updateTopping(${index}, 'toppingName', this.value)"
placeholder="例如:珍珠、椰果、布丁">
</td>
<td>
<input type="number" class="form-control form-control-sm"
value="${topping.price || 0}"
step="0.01"
onchange="updateTopping(${index}, 'price', this.value)"
placeholder="价格">
</td>
<td>
<button type="button" class="btn btn-sm btn-danger" onclick="removeTopping(${index})">删除</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
// 添加规格
function addSpec() {
specs.push({ specName: '', priceAdjust: 0 });
renderSpecs();
}
// 更新规格
function updateSpec(index, field, value) {
if (specs[index]) {
specs[index][field] = field === 'priceAdjust' ? parseFloat(value) || 0 : value;
}
}
// 删除规格
function removeSpec(index) {
if (confirm('确定要删除这个规格吗?')) {
specs.splice(index, 1);
renderSpecs();
}
}
// 添加配料
function addTopping() {
toppings.push({ toppingName: '', price: 0 });
renderToppings();
}
// 更新配料
function updateTopping(index, field, value) {
if (toppings[index]) {
toppings[index][field] = field === 'price' ? parseFloat(value) || 0 : value;
}
}
// 删除配料
function removeTopping(index) {
if (confirm('确定要删除这个配料吗?')) {
toppings.splice(index, 1);
renderToppings();
}
}
// 渲染定制选项
function renderCustoms() {
renderCustomType('sweetness');
renderCustomType('ice');
}
// 渲染特定类型的定制选项
function renderCustomType(type) {
const container = document.getElementById(type + 'List');
const options = customs[type] || [];
if (options.length === 0) {
container.innerHTML = '<p class="text-muted">暂无' + (type === 'sweetness' ? '甜度' : '冰度') + '选项,点击"添加' + (type === 'sweetness' ? '甜度' : '冰度') + '"按钮添加</p>';
return;
}
container.innerHTML = `
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>选项值</th>
<th>价格调整</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${options.map((custom, index) => `
<tr>
<td>
<input type="text" class="form-control form-control-sm"
value="${custom.optionValue || ''}"
onchange="updateCustom('${type}', ${index}, 'optionValue', this.value)"
placeholder="例如:${type === 'sweetness' ? '无糖、少糖、正常、多糖' : '去冰、少冰、正常、多冰'}">
</td>
<td>
<input type="number" class="form-control form-control-sm"
value="${custom.priceAdjust || 0}"
step="0.01"
onchange="updateCustom('${type}', ${index}, 'priceAdjust', this.value)"
placeholder="价格调整">
</td>
<td>
<button type="button" class="btn btn-sm btn-danger" onclick="removeCustom('${type}', ${index})">删除</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
// 添加定制选项
function addCustom(type) {
if (!customs[type]) {
customs[type] = [];
}
customs[type].push({
optionType: type,
optionValue: '',
priceAdjust: 0
});
renderCustomType(type);
}
// 更新定制选项
function updateCustom(type, index, field, value) {
if (customs[type] && customs[type][index]) {
customs[type][index][field] = field === 'priceAdjust' ? parseFloat(value) || 0 : value;
}
}
// 删除定制选项
function removeCustom(type, index) {
if (confirm('确定要删除这个选项吗?')) {
customs[type].splice(index, 1);
renderCustomType(type);
}
}
// 提交表单
document.getElementById('productForm').addEventListener('submit', async function(e) {
e.preventDefault();
// 表单验证
if (!UIEnhancements.validateForm('productForm')) {
UIEnhancements.showWarning('请填写所有必填项');
return;
}
const submitBtn = e.target.querySelector('button[type="submit"]');
const formData = new FormData();
formData.append('name', document.getElementById('name').value);
formData.append('category', document.getElementById('category').value);
formData.append('description', document.getElementById('description').value);
formData.append('basePrice', document.getElementById('basePrice').value);
const imageFile = document.getElementById('image').files[0];
if (imageFile) {
formData.append('image', imageFile);
}
const productIdValue = document.getElementById('productId').value;
const url = productIdValue ? `/merchant/products/${productIdValue}` : '/merchant/products';
const method = productIdValue ? 'PUT' : 'POST';
try {
UIEnhancements.setButtonLoading(submitBtn, true);
// 先保存商品基本信息
const response = await fetch(url, {
method: method,
credentials: 'include',
body: formData
});
const data = await response.json();
if (data.code === 200) {
const savedProductId = data.data.id || productIdValue;
// 保存规格、配料和定制选项如果有商品ID
if (savedProductId) {
await saveSpecsAndToppings(savedProductId);
await saveCustoms(savedProductId);
}
UIEnhancements.showSuccess('商品保存成功!');
setTimeout(() => {
window.location.href = '/merchant/products.html';
}, 1000);
} else {
if (data.message && data.message.includes('登录')) {
UIEnhancements.showWarning('请先登录');
setTimeout(() => {
window.location.href = '/merchant/login.html';
}, 1500);
} else {
UIEnhancements.showError(data.message || '保存失败');
}
UIEnhancements.setButtonLoading(submitBtn, false);
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('保存失败,请检查网络连接');
UIEnhancements.setButtonLoading(submitBtn, false);
}
});
// 保存规格和配料
async function saveSpecsAndToppings(productId) {
// 获取现有的规格和配料
const existingResponse = await fetch(`/merchant/products/${productId}`, {
credentials: 'include'
});
const existingData = await existingResponse.json();
const existingSpecs = existingData.code === 200 ? (existingData.data.specs || []) : [];
const existingToppings = existingData.code === 200 ? (existingData.data.toppings || []) : [];
// 删除所有现有规格和配料
for (const spec of existingSpecs) {
if (spec.id) {
await fetch(`/merchant/products/specs/${spec.id}`, {
method: 'DELETE',
credentials: 'include'
});
}
}
for (const topping of existingToppings) {
if (topping.id) {
await fetch(`/merchant/products/toppings/${topping.id}`, {
method: 'DELETE',
credentials: 'include'
});
}
}
// 添加新的规格
for (const spec of specs) {
if (spec.specName && spec.specName.trim()) {
await fetch(`/merchant/products/${productId}/specs`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
specName: spec.specName,
priceAdjust: spec.priceAdjust || 0
})
});
}
}
// 添加新的配料
for (const topping of toppings) {
if (topping.toppingName && topping.toppingName.trim()) {
await fetch(`/merchant/products/${productId}/toppings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
toppingName: topping.toppingName,
price: topping.price || 0
})
});
}
}
}
// 保存定制选项
async function saveCustoms(productId) {
// 获取现有的定制选项
const existingResponse = await fetch(`/merchant/products/${productId}/customs`, {
credentials: 'include'
});
const existingData = await existingResponse.json();
const existingCustoms = existingData.code === 200 ? (existingData.data || []) : [];
// 删除所有现有定制选项
for (const custom of existingCustoms) {
if (custom.id) {
await fetch(`/merchant/products/customs/${custom.id}`, {
method: 'DELETE',
credentials: 'include'
});
}
}
// 添加新的甜度选项
for (const custom of customs.sweetness) {
if (custom.optionValue && custom.optionValue.trim()) {
await fetch(`/merchant/products/${productId}/customs`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
optionType: 'sweetness',
optionValue: custom.optionValue,
priceAdjust: custom.priceAdjust || 0
})
});
}
}
// 添加新的冰度选项
for (const custom of customs.ice) {
if (custom.optionValue && custom.optionValue.trim()) {
await fetch(`/merchant/products/${productId}/customs`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
optionType: 'ice',
optionValue: custom.optionValue,
priceAdjust: custom.priceAdjust || 0
})
});
}
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,470 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商品管理 - 商家后台</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
.product-table-card {
transition: transform 0.2s;
}
.product-table-card:hover {
transform: translateX(5px);
}
/* 侧边栏样式 */
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 260px;
height: 100vh;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
color: #fff;
z-index: 1050;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 600;
}
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 1rem 0;
}
.sidebar-nav .nav-link {
color: rgba(255, 255, 255, 0.8);
padding: 0.75rem 1.5rem;
transition: all 0.3s;
border-left: 3px solid transparent;
display: flex;
align-items: center;
text-decoration: none;
}
.sidebar-nav .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
border-left-color: #0d6efd;
}
.sidebar-nav .nav-link.active {
background-color: rgba(13, 110, 253, 0.2);
color: #fff;
border-left-color: #0d6efd;
font-weight: 600;
}
.sidebar-nav .nav-link i {
width: 24px;
font-size: 1.1rem;
margin-right: 0.75rem;
}
.sidebar-nav .nav-text {
flex: 1;
}
.sidebar-footer {
padding: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
/* 移动端样式 */
@media (max-width: 767.98px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.show {
transform: translateX(0);
}
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1049;
display: none;
transition: opacity 0.3s;
}
.sidebar-overlay.show {
display: block;
}
.top-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
z-index: 1048;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
body {
padding-top: 56px;
}
}
/* 桌面端主内容区域 */
@media (min-width: 768px) {
.main-content {
margin-left: 260px;
min-height: 100vh;
position: relative;
z-index: 1;
width: calc(100% - 260px);
}
}
/* 滚动条样式 */
.sidebar-nav::-webkit-scrollbar {
width: 6px;
}
.sidebar-nav::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.sidebar-nav::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.sidebar-nav::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>
</head>
<body>
<!-- 侧边栏导航 -->
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="d-flex align-items-center">
<i class="bi bi-shop fs-4 me-2"></i>
<span class="sidebar-brand">商家后台</span>
</div>
<button class="btn btn-link text-white p-0 d-md-none" id="sidebarCloseBtn">
<i class="bi bi-x-lg"></i>
</button>
</div>
<nav class="sidebar-nav">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="/merchant/dashboard.html" data-page="dashboard">
<i class="bi bi-speedometer2"></i>
<span class="nav-text">首页</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/merchant/products.html" data-page="products">
<i class="bi bi-box-seam"></i>
<span class="nav-text">商品管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/orders.html" data-page="orders">
<i class="bi bi-receipt-cutoff"></i>
<span class="nav-text">订单管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/inventory.html" data-page="inventory">
<i class="bi bi-archive"></i>
<span class="nav-text">库存管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/statistics.html" data-page="statistics">
<i class="bi bi-bar-chart"></i>
<span class="nav-text">数据统计</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/announcements.html" data-page="announcements">
<i class="bi bi-megaphone"></i>
<span class="nav-text">公告管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/messages.html" data-page="messages">
<i class="bi bi-chat-dots"></i>
<span class="nav-text">留言管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/reviews.html" data-page="reviews">
<i class="bi bi-star"></i>
<span class="nav-text">评价管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/users.html" data-page="users">
<i class="bi bi-people"></i>
<span class="nav-text">用户管理</span>
</a>
</li>
</ul>
</nav>
<div class="sidebar-footer">
<a href="/merchant/logout" class="btn btn-outline-light w-100">
<i class="bi bi-box-arrow-right"></i>
<span class="ms-2">退出登录</span>
</a>
</div>
</div>
<!-- 侧边栏遮罩层(移动端) -->
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<!-- 顶部导航栏(移动端) -->
<nav class="top-navbar d-md-none">
<div class="container-fluid d-flex align-items-center">
<button class="btn btn-link text-white p-0" id="sidebarToggleBtn">
<i class="bi bi-list fs-4"></i>
</button>
<span class="ms-3 text-white fw-bold">商家后台</span>
</div>
</nav>
<div class="main-content">
<div class="container-fluid mt-4 mb-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-box-seam"></i> 商品管理</h2>
<button class="btn btn-primary" onclick="window.location.href='/merchant/product-edit.html'">
<i class="bi bi-plus-circle"></i> 新增商品
</button>
</div>
<div class="card shadow-sm">
<div class="card-body">
<div id="productsList"></div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 使用Session浏览器自动发送Cookie
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
async function loadProducts() {
const container = document.getElementById('productsList');
showLoading('productsList', '加载商品列表...');
try {
const { data } = await UIEnhancements.enhancedFetch('/merchant/products?pageNum=1&pageSize=20', {
credentials: 'include',
showLoading: 'productsList'
});
if (data.code === 200 && data.data && data.data.list) {
const products = data.data.list;
if (products.length === 0) {
container.innerHTML = '<div class="alert alert-info">暂无商品,请点击"新增商品"按钮添加商品</div>';
return;
}
container.innerHTML = `
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>图片</th>
<th>商品名称</th>
<th>分类</th>
<th>价格</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${products.map(product => `
<tr class="product-table-card">
<td>
<img src="${product.image || '/static/images/default.jpg'}"
class="img-thumbnail rounded"
style="width: 80px; height: 80px; object-fit: cover;"
alt="${product.name}">
</td>
<td><strong>${product.name || ''}</strong></td>
<td><span class="badge bg-secondary">${product.category || '-'}</span></td>
<td><strong class="text-primary">¥${product.basePrice || '0.00'}</strong></td>
<td>
<span class="badge ${product.status === 1 ? 'bg-success' : 'bg-secondary'}">
${product.status === 1 ? '上架' : '下架'}
</span>
</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="editProduct(${product.id})" title="编辑">
<i class="bi bi-pencil"></i>
</button>
${product.status === 1 ?
`<button class="btn btn-outline-warning" onclick="offlineProduct(${product.id})" title="下架">
<i class="bi bi-arrow-down-circle"></i>
</button>` :
`<button class="btn btn-outline-success" onclick="onlineProduct(${product.id})" title="上架">
<i class="bi bi-arrow-up-circle"></i>
</button>`
}
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
} else {
if (data.message && data.message.includes('登录')) {
alert('请先登录');
window.location.href = '/merchant/login.html';
} else {
container.innerHTML = '<div class="alert alert-warning">加载商品列表失败:' + (data.message || '未知错误') + '</div>';
}
}
} catch (error) {
console.error('Error:', error);
document.getElementById('productsList').innerHTML = '<div class="alert alert-danger">加载商品列表失败,请刷新页面重试</div>';
}
}
async function offlineProduct(productId) {
const confirmed = await UIEnhancements.confirmAction('确定要下架该商品吗?', '确认下架');
if (!confirmed) return;
try {
const { data } = await UIEnhancements.enhancedFetch(`/merchant/products/${productId}/offline`, {
method: 'PUT',
credentials: 'include'
});
if (data.code === 200) {
UIEnhancements.showSuccess('商品已下架');
loadProducts();
} else {
UIEnhancements.showError(data.message || '下架失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('操作失败,请稍后重试');
}
}
async function onlineProduct(productId) {
try {
const { data } = await UIEnhancements.enhancedFetch(`/merchant/products/${productId}/online`, {
method: 'PUT',
credentials: 'include'
});
if (data.code === 200) {
UIEnhancements.showSuccess('商品已上架');
loadProducts();
} else {
UIEnhancements.showError(data.message || '上架失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('操作失败,请稍后重试');
}
}
function editProduct(productId) {
window.location.href = `/merchant/product-edit.html?id=${productId}`;
}
// 侧边栏交互逻辑
(function() {
const sidebar = document.getElementById('sidebar');
const sidebarOverlay = document.getElementById('sidebarOverlay');
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
const sidebarCloseBtn = document.getElementById('sidebarCloseBtn');
// 移动端切换侧边栏
if (sidebarToggleBtn) {
sidebarToggleBtn.addEventListener('click', function() {
sidebar.classList.add('show');
sidebarOverlay.classList.add('show');
document.body.style.overflow = 'hidden';
});
}
// 关闭侧边栏
function closeSidebar() {
sidebar.classList.remove('show');
sidebarOverlay.classList.remove('show');
document.body.style.overflow = '';
}
if (sidebarCloseBtn) {
sidebarCloseBtn.addEventListener('click', closeSidebar);
}
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', closeSidebar);
}
// 点击导航项时,移动端自动关闭侧边栏
const navLinks = document.querySelectorAll('.sidebar-nav .nav-link');
navLinks.forEach(link => {
link.addEventListener('click', function() {
if (window.innerWidth < 768) {
closeSidebar();
}
});
});
})();
// 页面加载时检查登录
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
loadProducts();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商家注册 - 奶茶店管理系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
body {
background-color: #f5f5f5;
min-height: 100vh;
display: flex;
align-items: center;
}
.register-card {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-7">
<div class="card register-card shadow-lg">
<div class="card-header bg-dark text-white text-center py-4">
<h3 class="mb-0">
<i class="bi bi-shop"></i> 商家后台
</h3>
<p class="mb-0 mt-2">商家注册</p>
</div>
<div class="card-body p-4">
<form id="registerForm">
<div class="row">
<div class="col-md-6 mb-3">
<label for="username" class="form-label">
<i class="bi bi-person"></i> 用户名 <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" id="username" required>
</div>
<div class="col-md-6 mb-3">
<label for="password" class="form-label">
<i class="bi bi-lock"></i> 密码 <span class="text-danger">*</span>
</label>
<input type="password" class="form-control" id="password" required>
</div>
</div>
<div class="mb-3">
<label for="storeName" class="form-label">
<i class="bi bi-shop-window"></i> 门店名称 <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" id="storeName" required>
</div>
<div class="mb-3">
<label for="address" class="form-label">
<i class="bi bi-geo-alt"></i> 门店地址
</label>
<input type="text" class="form-control" id="address">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="phone" class="form-label">
<i class="bi bi-phone"></i> 联系电话
</label>
<input type="tel" class="form-control" id="phone">
</div>
<div class="col-md-6 mb-3">
<label for="email" class="form-label">
<i class="bi bi-envelope"></i> 邮箱
</label>
<input type="email" class="form-control" id="email">
</div>
</div>
<div class="mb-3">
<label for="managerName" class="form-label">
<i class="bi bi-person-badge"></i> 负责人姓名
</label>
<input type="text" class="form-control" id="managerName">
</div>
<button type="submit" class="btn btn-dark btn-lg w-100 mt-3">
<i class="bi bi-person-plus"></i> 注册
</button>
</form>
<div class="mt-4 text-center">
<a href="/merchant/login.html" class="text-decoration-none">已有账号?立即登录</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.getElementById('registerForm').addEventListener('submit', async function(e) {
e.preventDefault();
const response = await fetch('/merchant/register', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
credentials: 'include', // 确保发送Cookie
body: JSON.stringify({
username: document.getElementById('username').value,
password: document.getElementById('password').value,
storeName: document.getElementById('storeName').value,
address: document.getElementById('address').value,
phone: document.getElementById('phone').value,
email: document.getElementById('email').value,
managerName: document.getElementById('managerName').value
})
});
const data = await response.json();
if (data.code === 200) {
alert('注册成功!');
window.location.href = '/merchant/login.html';
} else {
alert(data.message || '注册失败');
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,212 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>评价管理 - 商家后台</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/merchant/dashboard.html">
<i class="bi bi-shop"></i> 商家后台
</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/merchant/dashboard.html">首页</a>
<a class="nav-link" href="/merchant/products.html">商品管理</a>
<a class="nav-link" href="/merchant/orders.html">订单管理</a>
<a class="nav-link" href="/merchant/inventory.html">库存管理</a>
<a class="nav-link" href="/merchant/statistics.html">数据统计</a>
<a class="nav-link" href="/merchant/announcements.html">公告管理</a>
<a class="nav-link" href="/merchant/messages.html">留言管理</a>
<a class="nav-link active" href="/merchant/reviews.html">评价管理</a>
<a class="nav-link" href="/merchant/users.html">用户管理</a>
</div>
</div>
</nav>
<div class="container mt-4 mb-5">
<h2 class="mb-4"><i class="bi bi-star"></i> 评价管理</h2>
<div class="card shadow-sm">
<div class="card-body">
<div id="reviewsList"></div>
</div>
</div>
</div>
<!-- 回复评价模态框 -->
<div class="modal fade" id="replyModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">回复评价</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">评价内容</label>
<div class="form-control" id="reviewContent" style="min-height: 60px; background-color: #f8f9fa;"></div>
</div>
<div class="mb-3">
<label for="replyContent" class="form-label">回复内容 <span class="text-danger">*</span></label>
<textarea class="form-control" id="replyContent" rows="4" required></textarea>
</div>
<input type="hidden" id="replyReviewId">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveReply()">保存回复</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
// 加载评价列表
async function loadReviews() {
try {
const response = await fetch('/merchant/reviews', {
credentials: 'include'
});
const data = await response.json();
const container = document.getElementById('reviewsList');
if (data.code === 200 && data.data) {
const reviews = data.data || [];
if (reviews.length === 0) {
container.innerHTML = '<div class="alert alert-info">暂无评价</div>';
return;
}
container.innerHTML = `
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>商品</th>
<th>评分</th>
<th>评价内容</th>
<th>评价时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${reviews.map(review => `
<tr>
<td>商品ID: ${review.productId || '-'}</td>
<td>
<div class="d-flex align-items-center">
${Array.from({length: 5}, (_, i) =>
`<i class="bi bi-star${i < (review.rating || 0) ? '-fill text-warning' : ''}"></i>`
).join('')}
<span class="ms-2">${review.rating || 0}分</span>
</div>
</td>
<td>${review.content || '-'}</td>
<td>${review.createTime ? new Date(review.createTime).toLocaleString('zh-CN') : '-'}</td>
<td>
${review.reply ?
`<button class="btn btn-sm btn-outline-primary" onclick="showReplyModal(${review.id}, '${(review.content || '').replace(/'/g, "\\'")}', '${(review.reply || '').replace(/'/g, "\\'")}')">
<i class="bi bi-pencil"></i> 查看/编辑
</button>` :
`<button class="btn btn-sm btn-primary" onclick="showReplyModal(${review.id}, '${(review.content || '').replace(/'/g, "\\'")}')">
<i class="bi bi-reply"></i> 回复
</button>`
}
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
} else {
container.innerHTML = '<div class="alert alert-warning">加载评价列表失败:' + (data.message || '未知错误') + '</div>';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('reviewsList').innerHTML = '<div class="alert alert-danger">加载评价列表失败,请刷新页面重试</div>';
}
}
// 显示回复模态框
function showReplyModal(reviewId, content, reply = '') {
document.getElementById('replyReviewId').value = reviewId;
document.getElementById('reviewContent').textContent = content;
document.getElementById('replyContent').value = reply;
const modal = new bootstrap.Modal(document.getElementById('replyModal'));
modal.show();
}
// 保存回复
async function saveReply() {
const reviewId = document.getElementById('replyReviewId').value;
const reply = document.getElementById('replyContent').value;
if (!reply.trim()) {
UIEnhancements.showWarning('请输入回复内容');
return;
}
const saveBtn = document.querySelector('#replyModal .btn-primary');
try {
const { data } = await UIEnhancements.enhancedFetch(`/merchant/reviews/${reviewId}/reply`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
credentials: 'include',
body: `reply=${encodeURIComponent(reply)}`,
button: saveBtn
});
if (data.code === 200) {
UIEnhancements.showSuccess('回复已保存');
const modal = bootstrap.Modal.getInstance(document.getElementById('replyModal'));
modal.hide();
loadReviews();
} else {
UIEnhancements.showError(data.message || '保存失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('保存失败,请稍后重试');
}
}
// 页面加载
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
loadReviews();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,491 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据统计 - 商家后台</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
.filter-btn.active {
background-color: #0d6efd;
color: white;
border-color: #0d6efd;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/merchant/dashboard.html">商家后台</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/merchant/dashboard.html">首页</a>
<a class="nav-link" href="/merchant/products.html">商品管理</a>
<a class="nav-link" href="/merchant/orders.html">订单管理</a>
<a class="nav-link" href="/merchant/inventory.html">库存管理</a>
<a class="nav-link active" href="/merchant/statistics.html">数据统计</a>
<a class="nav-link" href="/merchant/announcements.html">公告管理</a>
<a class="nav-link" href="/merchant/messages.html">留言管理</a>
<a class="nav-link" href="/merchant/reviews.html">评价管理</a>
<a class="nav-link" href="/merchant/users.html">用户管理</a>
</div>
</div>
</nav>
<div class="container mt-4 mb-5">
<h2 class="mb-4"><i class="bi bi-graph-up"></i> 数据统计</h2>
<div class="mb-4">
<div class="btn-group" role="group">
<button class="btn btn-outline-primary filter-btn" onclick="loadStatistics('day')" id="filter-day">今日</button>
<button class="btn btn-outline-primary filter-btn" onclick="loadStatistics('week')" id="filter-week">本周</button>
<button class="btn btn-outline-primary filter-btn" onclick="loadStatistics('month')" id="filter-month">本月</button>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-cart-check"></i> 销售统计</h5>
</div>
<div class="card-body" id="salesStats">
<p class="text-muted">请选择时间段</p>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-currency-yen"></i> 营收统计</h5>
</div>
<div class="card-body" id="revenueStats">
<p class="text-muted">请选择时间段</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-people"></i> 活跃用户</h5>
</div>
<div class="card-body" id="activeUsersStats">
<p class="text-muted">加载中...</p>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-warning text-white">
<h5 class="mb-0"><i class="bi bi-star-fill"></i> 粘性用户</h5>
</div>
<div class="card-body" id="stickyUsersStats">
<p class="text-muted">加载中...</p>
</div>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-fire"></i> 热销商品</h5>
</div>
<div class="card-body">
<div id="hotProducts"></div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 使用Session浏览器自动发送Cookie
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
let currentPeriod = 'day';
// 页面加载
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
loadStatistics('day');
loadHotProducts();
loadActiveUsers();
loadStickyUsers();
// 激活"今日"按钮
document.getElementById('filter-day').classList.add('active');
}
});
// 加载统计数据
async function loadStatistics(period) {
currentPeriod = period;
// 更新按钮激活状态
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
});
const activeBtn = document.getElementById('filter-' + period);
if (activeBtn) activeBtn.classList.add('active');
try {
// 加载销售统计
const salesResponse = await fetch(`/merchant/statistics/sales?period=${period}`, {
credentials: 'include'
});
const salesData = await salesResponse.json();
if (salesData.code === 200 && salesData.data) {
const stats = salesData.data;
const totalAmount = parseFloat(stats.totalAmount || 0).toFixed(2);
const avgAmount = parseFloat(stats.avgOrderAmount || 0).toFixed(2);
document.getElementById('salesStats').innerHTML = `
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">总订单数</span>
<strong class="fs-4">${stats.totalOrders || 0}</strong>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">总销售额</span>
<strong class="fs-4 text-primary">¥${totalAmount}</strong>
</div>
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted">平均订单金额</span>
<strong>¥${avgAmount}</strong>
</div>
</div>
`;
} else {
document.getElementById('salesStats').innerHTML = '<p class="text-muted">加载失败:' + (salesData.message || '未知错误') + '</p>';
}
// 加载营收统计
const revenueResponse = await fetch(`/merchant/statistics/revenue?period=${period}`, {
credentials: 'include'
});
const revenueData = await revenueResponse.json();
if (revenueData.code === 200 && revenueData.data) {
const stats = revenueData.data;
const totalRevenue = parseFloat(stats.totalRevenue || 0).toFixed(2);
const totalCost = parseFloat(stats.totalCost || 0).toFixed(2);
const netProfit = parseFloat(stats.netProfit || 0).toFixed(2);
document.getElementById('revenueStats').innerHTML = `
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">总收入</span>
<strong class="fs-4 text-success">¥${totalRevenue}</strong>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">总成本</span>
<strong>¥${totalCost}</strong>
</div>
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted">净利润</span>
<strong class="fs-5 text-primary">¥${netProfit}</strong>
</div>
</div>
`;
} else {
document.getElementById('revenueStats').innerHTML = '<p class="text-muted">加载失败:' + (revenueData.message || '未知错误') + '</p>';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('salesStats').innerHTML = '<p class="text-danger">加载失败,请刷新重试</p>';
document.getElementById('revenueStats').innerHTML = '<p class="text-danger">加载失败,请刷新重试</p>';
}
}
// 加载热销商品
async function loadHotProducts() {
try {
const response = await fetch('/merchant/statistics/hot-products?limit=10', {
credentials: 'include'
});
const data = await response.json();
const container = document.getElementById('hotProducts');
if (data.code === 200 && data.data) {
const products = data.data || [];
if (products.length === 0) {
container.innerHTML = '<p class="text-muted">暂无数据</p>';
return;
}
container.innerHTML = `
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>排名</th>
<th>商品名称</th>
<th>销量</th>
<th>销售额</th>
</tr>
</thead>
<tbody>
${products.map((product, index) => `
<tr>
<td>
<span class="badge ${index < 3 ? 'bg-warning' : 'bg-secondary'}">${index + 1}</span>
</td>
<td><strong>${product.productName || '未知商品'}</strong></td>
<td><span class="badge bg-info">${product.salesCount || 0}</span></td>
<td><strong class="text-primary">¥${parseFloat(product.salesAmount || 0).toFixed(2)}</strong></td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
} else {
container.innerHTML = '<p class="text-muted">加载失败</p>';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('hotProducts').innerHTML = '<p class="text-muted">加载失败</p>';
}
}
// 加载活跃用户统计
async function loadActiveUsers() {
try {
const response = await fetch('/merchant/statistics/active-users?days=30', {
credentials: 'include'
});
const data = await response.json();
const container = document.getElementById('activeUsersStats');
if (data.code === 200 && data.data) {
const users = data.data || [];
const count = users.length;
container.innerHTML = `
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">活跃用户数</span>
<strong class="fs-4 text-info">${count}</strong>
</div>
<div class="text-muted small mb-2">最近30天有浏览行为的用户</div>
${count > 0 ? `
<button class="btn btn-sm btn-outline-info w-100" onclick="showActiveUsersModal()">
<i class="bi bi-eye"></i> 查看详情
</button>
` : ''}
</div>
`;
} else {
container.innerHTML = '<p class="text-muted">加载失败</p>';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('activeUsersStats').innerHTML = '<p class="text-muted">加载失败</p>';
}
}
// 加载粘性用户统计
async function loadStickyUsers() {
try {
const response = await fetch('/merchant/statistics/sticky-users?days=30&minOrders=5', {
credentials: 'include'
});
const data = await response.json();
const container = document.getElementById('stickyUsersStats');
if (data.code === 200 && data.data) {
const users = data.data || [];
const count = users.length;
const totalAmount = users.reduce((sum, user) => sum + parseFloat(user.totalAmount || 0), 0).toFixed(2);
container.innerHTML = `
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">粘性用户数</span>
<strong class="fs-4 text-warning">${count}</strong>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">总消费金额</span>
<strong>¥${totalAmount}</strong>
</div>
<div class="text-muted small mb-2">最近30天下单5次以上</div>
${count > 0 ? `
<button class="btn btn-sm btn-outline-warning w-100" onclick="showStickyUsersModal()">
<i class="bi bi-eye"></i> 查看详情
</button>
` : ''}
</div>
`;
} else {
container.innerHTML = '<p class="text-muted">加载失败</p>';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('stickyUsersStats').innerHTML = '<p class="text-muted">加载失败</p>';
}
}
// 显示活跃用户列表模态框
async function showActiveUsersModal() {
try {
const response = await fetch('/merchant/statistics/active-users?days=30', {
credentials: 'include'
});
const data = await response.json();
if (data.code === 200 && data.data) {
const users = data.data || [];
let modalContent = `
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>用户名</th>
<th>姓名</th>
<th>浏览次数</th>
<th>最后浏览时间</th>
</tr>
</thead>
<tbody>
`;
users.forEach(user => {
const lastViewTime = user.lastViewTime ? new Date(user.lastViewTime).toLocaleString('zh-CN') : '-';
modalContent += `
<tr>
<td>${user.username || '-'}</td>
<td>${user.name || '-'}</td>
<td><span class="badge bg-info">${user.viewCount || 0}</span></td>
<td>${lastViewTime}</td>
</tr>
`;
});
modalContent += `
</tbody>
</table>
</div>
`;
showModal('活跃用户列表', modalContent);
}
} catch (error) {
console.error('Error:', error);
alert('加载活跃用户列表失败');
}
}
// 显示粘性用户列表模态框
async function showStickyUsersModal() {
try {
const response = await fetch('/merchant/statistics/sticky-users?days=30&minOrders=5', {
credentials: 'include'
});
const data = await response.json();
if (data.code === 200 && data.data) {
const users = data.data || [];
let modalContent = `
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>用户名</th>
<th>姓名</th>
<th>订单数</th>
<th>总消费金额</th>
</tr>
</thead>
<tbody>
`;
users.forEach(user => {
const totalAmount = parseFloat(user.totalAmount || 0).toFixed(2);
modalContent += `
<tr>
<td>${user.username || '-'}</td>
<td>${user.name || '-'}</td>
<td><span class="badge bg-warning">${user.orderCount || 0}</span></td>
<td><strong class="text-primary">¥${totalAmount}</strong></td>
</tr>
`;
});
modalContent += `
</tbody>
</table>
</div>
`;
showModal('粘性用户列表', modalContent);
}
} catch (error) {
console.error('Error:', error);
alert('加载粘性用户列表失败');
}
}
// 显示模态框的通用函数
function showModal(title, content) {
const modalHtml = `
<div class="modal fade" id="userListModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${title}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
${content}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
`;
// 移除旧的模态框
const oldModal = document.getElementById('userListModal');
if (oldModal) {
oldModal.remove();
}
// 添加新的模态框
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('userListModal'));
modal.show();
// 模态框关闭后移除
document.getElementById('userListModal').addEventListener('hidden.bs.modal', function() {
this.remove();
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,163 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理 - 商家后台</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/merchant/dashboard.html">
<i class="bi bi-shop"></i> 商家后台
</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/merchant/dashboard.html">首页</a>
<a class="nav-link" href="/merchant/products.html">商品管理</a>
<a class="nav-link" href="/merchant/orders.html">订单管理</a>
<a class="nav-link" href="/merchant/inventory.html">库存管理</a>
<a class="nav-link" href="/merchant/statistics.html">数据统计</a>
<a class="nav-link" href="/merchant/announcements.html">公告管理</a>
<a class="nav-link" href="/merchant/messages.html">留言管理</a>
<a class="nav-link" href="/merchant/reviews.html">评价管理</a>
<a class="nav-link active" href="/merchant/users.html">用户管理</a>
</div>
</div>
</nav>
<div class="container mt-4 mb-5">
<h2 class="mb-4"><i class="bi bi-people"></i> 用户管理</h2>
<div class="card shadow-sm">
<div class="card-body">
<div id="usersList"></div>
<nav aria-label="用户列表分页">
<ul class="pagination justify-content-center" id="pagination"></ul>
</nav>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/sidebar-replacer.js"></script>
<script>
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
let currentPage = 1;
const pageSize = 10;
// 页面加载
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
loadUsers(1);
}
});
// 加载用户列表
async function loadUsers(pageNum) {
try {
currentPage = pageNum;
const response = await fetch(`/merchant/users?pageNum=${pageNum}&pageSize=${pageSize}`, {
credentials: 'include'
});
const data = await response.json();
const container = document.getElementById('usersList');
const pagination = document.getElementById('pagination');
if (data.code === 200 && data.data) {
const pageResponse = data.data;
const users = pageResponse.list || [];
if (users.length === 0) {
container.innerHTML = '<div class="alert alert-info">暂无用户</div>';
pagination.innerHTML = '';
return;
}
container.innerHTML = `
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>用户名</th>
<th>姓名</th>
<th>手机号</th>
<th>邮箱</th>
<th>注册时间</th>
</tr>
</thead>
<tbody>
${users.map(user => `
<tr>
<td><strong>${user.username || '-'}</strong></td>
<td>${user.name || '-'}</td>
<td>${user.phone || '-'}</td>
<td>${user.email || '-'}</td>
<td>${user.createTime ? new Date(user.createTime).toLocaleString('zh-CN') : '-'}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
// 生成分页
const totalPages = pageResponse.pages || 1;
let paginationHtml = '';
// 上一页
paginationHtml += `<li class="page-item ${currentPage === 1 ? 'disabled' : ''}">
<a class="page-link" href="javascript:void(0)" onclick="loadUsers(${currentPage - 1})">上一页</a>
</li>`;
// 页码
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
paginationHtml += `<li class="page-item ${i === currentPage ? 'active' : ''}">
<a class="page-link" href="javascript:void(0)" onclick="loadUsers(${i})">${i}</a>
</li>`;
} else if (i === currentPage - 3 || i === currentPage + 3) {
paginationHtml += `<li class="page-item disabled">
<span class="page-link">...</span>
</li>`;
}
}
// 下一页
paginationHtml += `<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
<a class="page-link" href="javascript:void(0)" onclick="loadUsers(${currentPage + 1})">下一页</a>
</li>`;
pagination.innerHTML = paginationHtml;
} else {
container.innerHTML = '<div class="alert alert-warning">加载用户列表失败:' + (data.message || '未知错误') + '</div>';
pagination.innerHTML = '';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('usersList').innerHTML = '<div class="alert alert-danger">加载用户列表失败,请刷新页面重试</div>';
document.getElementById('pagination').innerHTML = '';
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,389 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>购物袋</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
:root {
--bg-body: #f8fafc;
--text-primary: #0f172a;
--accent-color: #3b82f6;
}
body {
background: var(--bg-body);
/* 增加底部留白TabBar高度(约70px) + 结算栏高度(约70px) + 间距(20px) */
padding-bottom: 160px;
font-family: -apple-system, sans-serif;
}
.page-title { padding: 20px 20px 10px; font-weight: 800; font-size: 24px; color: var(--text-primary); }
/* 列表项 */
.cart-item {
background: #fff;
margin: 0 16px 16px;
padding: 16px;
border-radius: 16px;
display: flex;
align-items: center;
box-shadow: 0 1px 2px rgba(0,0,0,0.02);
}
.item-img {
width: 64px; height: 64px; border-radius: 8px; object-fit: cover; background: #f1f5f9; flex-shrink: 0;
}
.item-info { flex: 1; margin-left: 12px; min-width: 0; /* 防止文本溢出撑开 */ }
.item-name { font-weight: 600; font-size: 15px; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.item-desc { font-size: 12px; color: #64748b; margin-bottom: 8px; }
.item-meta { display: flex; justify-content: space-between; align-items: center; }
.item-price { font-weight: 700; font-size: 15px; }
/* 数量微调器 */
.mini-stepper {
display: flex; align-items: center; background: #f8fafc; border-radius: 6px; padding: 2px;
}
.mini-btn { width: 24px; height: 24px; border: none; background: #fff; color: #64748b; border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.05); display: flex; align-items: center; justify-content: center; }
.mini-val { font-size: 12px; font-weight: 600; width: 24px; text-align: center; }
/* 底部结算栏 */
.settle-bar {
position: fixed;
/* 关键修改抬高位置避开TabBar */
bottom: 85px;
left: 16px; right: 16px;
background: #fff; padding: 12px 16px;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
display: flex; justify-content: space-between; align-items: center;
z-index: 900; /* 层级低于TabBar(通常999或1000) */
}
.total-info span { font-size: 12px; color: #64748b; }
.total-info strong { font-size: 18px; color: var(--text-primary); margin-left: 4px; }
.btn-checkout {
background: var(--text-primary); color: #fff; border: none;
padding: 10px 24px; border-radius: 10px; font-weight: 600; font-size: 14px;
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.2);
}
/* 底部导航 */
.mobile-tabbar {
position: fixed; bottom: 0; left: 0; right: 0;
background: #fff; padding: 8px 0 20px; /* 适配 iPhone Home Indicator */
display: flex; justify-content: space-around;
border-top: 1px solid #f1f5f9;
z-index: 1000;
}
.tab-item {
text-decoration: none; color: #94a3b8; font-size: 10px;
display: flex; flex-direction: column; align-items: center; flex: 1;
}
.tab-item.active { color: var(--accent-color); }
.tab-item i { font-size: 24px; margin-bottom: 2px; }
/* 空状态 */
.empty-state { text-align: center; padding: 60px 20px; color: #94a3b8; }
.empty-icon { font-size: 48px; margin-bottom: 16px; display: block; opacity: 0.5; }
</style>
</head>
<body>
<div class="page-title">购物袋</div>
<div id="cartList">
<div class="empty-state">
<i class="bi bi-cart3 empty-icon"></i>
<div>加载中...</div>
</div>
</div>
<!-- 底部结算 -->
<div class="settle-bar" id="settleBar" style="display: none;">
<div class="total-info">
<span>合计</span>
<strong id="totalPrice">¥0.00</strong>
</div>
<button class="btn-checkout" onclick="checkout()">去结算 (<span id="count">0</span>)</button>
</div>
<!-- 导航 -->
<div class="mobile-tabbar">
<a href="/index.html" class="tab-item">
<i class="bi bi-cup-hot"></i>
<span>点餐</span>
</a>
<a href="/cart.html" class="tab-item active">
<i class="bi bi-bag-fill"></i>
<span>购物袋</span>
</a>
<a href="/orders.html" class="tab-item">
<i class="bi bi-receipt"></i>
<span>订单</span>
</a>
<a href="/profile.html" class="tab-item">
<i class="bi bi-person"></i>
<span>我的</span>
</a>
</div>
<script src="/static/js/ui-enhancements.js"></script>
<script src="/static/js/cookie-utils.js"></script>
<script>
let cartItems = [];
let productCache = {};
let isLoggedIn = false;
// 检查用户是否登录
async function checkLoginStatus() {
try {
const res = await UIEnhancements.enhancedFetch('/cart', { credentials: 'include' });
isLoggedIn = res.data.code === 200;
return isLoggedIn;
} catch(e) {
isLoggedIn = false;
return false;
}
}
async function init() {
const loggedIn = await checkLoginStatus();
if (loggedIn) {
// 已登录从后端API获取
try {
const res = await UIEnhancements.enhancedFetch('/cart', { credentials: 'include' });
const json = res.data;
if(json.code === 200) {
cartItems = json.data || [];
await loadCartItems();
} else {
showEmptyState();
}
} catch(e) {
console.error(e);
showEmptyState();
}
} else {
// 未登录从cookies读取
const cookieCart = CookieUtils.getJSON('cartItems') || [];
if (cookieCart.length > 0) {
// 需要加载商品详情信息
cartItems = await loadCartItemsFromCookies(cookieCart);
await loadCartItems();
} else {
showEmptyState();
}
}
}
// 从cookies加载购物车商品需要获取商品详情
async function loadCartItemsFromCookies(cookieItems) {
const items = [];
for (const item of cookieItems) {
try {
// 获取商品详情
const res = await fetch(`/products/${item.productId}`);
const json = await res.json();
if (json.code === 200 && json.data) {
const product = json.data.product;
const spec = json.data.specs?.find(s => s.id === item.specId);
items.push({
id: `cookie_${item.productId}_${Date.now()}_${Math.random()}`, // 临时ID
productId: item.productId,
specId: item.specId,
quantity: item.quantity,
customSweetness: item.customSweetness,
customIce: item.customIce,
toppings: item.toppings,
product: product,
spec: spec
});
}
} catch(e) {
console.error('加载商品详情失败:', e);
}
}
return items;
}
// 加载购物车商品(计算价格并渲染)
async function loadCartItems() {
if (cartItems.length > 0) {
document.getElementById('settleBar').style.display = 'flex';
// 预加载价格信息
await Promise.all(cartItems.map(loadItemPrice));
render();
calcTotal();
} else {
showEmptyState();
}
}
function showEmptyState() {
document.getElementById('cartList').innerHTML = `
<div class="empty-state">
<i class="bi bi-cart-x empty-icon"></i>
<div>购物袋空空如也</div>
<a href="/index.html" class="btn btn-sm btn-outline-dark mt-3 rounded-pill px-4">去点餐</a>
</div>
`;
document.getElementById('settleBar').style.display = 'none';
}
async function loadItemPrice(item) {
let price = parseFloat(item.product?.basePrice || 0);
if(item.spec?.priceAdjust) price += parseFloat(item.spec.priceAdjust);
// 解析配料价格
if(item.toppings) {
try {
const tIds = JSON.parse(item.toppings);
if(tIds.length) {
if(!productCache[item.productId]) {
const pr = await fetch(`/products/${item.productId}`);
const pd = await pr.json();
productCache[item.productId] = pd.data?.toppings || [];
}
tIds.forEach(tid => {
const t = productCache[item.productId].find(x => x.id == tid);
if(t) price += parseFloat(t.price);
});
}
} catch(e){}
}
item._unitPrice = price;
}
function render() {
const list = document.getElementById('cartList');
list.innerHTML = cartItems.map((item, idx) => `
<div class="cart-item">
<img src="${item.product?.image || '/static/images/default.jpg'}" class="item-img" onclick="location.href='/product-detail.html?id=${item.product?.id}'">
<div class="item-info">
<div class="d-flex justify-content-between">
<div class="item-name">${item.product?.name}</div>
<i class="bi bi-x text-muted" style="font-size: 20px; cursor: pointer;" onclick="delItem(${item.id})"></i>
</div>
<div class="item-desc">${item.spec?.specName || '默认规格'}</div>
<div class="item-meta">
<div class="item-price">¥${item._unitPrice.toFixed(2)}</div>
<div class="mini-stepper">
<button class="mini-btn" onclick="updateQty(${item.id}, ${item.quantity-1})">-</button>
<span class="mini-val">${item.quantity}</span>
<button class="mini-btn" onclick="updateQty(${item.id}, ${item.quantity+1})">+</button>
</div>
</div>
</div>
</div>
`).join('');
}
function calcTotal() {
// 默认计算所有商品
let total = 0;
let count = 0;
cartItems.forEach(item => {
if (item._unitPrice !== undefined) {
total += item._unitPrice * item.quantity;
count += item.quantity;
}
});
document.getElementById('totalPrice').textContent = `¥${total.toFixed(2)}`;
document.getElementById('count').textContent = count;
}
async function updateQty(id, qty) {
if(qty < 1) return;
// 乐观更新UI
const item = cartItems.find(i => i.id === id);
if(item) {
const oldQty = item.quantity;
item.quantity = qty;
render();
calcTotal();
if (isLoggedIn) {
// 已登录:调用后端接口
try {
await fetch(`/cart/${id}?quantity=${qty}`, { method: 'PUT', credentials: 'include' });
} catch(e) {
item.quantity = oldQty; // 回滚
render();
calcTotal();
alert('更新失败');
}
} else {
// 未登录更新cookies
try {
let cookieCart = CookieUtils.getJSON('cartItems') || [];
const cookieItem = cookieCart.find(ci =>
ci.productId === item.productId &&
ci.specId === item.specId &&
ci.customSweetness === item.customSweetness &&
ci.customIce === item.customIce &&
ci.toppings === item.toppings
);
if (cookieItem) {
cookieItem.quantity = qty;
CookieUtils.setJSON('cartItems', cookieCart, 7);
}
} catch(e) {
item.quantity = oldQty; // 回滚
render();
calcTotal();
alert('更新失败');
}
}
}
}
async function delItem(id) {
if(confirm('确认将商品移出购物袋?')) {
if (isLoggedIn) {
// 已登录:调用后端接口
try {
await fetch(`/cart/${id}`, { method: 'DELETE', credentials: 'include' });
init();
} catch(e) {
alert('操作失败');
}
} else {
// 未登录从cookies删除
try {
const item = cartItems.find(i => i.id === id);
if (item) {
let cookieCart = CookieUtils.getJSON('cartItems') || [];
cookieCart = cookieCart.filter(ci =>
!(ci.productId === item.productId &&
ci.specId === item.specId &&
ci.customSweetness === item.customSweetness &&
ci.customIce === item.customIce &&
ci.toppings === item.toppings)
);
CookieUtils.setJSON('cartItems', cookieCart, 7);
init();
}
} catch(e) {
alert('操作失败');
}
}
}
}
function checkout() {
// 未登录用户需要先登录
if (!isLoggedIn) {
if (confirm('结算需要登录,是否前往登录?')) {
// 保存当前页面路径,登录后返回
sessionStorage.setItem('returnUrl', '/cart.html');
location.href = '/login.html';
}
return;
}
// 已登录:跳转到结算页面
location.href = '/checkout.html';
}
init();
</script>
</body>
</html>

View File

@@ -0,0 +1,616 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>确认订单</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
:root {
--primary: #0f172a;
--bg-body: #f8fafc;
--accent: #3b82f6;
}
body {
background: var(--bg-body);
padding-bottom: 120px;
font-family: -apple-system, sans-serif;
color: var(--primary);
overflow-x: hidden; /* 防止滑动动画导致横向滚动条 */
}
/* 顶部导航 */
.nav-header {
position: fixed; top: 0; left: 0; right: 0; height: 50px;
background: #fff; display: flex; align-items: center; justify-content: space-between;
padding: 0 16px; z-index: 1000; box-shadow: 0 1px 0 rgba(0,0,0,0.05);
}
.nav-title { font-size: 16px; font-weight: 600; }
.btn-back { border: none; background: transparent; font-size: 20px; padding: 0; }
/* 通用卡片 */
.section-card {
background: #fff; margin: 16px; padding: 16px; border-radius: 16px;
box-shadow: 0 1px 2px rgba(0,0,0,0.02);
transition: background 0.2s;
}
.section-card:active { background: #f8fafc; }
.card-header {
font-size: 14px; font-weight: 700; margin-bottom: 12px; display: flex; align-items: center;
}
.card-header i { margin-right: 6px; color: #64748b; font-size: 16px; }
/* 地址卡片 */
.address-card-content {
display: flex; align-items: center; justify-content: space-between;
min-height: 60px;
}
.addr-info { flex: 1; margin-right: 12px; }
.addr-top { font-size: 16px; font-weight: 700; margin-bottom: 4px; }
.addr-detail { font-size: 13px; color: #64748b; line-height: 1.4; }
.no-addr-hint { color: var(--accent); font-weight: 500; font-size: 14px; }
.icon-arrow { color: #cbd5e1; font-size: 14px; }
/* 商品列表 */
.checkout-item { display: flex; margin-bottom: 16px; }
.checkout-item:last-child { margin-bottom: 0; }
.item-img {
width: 56px; height: 56px; border-radius: 8px; background: #f1f5f9; object-fit: cover;
}
.item-content { flex: 1; margin-left: 12px; }
.item-row { display: flex; justify-content: space-between; align-items: flex-start; }
.item-name { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
.item-spec { font-size: 12px; color: #94a3b8; }
.item-price { font-size: 14px; font-weight: 600; }
.item-qty { font-size: 12px; color: #94a3b8; margin-left: 4px; }
/* 支付方式 */
.pay-option {
display: flex; align-items: center; padding: 12px 0; border-bottom: 1px solid #f1f5f9;
cursor: pointer;
}
.pay-option:last-child { border-bottom: none; }
.pay-icon { font-size: 20px; margin-right: 12px; }
.pay-wechat { color: #07c160; }
.pay-alipay { color: #1677ff; }
.pay-name { flex: 1; font-size: 14px; font-weight: 500; }
.pay-radio { accent-color: var(--primary); width: 18px; height: 18px; }
/* 备注输入 */
.remark-input {
width: 100%; border: none; background: #f8fafc; padding: 12px;
border-radius: 8px; font-size: 13px; color: var(--primary);
}
/* 底部结算栏 */
.bottom-bar {
position: fixed; bottom: 0; left: 0; right: 0;
background: #fff; padding: 12px 20px 30px;
box-shadow: 0 -4px 16px rgba(0,0,0,0.05);
display: flex; justify-content: space-between; align-items: center; z-index: 100;
}
.total-area { display: flex; align-items: baseline; }
.total-label { font-size: 12px; color: #64748b; margin-right: 4px; }
.total-val { font-size: 24px; font-weight: 800; color: var(--primary); }
.btn-submit {
background: var(--primary); color: #fff; border: none;
padding: 14px 32px; border-radius: 12px; font-weight: 600; font-size: 16px;
}
.btn-submit:active { transform: scale(0.98); opacity: 0.9; }
.btn-submit:disabled { background: #cbd5e1; }
/* ============================
伪二级页面:地址管理
============================ */
.page-layer {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: var(--bg-body); z-index: 2000;
transform: translateX(100%); transition: transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
display: flex; flex-direction: column;
}
.page-layer.active { transform: translateX(0); }
.layer-body {
flex: 1; overflow-y: auto; padding: 16px; padding-bottom: 100px;
margin-top: 50px; /* Header Height */
}
/* 地址管理列表项 */
.addr-manage-item {
background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 12px;
position: relative; border: 1px solid transparent;
}
.addr-manage-item.selected { border-color: var(--primary); background: #f8fafc; }
.addr-manage-top { display: flex; justify-content: space-between; margin-bottom: 6px; }
.addr-manage-name { font-weight: 700; font-size: 15px; }
.addr-manage-phone { font-weight: 400; color: #64748b; margin-left: 8px; font-size: 14px; }
.addr-manage-detail { color: #475569; font-size: 13px; line-height: 1.4; padding-right: 40px; }
.addr-tag { font-size: 10px; padding: 2px 6px; background: var(--primary); color: #fff; border-radius: 4px; vertical-align: middle; margin-right: 4px; }
/* 编辑按钮区域 */
.addr-manage-actions {
position: absolute; right: 16px; top: 50%; transform: translateY(-50%);
display: flex; flex-direction: column; gap: 12px;
padding-left: 12px; border-left: 1px solid #f1f5f9;
}
.action-icon { color: #94a3b8; font-size: 18px; padding: 4px; }
/* 底部新增按钮 */
.layer-footer {
position: fixed; bottom: 0; left: 0; right: 0;
background: #fff; padding: 12px 16px 30px;
box-shadow: 0 -4px 12px rgba(0,0,0,0.03);
}
/* 扁平化输入框 */
.flat-input {
width: 100%; padding: 12px 0; border: none; border-bottom: 1px solid #e2e8f0;
outline: none; transition: border-color 0.2s; background: transparent;
}
.flat-input:focus { border-color: var(--primary); }
/* === 修复模态框层级问题 === */
/* 确保模态框在伪二级页面(z-index: 2000)之上 */
.modal-backdrop { z-index: 2040 !important; }
.modal { z-index: 2050 !important; }
</style>
</head>
<body>
<!-- 主页面:顶部导航 -->
<div class="nav-header">
<button class="btn-back" onclick="history.back()"><i class="bi bi-arrow-left"></i></button>
<span class="nav-title">确认订单</span>
<div style="width: 24px;"></div>
</div>
<div style="height: 50px;"></div>
<!-- 主页面:地址卡片 -->
<div class="section-card" onclick="openAddressManager()">
<div class="card-header"><i class="bi bi-geo-alt-fill"></i> 配送地址</div>
<div class="address-card-content">
<div class="addr-info" id="addrDisplay">
<div class="d-flex align-items-center">
<div class="spinner-border spinner-border-sm text-muted"></div>
<span class="ms-2 small text-muted">加载地址...</span>
</div>
</div>
<i class="bi bi-chevron-right icon-arrow"></i>
</div>
<input type="hidden" id="selectedAddrId">
</div>
<!-- 主页面:商品清单 -->
<div class="section-card">
<div class="card-header"><i class="bi bi-bag-fill"></i> 订单商品</div>
<div id="itemList"></div>
</div>
<!-- 支付方式选择已迁移到支付页面 -->
<!-- 主页面:备注 -->
<div class="section-card">
<div class="card-header"><i class="bi bi-chat-left-text-fill"></i> 订单备注</div>
<textarea class="remark-input" id="remark" rows="2" placeholder="如有特殊需求,请在此填写"></textarea>
</div>
<!-- 主页面:底部操作栏 -->
<div class="bottom-bar">
<div class="total-area">
<span class="total-label">合计</span>
<span class="total-val" id="totalAmount">¥0.00</span>
</div>
<button class="btn-submit" onclick="submitOrder()">提交订单</button>
</div>
<!-- ========================================== -->
<!-- 伪二级页面:地址管理 (全屏覆盖) -->
<!-- ========================================== -->
<div id="addressManagerPage" class="page-layer">
<div class="nav-header">
<button class="btn-back" onclick="closeAddressManager()"><i class="bi bi-chevron-left"></i></button>
<span class="nav-title">选择地址</span>
<div style="width: 24px;"></div>
</div>
<div class="layer-body" id="addrManagerList">
<!-- 列表动态渲染 -->
</div>
<div class="layer-footer">
<button class="btn-submit w-100" onclick="openEditModal()">+ 新增收货地址</button>
</div>
</div>
<!-- 地址编辑模态框 (Bootstrap Modal) -->
<div class="modal fade" id="addrEditModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" style="border-radius: 16px; border:none;">
<div class="modal-header border-0">
<h5 class="modal-title fw-bold" id="modalTitle">新增地址</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addrForm">
<input type="hidden" id="editAddrId">
<div class="mb-3">
<input type="text" class="flat-input" id="editName" placeholder="收货人姓名" required>
</div>
<div class="mb-3">
<input type="tel" class="flat-input" id="editPhone" placeholder="手机号码" required>
</div>
<div class="mb-3">
<textarea class="flat-input" id="editDetail" rows="2" placeholder="详细地址 (街道/楼牌号)" required></textarea>
</div>
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" id="editDefault">
<label class="form-check-label small text-muted" for="editDefault">设为默认地址</label>
</div>
</form>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-pill" onclick="deleteCurrentAddr()" id="btnDelete" style="display:none;">删除</button>
<button type="button" class="btn btn-dark rounded-pill px-4" onclick="saveAddress()">保存并使用</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
let cartItems = [];
let addresses = [];
let currentSelectedAddr = null; // 全局变量存储当前选中对象
// 初始化
(async () => {
await Promise.all([loadAddresses(), loadCart()]);
})();
// --- 核心逻辑:加载地址 ---
async function loadAddresses() {
try {
const res = await UIEnhancements.enhancedFetch('/address', { credentials: 'include' });
const json = res.data;
if(json.code === 200) {
addresses = json.data || [];
// 策略:如果没有选中过,优先选默认,否则选第一个
if (!currentSelectedAddr && addresses.length > 0) {
currentSelectedAddr = addresses.find(a => a.isDefault) || addresses[0];
}
// 如果当前选中的地址被删了,重置
else if (currentSelectedAddr && !addresses.find(a => a.id === currentSelectedAddr.id)) {
currentSelectedAddr = addresses.find(a => a.isDefault) || addresses[0] || null;
}
renderCurrentAddress();
}
} catch(e) { console.error(e); }
}
// 渲染主页面地址卡片
function renderCurrentAddress() {
const el = document.getElementById('addrDisplay');
const hidden = document.getElementById('selectedAddrId');
if (currentSelectedAddr) {
hidden.value = currentSelectedAddr.id;
// 兼容字段名:后端可能返回 contactName/recipient 或 contactPhone/phone
const name = currentSelectedAddr.contactName || currentSelectedAddr.recipient;
const phone = currentSelectedAddr.contactPhone || currentSelectedAddr.phone;
el.innerHTML = `
<div class="addr-top">
${name} <span class="fw-normal ms-2 text-muted" style="font-size:14px;">${phone}</span>
</div>
<div class="addr-detail text-truncate">${currentSelectedAddr.address}</div>
`;
} else {
hidden.value = '';
el.innerHTML = `<span class="no-addr-hint">+ 请添加收货地址</span>`;
}
}
// --- 核心逻辑:加载购物车 ---
async function loadCart() {
try {
const res = await UIEnhancements.enhancedFetch('/cart', { credentials: 'include' });
const json = res.data;
if(json.code === 200) {
cartItems = json.data || [];
if(cartItems.length === 0) {
alert('购物袋为空,请先选购');
location.href = '/index.html';
return;
}
renderItems();
calcTotal();
} else if (json.message?.includes('登录')) {
location.href = '/login.html';
}
} catch(e) { console.error(e); }
}
function renderItems() {
const container = document.getElementById('itemList');
let html = '';
cartItems.forEach(item => {
let specDesc = item.spec?.specName || '';
let price = item.product.basePrice + (item.spec?.priceAdjust || 0);
// 简单处理配料显示
let toppingTxt = '';
try {
// 实际需要请求产品详情获得配料名,此处简化
// const tIds = JSON.parse(item.toppings || '[]');
// if(tIds.length) toppingTxt = ` +配料(${tIds.length})`;
} catch(e){}
html += `
<div class="checkout-item">
<img src="${item.product.image || '/static/images/default.jpg'}" class="item-img">
<div class="item-content">
<div class="item-row">
<span class="item-name">${item.product.name}</span>
<span class="item-price">¥${price}</span>
</div>
<div class="item-row mt-1">
<span class="item-spec">${specDesc}${toppingTxt}</span>
<span class="item-qty">x${item.quantity}</span>
</div>
</div>
</div>
`;
});
container.innerHTML = html;
}
async function calcTotal() {
let total = 0;
for (let item of cartItems) {
let p = item.product.basePrice + (item.spec?.priceAdjust || 0);
// 简化计算如果需要配料价格应在cart接口返回完整price
total += p * item.quantity;
}
document.getElementById('totalAmount').innerText = '¥' + total.toFixed(2);
}
// ==========================================
// 二级页面交互逻辑
// ==========================================
// 1. 打开地址管理页
function openAddressManager() {
const page = document.getElementById('addressManagerPage');
renderManagerList();
page.classList.add('active');
// 禁止背景滚动
document.body.style.overflow = 'hidden';
}
// 2. 关闭地址管理页
function closeAddressManager() {
const page = document.getElementById('addressManagerPage');
page.classList.remove('active');
document.body.style.overflow = '';
// 关闭时刷新主页显示(可能改了选中项)
renderCurrentAddress();
}
// 3. 渲染管理列表
function renderManagerList() {
const container = document.getElementById('addrManagerList');
if (addresses.length === 0) {
container.innerHTML = '<div class="text-center py-5 text-muted">暂无地址,请添加</div>';
return;
}
container.innerHTML = addresses.map(a => {
const name = a.contactName || a.recipient;
const phone = a.contactPhone || a.phone;
const isSelected = currentSelectedAddr && currentSelectedAddr.id === a.id;
return `
<div class="addr-manage-item ${isSelected ? 'selected' : ''}" onclick="selectAndClose('${a.id}')">
<div class="addr-manage-top">
<div class="text-truncate">
${a.isDefault ? '<span class="addr-tag">默认</span>' : ''}
<span class="addr-manage-name">${name}</span>
<span class="addr-manage-phone">${phone}</span>
</div>
</div>
<div class="addr-manage-detail">${a.address}</div>
<div class="addr-manage-actions" onclick="event.stopPropagation()">
<i class="bi bi-pencil-square action-icon" onclick="openEditModal('${a.id}')"></i>
</div>
</div>`;
}).join('');
}
// 4. 在管理页选中并返回
function selectAndClose(id) {
// == 比较以兼容 string/number
const target = addresses.find(a => a.id == id);
if (target) {
currentSelectedAddr = target;
closeAddressManager();
}
}
// ==========================================
// 增删改逻辑 (修复传参问题)
// ==========================================
let editingAddrId = null;
// 打开编辑/新增模态框
function openEditModal(id = null) {
const modal = new bootstrap.Modal(document.getElementById('addrEditModal'));
const form = document.getElementById('addrForm');
const btnDel = document.getElementById('btnDelete');
form.reset();
editingAddrId = id; // 保存当前编辑ID
if (id) {
document.getElementById('modalTitle').innerText = '编辑地址';
btnDel.style.display = 'block';
// 查找对象
const target = addresses.find(a => a.id == id);
if (target) {
document.getElementById('editName').value = target.contactName || target.recipient;
document.getElementById('editPhone').value = target.contactPhone || target.phone;
document.getElementById('editDetail').value = target.address;
document.getElementById('editDefault').checked = target.isDefault;
}
} else {
document.getElementById('modalTitle').innerText = '新增地址';
btnDel.style.display = 'none';
}
modal.show();
}
// 保存地址
async function saveAddress() {
const name = document.getElementById('editName').value.trim();
const phone = document.getElementById('editPhone').value.trim();
const detail = document.getElementById('editDetail').value.trim();
const isDefault = document.getElementById('editDefault').checked;
if (!name || !phone || !detail) return alert('请填写完整信息');
// 构造符合后端规范的 Payload
// 注意:使用后端实体类字段名 contactName 和 contactPhone
const payload = {
contactName: name,
contactPhone: phone,
address: detail,
isDefault: isDefault ? 1 : 0
};
// 如果是编辑,加上 ID
// 注意RESTful 风格通常把 ID 放在 URL 里PUT body 里不需要 ID但放了也无妨
try {
const url = editingAddrId ? `/address/${editingAddrId}` : '/address';
const method = editingAddrId ? 'PUT' : 'POST';
const res = await fetch(url, {
method: method,
headers: {'Content-Type': 'application/json'},
credentials: 'include',
body: JSON.stringify(payload)
});
const json = await res.json();
if (json.code === 200) {
// 成功后关闭模态框
bootstrap.Modal.getInstance(document.getElementById('addrEditModal')).hide();
// 重新加载列表
await loadAddresses();
renderManagerList(); // 刷新管理页列表
// 如果是新增的,自动选中
if (!editingAddrId) {
// 重新获取后,默认可能不是最新的,这里简单处理:不自动选中,让用户点
}
} else {
alert(json.message);
}
} catch (e) {
console.error(e);
alert('操作失败');
}
}
// 删除地址
async function deleteCurrentAddr() {
if (!editingAddrId) return;
if (!confirm('确认删除此地址?')) return;
try {
const res = await fetch(`/address/${editingAddrId}`, { method: 'DELETE' });
const json = await res.json();
if (json.code === 200) {
bootstrap.Modal.getInstance(document.getElementById('addrEditModal')).hide();
// 如果删的是当前选中的,清空选中态
if (currentSelectedAddr && currentSelectedAddr.id == editingAddrId) {
currentSelectedAddr = null;
}
await loadAddresses();
renderManagerList();
} else {
alert(json.message);
}
} catch (e) { alert('删除失败'); }
}
// ==========================================
// 提交订单(跳转到支付页面)
// ==========================================
async function submitOrder() {
if(!currentSelectedAddr) return alert('请选择收货地址');
const remark = document.getElementById('remark').value;
const btn = document.querySelector('.btn-submit');
btn.disabled = true;
btn.innerText = '提交中...';
try {
// === 提取 merchantId ===
// 从购物车第一项获取 merchantId后端也会从购物车验证但前端需要传参以满足验证要求
let merchantId = null;
if (cartItems.length > 0 && cartItems[0].product) {
merchantId = cartItems[0].product.merchantId;
}
if (!merchantId) {
alert('无法获取商家信息,请刷新页面重试');
btn.disabled = false;
btn.innerText = '提交订单';
return;
}
// 构造符合后端 OrderCreateRequest 的 payload
// 后端只需要merchantId, address, contactPhone
const payload = {
merchantId: merchantId,
address: currentSelectedAddr.address,
contactPhone: currentSelectedAddr.contactPhone || currentSelectedAddr.phone
};
const res = await fetch('/order/create', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
credentials: 'include',
body: JSON.stringify(payload)
});
const json = await res.json();
if(json.code === 200) {
const orderId = json.data.id || json.data;
// 跳转到支付页面而不是订单详情页
location.href = `/payment.html?orderId=${orderId}`;
} else {
alert(json.message);
btn.disabled = false;
btn.innerText = '提交订单';
}
} catch(e) {
console.error(e);
alert('提交失败');
btn.disabled = false;
btn.innerText = '提交订单';
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,652 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首页 - 荣光奶茶店</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
:root {
--primary-color: #3b82f6;
--bg-body: #f8fafc;
--text-main: #1e293b;
--text-sub: #64748b;
/* 修改 1: 加大圆角至 24px营造超级圆润的视觉感 */
--radius-box: 24px;
--card-shadow: 0 8px 24px rgba(149, 157, 165, 0.08);
}
body {
background-color: var(--bg-body);
color: var(--text-main);
font-family: -apple-system, "SF Pro Text", Roboto, "Helvetica Neue", sans-serif;
padding-bottom: 80px;
-webkit-font-smoothing: antialiased;
}
/* --- 店铺头部卡片样式 --- */
.store-header-card {
background: #ffffff;
border-radius: var(--radius-box);
padding: 20px;
margin-bottom: 24px;
display: flex;
align-items: center;
box-shadow: var(--card-shadow);
position: relative;
overflow: hidden;
}
.store-header-card::after {
content: '';
position: absolute;
top: -20px;
right: -20px;
width: 100px;
height: 100px;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0) 70%);
border-radius: 50%;
z-index: 0;
}
.store-logo-wrapper {
width: 64px;
height: 64px;
flex-shrink: 0;
margin-right: 16px;
position: relative;
z-index: 1;
}
.store-logo {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
border: 2px solid #f1f5f9;
}
.store-info {
flex: 1;
z-index: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.store-name {
font-size: 20px;
font-weight: 800;
color: var(--text-main);
margin: 0 0 4px 0;
letter-spacing: -0.5px;
}
.store-meta {
font-size: 12px;
color: var(--text-sub);
display: flex;
align-items: center;
gap: 12px;
}
.store-meta span {
display: flex;
align-items: center;
gap: 4px;
}
.store-actions {
margin-left: 8px;
z-index: 1;
}
.btn-icon-circle {
width: 36px;
height: 36px;
border-radius: 50%;
background: #f8fafc;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-main);
border: none;
transition: all 0.2s;
}
.btn-icon-circle:active {
transform: scale(0.9);
background: #e2e8f0;
}
/* --- 基础布局样式 --- */
.section-header {
margin: 24px 0 16px;
padding: 0 4px;
display: flex;
align-items: center;
}
.section-header h2 {
font-size: 18px;
font-weight: 700;
margin: 0;
color: var(--text-main);
}
.section-header i {
margin-right: 8px;
color: var(--primary-color);
}
.banner-carousel {
border-radius: var(--radius-box);
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
margin-bottom: 24px;
background: #fff;
}
.carousel-item {
padding: 20px;
min-height: 120px;
background: linear-gradient(135deg, #eff6ff 0%, #ffffff 100%);
cursor: pointer;
}
.carousel-title {
font-size: 18px;
font-weight: 700;
color: var(--primary-color);
margin-bottom: 8px;
}
.carousel-content {
font-size: 14px;
color: #475569;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.carousel-indicators [data-bs-target] {
background-color: var(--primary-color);
width: 6px; height: 6px; border-radius: 50%;
}
/* --- 商品卡片样式 --- */
.product-card {
border: none;
border-radius: var(--radius-box);
background: #fff;
height: 100%;
overflow: hidden;
cursor: pointer;
box-shadow: 0 4px 20px rgba(0,0,0,0.03);
transition: transform 0.2s ease, box-shadow 0.2s ease;
display: flex;
flex-direction: column;
position: relative;
}
.product-card:active {
transform: scale(0.97);
}
/* 修改 2: 图片容器不需要动,但父级的圆角变大会自动裁剪 */
.img-wrapper {
position: relative;
width: 100%;
padding-top: 100%;
background: #f8fafc;
}
.img-wrapper img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: opacity 0.3s;
}
/* 修改 3: 稍微增加内边距,配合大圆角,避免文字贴边 */
.card-body {
padding: 14px;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.card-main-info {
margin-bottom: 8px;
}
.card-title {
font-size: 15px;
font-weight: 700;
color: var(--text-main);
margin-bottom: 4px;
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-desc {
font-size: 11px;
color: #94a3b8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.2;
}
.card-footer-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
}
.price-text {
font-size: 17px;
font-weight: 800;
color: var(--text-main);
letter-spacing: -0.5px;
display: flex;
align-items: baseline;
}
.price-text::before {
content: '¥';
font-size: 11px;
margin-right: 1px;
font-weight: 600;
color: var(--text-main);
}
.btn-add-icon {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--primary-color);
color: white;
border: none;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 10px rgba(59, 130, 246, 0.3);
transition: background 0.2s, transform 0.2s;
}
.btn-add-icon:active {
background: #2563eb;
transform: scale(0.9);
}
.btn-add-icon i {
font-size: 18px;
margin-top: 1px;
}
.mobile-tabbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 8px 0 20px;
display: flex;
justify-content: space-around;
box-shadow: 0 -1px 0 rgba(0,0,0,0.03);
z-index: 1000;
}
.tab-item {
text-decoration: none;
color: #94a3b8;
display: flex;
flex-direction: column;
align-items: center;
font-size: 10px;
flex: 1;
}
.tab-item i {
font-size: 24px;
margin-bottom: 2px;
}
.tab-item.active {
color: var(--primary-color);
}
/* 模态框也同步变圆润 */
.modal-content {
border: none;
border-radius: 24px;
}
.modal-header {
border-bottom: 1px solid #f1f5f9;
}
/* 加载提示样式 */
.loading-tip, .no-more-tip {
margin-top: 16px;
}
.spin {
display: inline-block;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container mt-3">
<!-- 店铺信息卡片 -->
<div class="store-header-card">
<div class="store-logo-wrapper">
<img src="https://picsum.photos/200/200?random=1" alt="Logo" class="store-logo">
</div>
<div class="store-info">
<h1 class="store-name">荣光奶茶店</h1>
<div class="store-meta">
<span>
<i class="bi bi-geo-alt-fill text-danger"></i> 科技园南区C栋
</span>
<span>
<i class="bi bi-star-fill text-warning"></i> 4.9
</span>
</div>
</div>
<div class="store-actions">
<button class="btn-icon-circle">
<i class="bi bi-telephone-fill" style="font-size: 16px;"></i>
</button>
</div>
</div>
<!-- 公告轮播区 -->
<div id="announcementCarousel" class="carousel slide banner-carousel" data-bs-ride="carousel" style="display: none;">
<div class="carousel-indicators" id="carouselIndicators"></div>
<div class="carousel-inner" id="carouselInner"></div>
</div>
<!-- 推荐区 -->
<div class="section-header">
<i class="bi bi-fire text-danger"></i>
<h2>店长推荐</h2>
</div>
<div id="recommendProducts" class="row gx-3 gy-3"></div>
<!-- 全部商品 -->
<div class="section-header mt-4">
<i class="bi bi-grid-fill"></i>
<h2>全部饮品</h2>
</div>
<div id="allProducts" class="row gx-3 gy-3"></div>
</div>
<!-- 公告模态框 -->
<div class="modal fade" id="announcementModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold" id="announcementModalTitle">公告详情</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="announcementModalBody"></div>
</div>
</div>
</div>
<!-- 逻辑脚本 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// --- 核心逻辑 ---
// 分页状态管理(仅用于全部商品列表)
let currentPage = 1;
let hasMore = true;
let isLoading = false;
const pageSize = 12;
async function loadRecommendations() {
try {
const { data } = await UIEnhancements.enhancedFetch('/recommend?limit=6', { credentials: 'include' });
if (data?.code === 200 && data.data) {
renderProducts(data.data, 'recommendProducts', false);
} else {
showEmpty('recommendProducts', '暂无推荐');
}
} catch (e) { console.error(e); }
}
async function loadProducts(pageNum = 1, append = false) {
if (isLoading) return false;
if (!hasMore && append) return false;
isLoading = true;
try {
const { data } = await UIEnhancements.enhancedFetch(`/products?pageNum=${pageNum}&pageSize=${pageSize}`, { credentials: 'include' });
if (data?.code === 200 && data.data) {
const products = data.data.list || [];
const total = data.data.total || 0;
const totalPages = Math.ceil(total / pageSize);
if (products.length > 0) {
renderProducts(products, 'allProducts', append);
currentPage = pageNum;
hasMore = pageNum < totalPages;
// 移除加载提示
removeLoadingTip();
return true;
} else {
if (!append) {
showEmpty('allProducts', '暂无商品');
} else {
showNoMoreTip();
}
hasMore = false;
return false;
}
} else {
if (!append) {
showEmpty('allProducts', '暂无商品');
}
hasMore = false;
return false;
}
} catch (e) {
console.error(e);
if (!append) {
showEmpty('allProducts', '加载失败');
}
return false;
} finally {
isLoading = false;
}
}
// 更新后的渲染函数
function renderProducts(products, containerId, append = false) {
const container = document.getElementById(containerId);
if (!products?.length) {
if (!append) {
return showEmpty(containerId);
}
return;
}
const html = products.map(p => `
<div class="col-6 col-md-3">
<div class="product-card" onclick="location.href='/product-detail.html?id=${p.id}'">
<div class="img-wrapper">
<img src="${p.image || '/static/images/default.jpg'}" loading="lazy">
</div>
<div class="card-body">
<div class="card-main-info">
<div class="card-title">${p.name}</div>
<div class="card-desc">${p.description || p.category || '暂无描述'}</div>
</div>
<div class="card-footer-row">
<span class="price-text">${p.basePrice}</span>
<button class="btn-add-icon" aria-label="添加">
<i class="bi bi-plus"></i>
</button>
</div>
</div>
</div>
</div>
`).join('');
if (append) {
// 追加模式:在容器末尾追加
container.insertAdjacentHTML('beforeend', html);
} else {
// 替换模式:直接替换内容
container.innerHTML = html;
}
}
// 显示加载提示
function showLoadingTip() {
const container = document.getElementById('allProducts');
const existingTip = container.querySelector('.loading-tip');
if (existingTip) return;
const tip = document.createElement('div');
tip.className = 'loading-tip col-12 text-center text-muted py-3 small';
tip.innerHTML = '<i class="bi bi-arrow-repeat spin"></i> 加载中...';
container.appendChild(tip);
}
// 移除加载提示
function removeLoadingTip() {
const container = document.getElementById('allProducts');
const tip = container.querySelector('.loading-tip');
if (tip) tip.remove();
}
// 显示没有更多数据提示
function showNoMoreTip() {
const container = document.getElementById('allProducts');
const existingTip = container.querySelector('.no-more-tip');
if (existingTip) return;
const tip = document.createElement('div');
tip.className = 'no-more-tip col-12 text-center text-muted py-3 small';
tip.textContent = '没有更多商品了';
container.appendChild(tip);
}
function showEmpty(id, text='暂无数据') {
document.getElementById(id).innerHTML = `<div class="col-12 text-center text-muted py-4 small">${text}</div>`;
}
async function loadAnnouncements() {
try {
const { data } = await UIEnhancements.enhancedFetch('/announcements', { credentials: 'include' });
if (data?.code === 200 && data.data?.length) {
renderAnnouncementsCarousel(data.data);
}
} catch (e) { console.error(e); }
}
function renderAnnouncementsCarousel(list) {
const inner = document.getElementById('carouselInner');
const indicators = document.getElementById('carouselIndicators');
const container = document.getElementById('announcementCarousel');
if (!list.length) return;
container.style.display = 'block';
inner.innerHTML = list.map((a, i) => `
<div class="carousel-item ${i === 0 ? 'active' : ''}" onclick="showAnnouncement(${a.id})">
<div class="d-flex align-items-center mb-2">
<span class="badge ${a.type === 'ACTIVITY' ? 'bg-warning text-dark' : 'bg-primary'} me-2">
${a.type === 'ACTIVITY' ? '活动' : '通知'}
</span>
<div class="carousel-title mb-0">${a.title}</div>
</div>
<div class="carousel-content">${a.content}</div>
</div>
`).join('');
indicators.innerHTML = list.map((_, i) => `
<button type="button" data-bs-target="#announcementCarousel" data-bs-slide-to="${i}"
class="${i === 0 ? 'active' : ''}"></button>
`).join('');
}
async function showAnnouncement(id) {
const modal = new bootstrap.Modal(document.getElementById('announcementModal'));
// 显示加载状态
document.getElementById('announcementModalBody').innerHTML = '<div class="text-center py-3">加载中...</div>';
modal.show();
try {
const { data } = await UIEnhancements.enhancedFetch('/announcements', { credentials: 'include' });
const item = data?.data?.find(i => i.id === id);
if (item) {
document.getElementById('announcementModalTitle').innerText = item.title;
document.getElementById('announcementModalBody').innerHTML = `
<div class="mb-3 text-muted small">
${item.type === 'ACTIVITY' ? '<span class="badge bg-warning text-dark me-2">活动</span>' : '<span class="badge bg-primary me-2">通知</span>'}
${new Date(item.createTime).toLocaleString()}
</div>
<div style="line-height:1.6;white-space:pre-wrap;font-size:15px;color:#334155;">${item.content}</div>
`;
} else {
document.getElementById('announcementModalBody').innerHTML = '公告未找到';
}
} catch(e) {
document.getElementById('announcementModalBody').innerHTML = '加载失败';
}
}
// 触底加载
function handleScroll() {
if (isLoading || !hasMore) return;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
// 距离底部100px时触发加载
if (scrollTop + windowHeight >= documentHeight - 100) {
showLoadingTip();
loadProducts(currentPage + 1, true);
}
}
// 添加滚动监听(使用防抖)
let scrollTimer = null;
window.addEventListener('scroll', () => {
if (scrollTimer) clearTimeout(scrollTimer);
scrollTimer = setTimeout(handleScroll, 100);
});
// Init
loadAnnouncements();
loadRecommendations();
loadProducts(1, false);
</script>
<!-- 底部导航 -->
<div class="mobile-tabbar">
<a href="/index.html" class="tab-item active">
<i class="bi bi-cup-hot-fill"></i>
<span>点餐</span>
</a>
<a href="/cart.html" class="tab-item">
<i class="bi bi-bag"></i>
<span>购物袋</span>
</a>
<a href="/orders.html" class="tab-item">
<i class="bi bi-receipt"></i>
<span>订单</span>
</a>
<a href="/profile.html" class="tab-item">
<i class="bi bi-person"></i>
<span>我的</span>
</a>
</div>
</body>
</html>

View File

@@ -0,0 +1,236 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
:root {
--primary-color: #0f172a;
--bg-body: #ffffff;
--input-bg: #f1f5f9;
--text-muted: #94a3b8;
}
body {
background: var(--bg-body);
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
padding: 40px;
font-family: -apple-system, "SF Pro Text", "Helvetica Neue", sans-serif;
color: var(--primary-color);
}
.brand-area { margin-bottom: 48px; }
.brand-title { font-size: 32px; font-weight: 800; letter-spacing: -1px; margin-bottom: 8px; }
.brand-subtitle { color: #64748b; font-size: 15px; font-weight: 400; }
/* 现代输入框容器 */
.input-group-modern {
background: var(--input-bg);
border-radius: 16px;
padding: 4px 16px;
display: flex;
align-items: center;
margin-bottom: 20px;
border: 1px solid transparent;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
/* 聚焦态:背景变白,加边框和阴影 */
.input-group-modern:focus-within {
background: #fff;
border-color: var(--primary-color);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
transform: translateY(-1px);
}
.input-icon {
font-size: 20px;
color: var(--text-muted);
margin-right: 12px;
transition: color 0.3s;
}
.input-group-modern:focus-within .input-icon {
color: var(--primary-color);
}
.modern-input {
border: none;
background: transparent;
width: 100%;
padding: 12px 0;
font-size: 16px;
outline: none;
color: var(--primary-color);
font-weight: 500;
}
.modern-input::placeholder { color: #cbd5e1; font-weight: 400; }
/* 按钮 */
.btn-modern {
width: 100%;
background: var(--primary-color);
color: white;
padding: 18px;
border-radius: 16px;
font-weight: 600;
font-size: 16px;
border: none;
margin-top: 24px;
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.2);
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-modern:active { transform: scale(0.98); box-shadow: none; }
.btn-modern:disabled { opacity: 0.7; cursor: not-allowed; }
.link-area { text-align: center; margin-top: 32px; }
.link-area a {
color: #64748b;
text-decoration: none;
font-size: 14px;
font-weight: 500;
display: inline-flex;
align-items: center;
}
.link-area a span { color: #3b82f6; margin-left: 4px; }
</style>
</head>
<body>
<div class="brand-area">
<div class="brand-title">欢迎回来</div>
<div class="brand-subtitle">登录您的账号以享受美味</div>
</div>
<form id="loginForm">
<div class="input-group-modern">
<i class="bi bi-person input-icon"></i>
<input type="text" class="modern-input" id="username" placeholder="请输入用户名" required>
</div>
<div class="input-group-modern">
<i class="bi bi-lock input-icon"></i>
<input type="password" class="modern-input" id="password" placeholder="请输入密码" required>
</div>
<button type="submit" class="btn-modern">立即登录</button>
</form>
<div class="link-area">
<a href="/register.html">
还没有账号? <span>去注册</span>
</a>
</div>
<script src="/static/js/cookie-utils.js"></script>
<script>
// 同步cookies中的浏览行为
async function syncViewHistory() {
try {
const viewHistory = CookieUtils.getJSON('viewHistory') || [];
if (viewHistory.length === 0) {
return;
}
// 提取productId列表
const productIds = viewHistory.map(v => v.productId);
// 调用后端接口批量记录
const res = await fetch('/products/record-views', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(productIds),
credentials: 'include'
});
if (res.ok) {
// 同步成功后清除cookies
CookieUtils.remove('viewHistory');
}
} catch(e) {
console.error('同步浏览历史失败:', e);
// 失败不影响登录流程,只记录日志
}
}
// 同步cookies中的购物车数据
async function syncCartItems() {
try {
const cartItems = CookieUtils.getJSON('cartItems') || [];
if (cartItems.length === 0) {
return;
}
// 调用后端接口同步购物车
const res = await fetch('/cart/sync', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(cartItems),
credentials: 'include'
});
if (res.ok) {
// 同步成功后清除cookies
CookieUtils.remove('cartItems');
}
} catch(e) {
console.error('同步购物车失败:', e);
// 失败不影响登录流程,只记录日志
}
}
document.getElementById('loginForm').onsubmit = async (e) => {
e.preventDefault();
const btn = document.querySelector('button');
const old = btn.textContent;
btn.textContent = '登录中...';
btn.disabled = true;
try {
const res = await fetch('/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: document.getElementById('username').value,
password: document.getElementById('password').value
}),
credentials: 'include'
});
const data = await res.json();
if(data.code === 200) {
// 登录成功同步cookies中的数据
btn.textContent = '同步数据中...';
// 并行同步浏览历史和购物车
await Promise.all([
syncViewHistory(),
syncCartItems()
]);
// 获取返回URL或默认跳转到首页
const returnUrl = sessionStorage.getItem('returnUrl') || '/index.html';
sessionStorage.removeItem('returnUrl');
// 跳转
location.href = returnUrl;
} else {
alert(data.message);
btn.textContent = old;
btn.disabled = false;
}
} catch(err) {
console.error('登录错误:', err);
alert('网络错误,请稍后重试');
btn.textContent = old;
btn.disabled = false;
}
};
</script>
</body>
</html>

View File

@@ -0,0 +1,356 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>订单详情</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
:root {
--bg-body: #f8fafc;
--text-primary: #0f172a;
--primary-color: #3b82f6;
}
body {
background: var(--bg-body);
padding-bottom: 90px;
font-family: -apple-system, sans-serif;
color: #334155;
}
/* 顶部导航 Header */
.nav-header {
position: fixed; top: 0; left: 0; right: 0; height: 50px;
background: #fff;
display: flex; align-items: center; justify-content: space-between;
padding: 0 16px; z-index: 1000; box-shadow: 0 1px 0 rgba(0,0,0,0.05);
}
.nav-title { font-size: 16px; font-weight: 600; color: var(--text-primary); }
.btn-back { border: none; background: transparent; font-size: 20px; color: var(--text-primary); padding: 0; }
/* 状态头 */
.status-header {
background: #fff; padding: 20px; margin-bottom: 12px;
margin-top: 50px; /* 避开Header */
}
.status-title { font-size: 20px; font-weight: 800; color: var(--text-primary); margin-bottom: 4px; }
.status-desc { font-size: 13px; color: #64748b; }
/* 进度追踪 - 极简风 */
.track-list { margin-top: 20px; position: relative; }
.track-item { display: flex; margin-bottom: 24px; position: relative; }
.track-item:last-child { margin-bottom: 0; }
/* 连接线 */
.track-item:not(:last-child)::before {
content: ''; position: absolute; left: 7px; top: 20px; bottom: -20px;
width: 2px; background: #e2e8f0;
}
.track-dot {
width: 16px; height: 16px; border-radius: 50%; background: #cbd5e1;
margin-right: 16px; flex-shrink: 0; margin-top: 4px; border: 3px solid #fff; box-shadow: 0 0 0 1px #cbd5e1;
}
.track-item.active .track-dot {
background: var(--primary-color); box-shadow: 0 0 0 1px var(--primary-color);
}
.track-content { flex: 1; }
.track-status { font-size: 14px; font-weight: 600; color: var(--text-primary); }
.track-time { font-size: 12px; color: #94a3b8; margin-top: 2px; }
/* 通用卡片 */
.info-card { background: #fff; padding: 16px; margin-bottom: 12px; }
.card-title { font-size: 14px; font-weight: 700; color: var(--text-primary); margin-bottom: 12px; display: flex; align-items: center; }
.card-title i { margin-right: 6px; font-size: 16px; color: #64748b; }
/* 商品列表 */
.order-item { display: flex; margin-bottom: 16px; align-items: flex-start; }
.order-item:last-child { margin-bottom: 0; }
.item-img {
width: 60px; height: 60px; border-radius: 8px; object-fit: cover;
background: #f1f5f9; margin-right: 12px; cursor: pointer; flex-shrink: 0;
transition: transform 0.2s;
}
.item-img:hover { transform: scale(1.05); }
.item-info { flex: 1; }
.item-name { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
.item-spec { font-size: 12px; color: #64748b; margin-bottom: 4px; }
.item-price-row { display: flex; justify-content: space-between; align-items: baseline; }
.item-price { font-size: 14px; font-weight: 600; color: var(--text-primary); }
.item-qty { font-size: 12px; color: #94a3b8; margin-left: 8px; }
.item-subtotal { font-size: 13px; color: #64748b; text-align: right; }
/* 详情行 */
.detail-row { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 8px; }
.detail-label { color: #64748b; }
.detail-val { color: var(--text-primary); font-weight: 500; text-align: right; max-width: 70%; }
.total-row {
border-top: 1px solid #f1f5f9; margin-top: 12px; padding-top: 12px;
display: flex; justify-content: space-between; align-items: center;
}
.total-price { font-size: 18px; font-weight: 800; color: var(--text-primary); }
/* 底部操作 */
.bottom-bar {
position: fixed; bottom: 0; left: 0; right: 0;
background: #fff; padding: 12px 16px 20px;
box-shadow: 0 -4px 12px rgba(0,0,0,0.05);
display: flex; justify-content: flex-end; gap: 12px;
z-index: 100;
}
.btn-outline {
padding: 8px 20px; border-radius: 20px; font-size: 13px; font-weight: 600;
background: #fff; border: 1px solid #cbd5e1; color: #475569;
}
.btn-primary-action {
padding: 8px 20px; border-radius: 20px; font-size: 13px; font-weight: 600;
background: #0f172a; border: 1px solid #0f172a; color: #fff;
}
/* 商品推荐模块 */
.recommend-section {
margin: 24px 16px;
padding: 20px 16px;
background: #fff;
border-radius: 16px;
}
.recommend-title {
font-size: 16px; font-weight: 700; color: var(--text-primary);
margin-bottom: 16px; display: flex; align-items: center;
}
.recommend-title i { margin-right: 8px; color: var(--primary-color); }
.recommend-grid {
display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;
}
.recommend-item {
background: #f8fafc; border-radius: 12px; overflow: hidden;
cursor: pointer; transition: transform 0.2s;
}
.recommend-item:active { transform: scale(0.98); }
.recommend-img {
width: 100%; height: 120px; object-fit: cover; background: #f1f5f9;
}
.recommend-info {
padding: 10px;
}
.recommend-name {
font-size: 13px; font-weight: 600; color: var(--text-primary);
margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.recommend-price {
font-size: 14px; font-weight: 700; color: var(--text-primary);
}
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="nav-header">
<button class="btn-back" onclick="history.back()"><i class="bi bi-arrow-left"></i></button>
<span class="nav-title">订单详情</span>
<div style="width: 24px;"></div>
</div>
<div id="loading" class="text-center p-5 text-muted" style="margin-top: 50px;">加载中...</div>
<div id="content" style="display: none;">
<!-- 头部状态 -->
<div class="status-header">
<div class="status-title" id="orderStatusText">--</div>
<div class="status-desc">感谢您的光临</div>
<div class="track-list" id="trackList"></div>
</div>
<!-- 商品明细 -->
<div class="info-card">
<div class="card-title"><i class="bi bi-bag"></i> 商品明细</div>
<div id="itemList"></div>
<div class="total-row">
<span>合计</span>
<span class="total-price" id="totalAmount">¥0.00</span>
</div>
</div>
<!-- 配送信息 -->
<div class="info-card">
<div class="card-title"><i class="bi bi-geo-alt"></i> 配送信息</div>
<div class="detail-row">
<span class="detail-label">收货人</span>
<span class="detail-val" id="contactInfo">--</span>
</div>
<div class="detail-row">
<span class="detail-label">收货地址</span>
<span class="detail-val" id="addressInfo">--</span>
</div>
</div>
<!-- 订单信息 -->
<div class="info-card">
<div class="card-title"><i class="bi bi-file-text"></i> 订单信息</div>
<div class="detail-row">
<span class="detail-label">订单编号</span>
<span class="detail-val" id="orderNo">--</span>
</div>
<div class="detail-row">
<span class="detail-label">下单时间</span>
<span class="detail-val" id="createTime">--</span>
</div>
<div class="detail-row">
<span class="detail-label">支付方式</span>
<span class="detail-val" id="payMethod">--</span>
</div>
</div>
<div style="height: 20px;"></div>
<!-- 商品推荐模块 -->
<div class="recommend-section" id="recommendSection" style="display: none;">
<div class="recommend-title">
<i class="bi bi-fire"></i>
<span>为您推荐</span>
</div>
<div class="recommend-grid" id="recommendGrid"></div>
</div>
</div>
<!-- 底部操作栏 -->
<div class="bottom-bar" id="actionBar">
<button class="btn-outline" onclick="history.back()">返回列表</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script src="/static/js/custom-modal.js"></script>
<script>
const oid = new URLSearchParams(location.search).get('id');
let currentOrder = null;
async function init() {
if(!oid) return alert('参数错误');
try {
const { data } = await UIEnhancements.enhancedFetch(`/order/orders/${oid}`, { credentials: 'include' });
if(data.code === 200) {
currentOrder = data.data.order;
render(data.data);
document.getElementById('loading').style.display = 'none';
document.getElementById('content').style.display = 'block';
// 加载推荐商品
loadRecommendations();
} else {
alert(data.message);
}
} catch(e) {
console.error(e);
alert('加载失败');
}
}
// 加载推荐商品
async function loadRecommendations() {
try {
const res = await fetch('/recommend?limit=4', { credentials: 'include' });
const data = await res.json();
if (data.code === 200 && data.data && data.data.length > 0) {
renderRecommendations(data.data);
document.getElementById('recommendSection').style.display = 'block';
}
} catch(e) { console.error(e); }
}
function renderRecommendations(products) {
const container = document.getElementById('recommendGrid');
container.innerHTML = products.map(p => `
<div class="recommend-item" onclick="location.href='/product-detail.html?id=${p.id}'">
<img src="${p.image || '/static/images/default.jpg'}" class="recommend-img" alt="${p.name}">
<div class="recommend-info">
<div class="recommend-name">${p.name}</div>
<div class="recommend-price">¥${p.basePrice}</div>
</div>
</div>
`).join('');
}
function render(data) {
const o = data.order;
const items = data.items || [];
const tracks = data.tracks || [];
// 基础信息
document.getElementById('orderStatusText').textContent = getStatusText(o.orderStatus);
document.getElementById('totalAmount').textContent = `¥${o.totalAmount}`;
document.getElementById('contactInfo').textContent = `${o.contactPhone}`;
document.getElementById('addressInfo').textContent = o.address;
document.getElementById('orderNo').textContent = o.orderNo;
document.getElementById('createTime').textContent = new Date(o.createTime).toLocaleString();
document.getElementById('payMethod').textContent = o.paymentMethod === 'wechat' ? '微信支付' : (o.paymentMethod === 'alipay' ? '支付宝' : '未支付');
// 渲染商品
const productImages = data.productImages || {};
document.getElementById('itemList').innerHTML = items.map(i => {
let extra = '';
if(i.specName) extra += i.specName + ' ';
if(i.customSweetness) extra += `甜度: ${i.customSweetness} `;
if(i.customIce) extra += `冰度: ${i.customIce} `;
// 可以增加 Topping 展示
const productImage = productImages[i.productId] || '/static/images/default.jpg';
const subtotal = (parseFloat(i.price) * i.quantity).toFixed(2);
return `
<div class="order-item">
<img src="${productImage}" class="item-img"
onclick="location.href='/product-detail.html?id=${i.productId}'"
alt="${i.productName}">
<div class="item-info">
<div class="item-name">${i.productName}</div>
<div class="item-spec">${extra}</div>
<div class="item-price-row">
<div>
<span class="item-price">¥${i.price}</span>
<span class="item-qty">x${i.quantity}</span>
</div>
<div class="item-subtotal">小计: ¥${subtotal}</div>
</div>
</div>
</div>
`;
}).join('');
// 渲染进度 (Tracks)
if(tracks.length > 0) {
document.getElementById('trackList').innerHTML = tracks.map((t, idx) => `
<div class="track-item ${idx === 0 ? 'active' : ''}">
<div class="track-dot"></div>
<div class="track-content">
<div class="track-status">${getStatusText(t.status)}</div>
<div class="track-time">${new Date(t.createTime).toLocaleString()}</div>
</div>
</div>
`).join('');
} else {
document.getElementById('trackList').innerHTML = '<div class="text-muted small">暂无物流信息</div>';
}
// 底部按钮
const bar = document.getElementById('actionBar');
if(o.orderStatus === 'PENDING_PAY') {
const btn = document.createElement('button');
btn.className = 'btn-primary-action';
btn.textContent = '立即支付';
btn.onclick = () => goToPayment(o.id);
bar.appendChild(btn);
}
}
function getStatusText(s) {
const map = { 'PENDING_PAY': '待支付', 'MAKING': '制作中', 'READY': '待取餐', 'COMPLETED': '已完成', 'CANCELLED': '已取消' };
return map[s] || s;
}
// 跳转到支付页面
function goToPayment(orderId) {
window.location.href = `/payment.html?orderId=${orderId}`;
}
init();
</script>
</body>
</html>

View File

@@ -0,0 +1,441 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的订单</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
:root { --bg-body: #f8fafc; --primary: #0f172a; --accent: #3b82f6; }
body { background: var(--bg-body); padding-bottom: 80px; font-family: -apple-system, sans-serif; }
/* 筛选条 */
.filter-bar {
position: sticky; top: 0; background: var(--bg-body); z-index: 10;
padding: 16px 16px 8px; overflow-x: auto; white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
.filter-chip {
display: inline-block; padding: 6px 16px; margin-right: 8px;
background: #fff; border: 1px solid #e2e8f0; border-radius: 20px;
font-size: 13px; color: #64748b; transition: all 0.2s; cursor: pointer;
}
.filter-chip.active {
background: var(--primary); color: #fff; border-color: var(--primary);
}
/* 订单卡片 */
.order-card {
background: #fff; margin: 0 16px 16px; padding: 16px;
border-radius: 16px; border: none; box-shadow: 0 1px 2px rgba(0,0,0,0.02);
}
.order-top {
display: flex; justify-content: space-between; margin-bottom: 12px; font-size: 12px;
}
.order-no { color: #94a3b8; }
.order-status { font-weight: 600; }
.st-PENDING_PAY { color: #f59e0b; }
.st-MAKING { color: var(--accent); }
.st-COMPLETED { color: #10b981; }
.order-info { margin-bottom: 12px; }
.order-price { font-size: 16px; font-weight: 700; color: var(--primary); }
.order-date { font-size: 12px; color: #94a3b8; margin-top: 4px; }
.order-actions {
border-top: 1px solid #f8fafc; padding-top: 12px; display: flex; justify-content: flex-end; gap: 8px;
}
.btn-act {
padding: 6px 14px; border-radius: 18px; font-size: 12px; font-weight: 500;
background: #fff; border: 1px solid #cbd5e1; color: #475569;
}
.btn-act.primary { background: var(--primary); border-color: var(--primary); color: #fff; }
/* 评价展示样式 */
.review-display-stars { color: #f59e0b; letter-spacing: 2px; }
.review-reply-box { background: #f8fafc; padding: 12px; border-radius: 8px; margin-top: 12px; font-size: 13px; color: #475569; }
/* 评价星星 */
.rating-star { font-size: 24px; color: #e2e8f0; cursor: pointer; margin-right: 4px; }
.rating-star.active { color: #f59e0b; }
/* 商品推荐模块 */
.recommend-section {
margin: 24px 16px;
padding: 20px 16px;
background: #fff;
border-radius: 16px;
}
.recommend-title {
font-size: 16px; font-weight: 700; color: var(--primary);
margin-bottom: 16px; display: flex; align-items: center;
}
.recommend-title i { margin-right: 8px; color: var(--accent); }
.recommend-grid {
display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;
}
.recommend-item {
background: #f8fafc; border-radius: 12px; overflow: hidden;
cursor: pointer; transition: transform 0.2s;
}
.recommend-item:active { transform: scale(0.98); }
.recommend-img {
width: 100%; height: 120px; object-fit: cover; background: #f1f5f9;
}
.recommend-info {
padding: 10px;
}
.recommend-name {
font-size: 13px; font-weight: 600; color: var(--primary);
margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.recommend-price {
font-size: 14px; font-weight: 700; color: var(--primary);
}
/* 底部导航 */
.mobile-tabbar {
position: fixed; bottom: 0; left: 0; right: 0; background: #fff;
padding: 8px 0 20px; display: flex; border-top: 1px solid #f1f5f9; z-index: 99;
}
.tab-item {
flex: 1; text-align: center; color: #94a3b8; text-decoration: none;
font-size: 10px; display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.tab-item.active { color: var(--accent); }
.tab-item i { font-size: 22px; margin-bottom: 4px; line-height: 1; }
</style>
</head>
<body>
<div class="filter-bar">
<div class="filter-chip active" onclick="filter('')">全部</div>
<div class="filter-chip" onclick="filter('PENDING_PAY')">待支付</div>
<div class="filter-chip" onclick="filter('MAKING')">制作中</div>
<div class="filter-chip" onclick="filter('READY')">待取餐</div>
<div class="filter-chip" onclick="filter('COMPLETED')">已完成</div>
</div>
<div id="orderList" class="pt-2"></div>
<!-- 商品推荐模块 -->
<div class="recommend-section" id="recommendSection" style="display: none;">
<div class="recommend-title">
<i class="bi bi-fire"></i>
<span>为您推荐</span>
</div>
<div class="recommend-grid" id="recommendGrid"></div>
</div>
<!-- 评价输入模态框 -->
<div class="modal fade" id="reviewModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" style="border-radius: 16px; border: none;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">评价订单</h5>
</div>
<div class="modal-body">
<input type="hidden" id="reviewOrderId">
<input type="hidden" id="reviewRating" value="0">
<div class="mb-3">
<label class="small text-muted mb-2 d-block">选择商品</label>
<select class="form-select border-0 bg-light" id="reviewProductSelect"></select>
</div>
<div class="mb-3 text-center">
<div id="starContainer">
<i class="bi bi-star-fill rating-star" data-val="1"></i>
<i class="bi bi-star-fill rating-star" data-val="2"></i>
<i class="bi bi-star-fill rating-star" data-val="3"></i>
<i class="bi bi-star-fill rating-star" data-val="4"></i>
<i class="bi bi-star-fill rating-star" data-val="5"></i>
</div>
<div class="small text-muted mt-1" id="ratingText">点击星星打分</div>
</div>
<textarea class="form-control bg-light border-0" id="reviewContent" rows="3" placeholder="口味如何?服务周到吗?"></textarea>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-pill" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-dark rounded-pill px-4" onclick="submitReview()">提交评价</button>
</div>
</div>
</div>
</div>
<!-- 查看评价模态框 -->
<div class="modal fade" id="viewReviewModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" style="border-radius: 16px; border: none;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">我的评价</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="viewReviewContent">
<div class="text-center py-3">加载中...</div>
</div>
</div>
</div>
</div>
<!-- 底部导航 -->
<div class="mobile-tabbar">
<a href="/index.html" class="tab-item">
<i class="bi bi-cup-hot"></i>
<span>点餐</span>
</a>
<a href="/cart.html" class="tab-item">
<i class="bi bi-bag"></i>
<span>购物袋</span>
</a>
<a href="/orders.html" class="tab-item active">
<i class="bi bi-receipt-cutoff"></i>
<span>订单</span>
</a>
<a href="/profile.html" class="tab-item">
<i class="bi bi-person"></i>
<span>我的</span>
</a>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script src="/static/js/custom-modal.js"></script>
<script>
let currentStatus = '';
// 初始化
filter('');
loadRecommendations();
async function filter(status) {
currentStatus = status;
document.querySelectorAll('.filter-chip').forEach(el => {
const isActive = (status === '' && el.innerText === '全部') ||
(status !== '' && el.onclick.toString().includes(status));
el.classList.toggle('active', isActive);
});
await loadOrders();
}
async function loadOrders() {
const box = document.getElementById('orderList');
box.innerHTML = '<div class="text-center py-5 text-muted small">加载中...</div>';
try {
const url = currentStatus ? `/order/orders?status=${currentStatus}` : '/order/orders';
const res = await fetch(url, { credentials: 'include' });
const data = await res.json();
if (data.code === 200) {
const orders = data.data || [];
if (!orders.length) {
box.innerHTML = '<div class="text-center py-5 text-muted small">暂无订单</div>';
return;
}
// 批量查询评论状态
const orderIds = orders.map(o => o.id);
const reviewsMap = await loadReviewsForOrders(orderIds);
box.innerHTML = orders.map(o => {
const statusMap = { 'PENDING_PAY': '待支付', 'MAKING': '制作中', 'READY': '待取餐', 'COMPLETED': '已完成', 'CANCELLED': '已取消' };
const statusTxt = statusMap[o.orderStatus] || o.orderStatus;
let btns = `<button class="btn-act" onclick="location.href='/order-detail.html?id=${o.id}'">详情</button>`;
if (o.orderStatus === 'PENDING_PAY') {
btns += `<button class="btn-act primary" onclick="goToPayment(${o.id})">去支付</button>`;
} else if (o.orderStatus === 'COMPLETED') {
// 根据评论状态显示不同按钮
if (reviewsMap.has(o.id)) {
btns += `<button class="btn-act" onclick="showViewReviewModal(${o.id})">查看评价</button>`;
} else {
btns += `<button class="btn-act" onclick="openReviewModal(${o.id})">评价</button>`;
}
}
return `
<div class="order-card">
<div class="order-top">
<span class="order-no">单号 ${o.orderNo.slice(-8)}</span>
<span class="order-status st-${o.orderStatus}">${statusTxt}</span>
</div>
<div class="order-info">
<div class="order-price">¥${o.totalAmount}</div>
<div class="order-date">${new Date(o.createTime).toLocaleString()}</div>
</div>
<div class="order-actions">
${btns}
</div>
</div>
`;
}).join('');
} else if (data.message?.includes('登录')) {
window.location.href = '/login.html';
}
} catch (e) { console.error(e); }
}
// 批量获取评论状态
async function loadReviewsForOrders(orderIds) {
if (!orderIds || orderIds.length === 0) return new Map();
try {
const res = await fetch(`/review/by-orders?orderIds=${orderIds.join(',')}`, { credentials: 'include' });
const data = await res.json();
if (data.code === 200 && data.data) {
const map = new Map();
data.data.forEach(r => map.set(r.orderId, r));
return map;
}
} catch (e) { console.error(e); }
return new Map();
}
// 跳转到支付页面
function goToPayment(orderId) {
window.location.href = `/payment.html?orderId=${orderId}`;
}
// 评价输入逻辑
let reviewModal;
async function openReviewModal(orderId) {
try {
const res = await fetch(`/order/orders/${orderId}`, { credentials: 'include' });
const d = await res.json();
if (d.code === 200) {
const items = d.data.items || [];
const select = document.getElementById('reviewProductSelect');
select.innerHTML = items.map(i =>
`<option value="${i.productId}">${i.productName}</option>`
).join('');
document.getElementById('reviewOrderId').value = orderId;
document.getElementById('reviewRating').value = 0;
document.getElementById('reviewContent').value = '';
resetStars();
reviewModal = new bootstrap.Modal(document.getElementById('reviewModal'));
reviewModal.show();
}
} catch(e) { alert('无法加载订单信息'); }
}
// 查看评价逻辑
async function showViewReviewModal(orderId) {
const modal = new bootstrap.Modal(document.getElementById('viewReviewModal'));
document.getElementById('viewReviewContent').innerHTML = '<div class="text-center py-3">加载中...</div>';
modal.show();
try {
const res = await fetch(`/review/by-order?orderId=${orderId}`, { credentials: 'include' });
const d = await res.json();
if (d.code === 200 && d.data) {
const r = d.data;
const stars = '★'.repeat(r.rating) + '☆'.repeat(5 - r.rating);
let html = `
<div class="mb-3">
<div class="review-display-stars fs-4 mb-2">${stars}</div>
<div class="text-dark">${r.content || '未填写评价内容'}</div>
<div class="text-muted small mt-2">${new Date(r.createTime).toLocaleString()}</div>
</div>
`;
if (r.reply) {
html += `
<div class="review-reply-box">
<div class="fw-bold mb-1">商家回复:</div>
<div>${r.reply}</div>
</div>
`;
}
document.getElementById('viewReviewContent').innerHTML = html;
} else {
document.getElementById('viewReviewContent').innerHTML = '<div class="text-center">未找到评价信息</div>';
}
} catch (e) {
document.getElementById('viewReviewContent').innerHTML = '<div class="text-center">加载失败</div>';
}
}
// 星星交互
document.querySelectorAll('#starContainer .rating-star').forEach(star => {
star.onclick = function() {
const val = this.dataset.val;
document.getElementById('reviewRating').value = val;
document.getElementById('ratingText').innerText = val + ' 分';
updateStars(val);
}
});
function updateStars(val) {
document.querySelectorAll('#starContainer .rating-star').forEach(s => {
if (s.dataset.val <= val) s.classList.add('active');
else s.classList.remove('active');
});
}
function resetStars() {
updateStars(0);
document.getElementById('ratingText').innerText = '点击星星打分';
}
async function submitReview() {
const oid = document.getElementById('reviewOrderId').value;
const pid = document.getElementById('reviewProductSelect').value;
const rating = document.getElementById('reviewRating').value;
const content = document.getElementById('reviewContent').value;
if (rating == 0) return alert('请打分');
try {
const params = new URLSearchParams();
params.append('orderId', oid);
params.append('productId', pid);
params.append('rating', rating);
params.append('content', content);
const res = await fetch('/review', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
credentials: 'include',
body: params
});
const d = await res.json();
if (d.code === 200) {
alert('评价成功');
reviewModal.hide();
loadOrders(); // 刷新列表状态
} else { alert(d.message); }
} catch(e) { alert('提交失败'); }
}
// 加载推荐商品限制3-5个
async function loadRecommendations() {
try {
const limit = Math.floor(Math.random() * 3) + 3; // 3-5个随机
const res = await fetch(`/recommend?limit=${limit}`, { credentials: 'include' });
const data = await res.json();
if (data.code === 200 && data.data && data.data.length > 0) {
renderRecommendations(data.data);
document.getElementById('recommendSection').style.display = 'block';
}
} catch(e) { console.error(e); }
}
function renderRecommendations(products) {
const container = document.getElementById('recommendGrid');
container.innerHTML = products.map(p => `
<div class="recommend-item" onclick="location.href='/product-detail.html?id=${p.id}'">
<img src="${p.image || '/static/images/default.jpg'}" class="recommend-img" alt="${p.name}">
<div class="recommend-info">
<div class="recommend-name">${p.name}</div>
<div class="recommend-price">¥${p.basePrice}</div>
</div>
</div>
`).join('');
}
</script>
</body>
</html>

View File

@@ -0,0 +1,214 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>支付成功</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
:root {
--bg-body: #f8fafc;
--primary-color: #3b82f6;
--text-primary: #0f172a;
}
body {
background: var(--bg-body);
font-family: -apple-system, sans-serif;
color: #334155;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.success-container {
background: #fff;
border-radius: 24px;
padding: 40px 24px;
text-align: center;
max-width: 400px;
width: 100%;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
}
.success-icon {
width: 80px;
height: 80px;
background: #10b981;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 24px;
animation: scaleIn 0.5s ease;
}
.success-icon i {
font-size: 40px;
color: #fff;
}
@keyframes scaleIn {
from {
transform: scale(0);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.success-title {
font-size: 24px;
font-weight: 800;
color: var(--text-primary);
margin-bottom: 8px;
}
.success-desc {
font-size: 14px;
color: #64748b;
margin-bottom: 32px;
}
.order-info {
background: #f8fafc;
border-radius: 12px;
padding: 16px;
margin-bottom: 24px;
text-align: left;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 13px;
margin-bottom: 8px;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-label {
color: #94a3b8;
}
.info-value {
color: var(--text-primary);
font-weight: 600;
}
.btn-group {
display: flex;
flex-direction: column;
gap: 12px;
}
.btn-action {
padding: 14px 24px;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary-action {
background: var(--text-primary);
color: #fff;
}
.btn-primary-action:hover {
background: #1e293b;
transform: translateY(-1px);
}
.btn-outline-action {
background: #fff;
color: var(--text-primary);
border: 2px solid #e2e8f0;
}
.btn-outline-action:hover {
border-color: var(--text-primary);
background: #f8fafc;
}
</style>
</head>
<body>
<div class="success-container">
<div class="success-icon">
<i class="bi bi-check-lg"></i>
</div>
<div class="success-title">支付成功</div>
<div class="success-desc">感谢您的购买,订单已提交成功</div>
<div class="order-info" id="orderInfo">
<div class="info-row">
<span class="info-label">订单编号</span>
<span class="info-value" id="orderNo">--</span>
</div>
<div class="info-row">
<span class="info-label">支付金额</span>
<span class="info-value" id="orderAmount">--</span>
</div>
</div>
<div class="btn-group">
<button class="btn-action btn-primary-action" onclick="viewOrderDetail()">
查看订单详情
</button>
<button class="btn-action btn-outline-action" onclick="goHome()">
返回首页
</button>
</div>
</div>
<script>
// 从URL参数获取订单ID
const urlParams = new URLSearchParams(window.location.search);
const orderId = urlParams.get('orderId');
const orderNo = urlParams.get('orderNo') || '';
const orderAmount = urlParams.get('amount') || '';
// 显示订单信息
if (orderNo) {
document.getElementById('orderNo').textContent = orderNo;
}
if (orderAmount) {
document.getElementById('orderAmount').textContent = '¥' + orderAmount;
}
// 如果没有订单信息,尝试从服务器获取
if (orderId && (!orderNo || !orderAmount)) {
fetch(`/order/orders/${orderId}`, { credentials: 'include' })
.then(res => res.json())
.then(data => {
if (data.code === 200 && data.data) {
const order = data.data.order;
document.getElementById('orderNo').textContent = order.orderNo || '--';
document.getElementById('orderAmount').textContent = '¥' + (order.totalAmount || '--');
}
})
.catch(e => console.error(e));
}
function viewOrderDetail() {
if (orderId) {
window.location.href = `/order-detail.html?id=${orderId}`;
} else {
window.location.href = '/orders.html';
}
}
function goHome() {
window.location.href = '/index.html';
}
</script>
</body>
</html>

View File

@@ -0,0 +1,348 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>支付订单</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
:root {
--bg-body: #f8fafc;
--primary: #0f172a;
--accent: #3b82f6;
}
body {
background: var(--bg-body);
padding-bottom: 120px;
font-family: -apple-system, sans-serif;
color: #334155;
}
/* 顶部导航 */
.nav-header {
position: fixed; top: 0; left: 0; right: 0; height: 50px;
background: #fff; display: flex; align-items: center; justify-content: space-between;
padding: 0 16px; z-index: 1000; box-shadow: 0 1px 0 rgba(0,0,0,0.05);
}
.nav-title { font-size: 16px; font-weight: 600; }
.btn-back { border: none; background: transparent; font-size: 20px; padding: 0; }
/* 通用卡片 */
.section-card {
background: #fff; margin: 16px; padding: 16px; border-radius: 16px;
box-shadow: 0 1px 2px rgba(0,0,0,0.02);
}
.card-header {
font-size: 14px; font-weight: 700; margin-bottom: 12px; display: flex; align-items: center;
}
.card-header i { margin-right: 6px; color: #64748b; font-size: 16px; }
/* 订单金额 */
.amount-display {
text-align: center; padding: 24px 0;
}
.amount-label {
font-size: 13px; color: #94a3b8; margin-bottom: 8px;
}
.amount-value {
font-size: 36px; font-weight: 800; color: var(--primary);
}
/* 商品简要信息 */
.product-summary {
display: flex; align-items: center; margin-bottom: 12px;
}
.product-summary:last-child { margin-bottom: 0; }
.summary-img {
width: 50px; height: 50px; border-radius: 8px; object-fit: cover;
background: #f1f5f9; margin-right: 12px; flex-shrink: 0;
}
.summary-info { flex: 1; }
.summary-name {
font-size: 14px; font-weight: 600; color: var(--primary);
margin-bottom: 2px;
}
.summary-detail {
font-size: 12px; color: #94a3b8;
}
.summary-price {
font-size: 14px; font-weight: 600; color: var(--primary);
}
/* 支付方式 */
.pay-option {
display: flex; align-items: center; padding: 16px; border-radius: 12px;
border: 2px solid #f1f5f9; margin-bottom: 12px; cursor: pointer;
transition: all 0.2s; background: #fff;
}
.pay-option:last-child { margin-bottom: 0; }
.pay-option:hover {
border-color: var(--accent);
background: #f8fafc;
}
.pay-option.selected {
border-color: var(--primary);
background: #f8fafc;
}
.pay-icon {
font-size: 32px; margin-right: 16px; width: 40px; text-align: center;
}
.pay-wechat { color: #07c160; }
.pay-alipay { color: #1677ff; }
.pay-info { flex: 1; }
.pay-name {
font-size: 15px; font-weight: 600; color: var(--primary);
margin-bottom: 2px;
}
.pay-desc {
font-size: 12px; color: #94a3b8;
}
.pay-radio {
width: 20px; height: 20px; accent-color: var(--primary);
}
/* 底部操作栏 */
.bottom-bar {
position: fixed; bottom: 0; left: 0; right: 0;
background: #fff; padding: 12px 20px 30px;
box-shadow: 0 -4px 16px rgba(0,0,0,0.05);
display: flex; justify-content: space-between; align-items: center; z-index: 100;
}
.total-area { display: flex; flex-direction: column; align-items: flex-end; }
.total-label { font-size: 12px; color: #64748b; }
.total-val { font-size: 24px; font-weight: 800; color: var(--primary); }
.btn-pay {
background: var(--primary); color: #fff; border: none;
padding: 14px 40px; border-radius: 12px; font-weight: 600; font-size: 16px;
}
.btn-pay:active { transform: scale(0.98); opacity: 0.9; }
.btn-pay:disabled { background: #cbd5e1; }
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="nav-header">
<button class="btn-back" onclick="history.back()"><i class="bi bi-arrow-left"></i></button>
<span class="nav-title">支付订单</span>
<div style="width: 24px;"></div>
</div>
<div style="height: 50px;"></div>
<!-- 订单金额 -->
<div class="section-card">
<div class="amount-display">
<div class="amount-label">需支付</div>
<div class="amount-value" id="totalAmount">¥0.00</div>
</div>
</div>
<!-- 商品简要信息 -->
<div class="section-card">
<div class="card-header"><i class="bi bi-bag-fill"></i> 订单商品</div>
<div id="productList"></div>
</div>
<!-- 支付方式 -->
<div class="section-card">
<div class="card-header"><i class="bi bi-credit-card-fill"></i> 选择支付方式</div>
<label class="pay-option" id="payWechat">
<i class="bi bi-wechat pay-icon pay-wechat"></i>
<div class="pay-info">
<div class="pay-name">微信支付</div>
<div class="pay-desc">推荐使用微信支付</div>
</div>
<input type="radio" name="payMethod" value="wechat" class="pay-radio" checked>
</label>
<label class="pay-option" id="payAlipay">
<i class="bi bi-alipay pay-icon pay-alipay"></i>
<div class="pay-info">
<div class="pay-name">支付宝</div>
<div class="pay-desc">安全便捷的支付方式</div>
</div>
<input type="radio" name="payMethod" value="alipay" class="pay-radio">
</label>
</div>
<!-- 底部操作栏 -->
<div class="bottom-bar">
<div class="total-area">
<span class="total-label">合计</span>
<span class="total-val" id="bottomTotal">¥0.00</span>
</div>
<button class="btn-pay" onclick="submitPayment()">立即支付</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script src="/static/js/custom-modal.js"></script>
<script>
let orderId = null;
let orderData = null;
// 初始化
(async () => {
// 从URL获取订单ID
const params = new URLSearchParams(location.search);
orderId = params.get('orderId');
if (!orderId) {
alert('订单ID不存在');
history.back();
return;
}
try {
await loadOrderDetail();
} catch(e) {
console.error('初始化失败:', e);
alert('页面加载失败,请刷新重试');
}
})();
// 加载订单详情
async function loadOrderDetail() {
try {
const res = await fetch(`/order/orders/${orderId}`, { credentials: 'include' });
if (!res.ok) {
throw new Error(`HTTP错误: ${res.status}`);
}
const data = await res.json();
console.log('订单详情响应:', data);
if (data.code === 200 && data.data) {
orderData = data.data;
console.log('订单数据:', orderData);
renderOrder();
} else {
const errorMsg = data.message || '加载订单失败';
console.error('加载订单失败:', errorMsg, data);
alert(errorMsg);
history.back();
}
} catch(e) {
console.error('加载订单详情失败:', e);
alert('加载失败: ' + (e.message || '网络错误'));
history.back();
}
}
function renderOrder() {
if (!orderData || !orderData.order) {
alert('订单数据异常');
history.back();
return;
}
const order = orderData.order;
const items = orderData.items || [];
const productImages = orderData.productImages || {};
// 显示金额
const totalAmount = order.totalAmount || 0;
document.getElementById('totalAmount').textContent = '¥' + totalAmount.toFixed(2);
document.getElementById('bottomTotal').textContent = '¥' + totalAmount.toFixed(2);
// 显示商品列表最多显示3个超过显示"等X件商品"
if (items.length === 0) {
document.getElementById('productList').innerHTML = '<div class="text-center text-muted small py-2">暂无商品信息</div>';
} else {
const displayItems = items.slice(0, 3);
const moreCount = items.length - 3;
let html = '';
displayItems.forEach(item => {
if (!item) return;
const productId = item.productId || item.id;
const image = (productImages && productImages[productId]) || '/static/images/default.jpg';
const productName = item.productName || '商品';
const specName = item.specName || '';
const quantity = item.quantity || 1;
const price = item.price || 0;
html += `
<div class="product-summary">
<img src="${image}" class="summary-img" alt="${productName}">
<div class="summary-info">
<div class="summary-name">${productName}</div>
<div class="summary-detail">${specName ? specName + ' ' : ''}x${quantity}</div>
</div>
<div class="summary-price">¥${parseFloat(price).toFixed(2)}</div>
</div>
`;
});
if (moreCount > 0) {
html += `<div class="text-center text-muted small mt-2">等${items.length}件商品</div>`;
}
document.getElementById('productList').innerHTML = html;
}
// 绑定支付方式选择
document.querySelectorAll('input[name="payMethod"]').forEach(radio => {
radio.addEventListener('change', function() {
document.querySelectorAll('.pay-option').forEach(opt => {
opt.classList.remove('selected');
});
if (this.value === 'wechat') {
document.getElementById('payWechat').classList.add('selected');
} else {
document.getElementById('payAlipay').classList.add('selected');
}
});
});
// 默认选中微信支付
document.getElementById('payWechat').classList.add('selected');
}
// 提交支付
async function submitPayment() {
const payMethod = document.querySelector('input[name="payMethod"]:checked').value;
const btn = document.querySelector('.btn-pay');
// 确认支付
const confirmed = await new Promise((resolve) => {
if (typeof confirmAction !== 'undefined') {
confirmAction('确认支付?', '确认支付', '确认', '取消').then(resolve);
} else {
resolve(confirm('确认支付?'));
}
});
if (!confirmed) return;
btn.disabled = true;
btn.textContent = '支付中...';
try {
const res = await fetch(`/order/${orderId}/pay`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ paymentMethod: payMethod }),
credentials: 'include'
});
const d = await res.json();
if (d.code === 200) {
// 跳转到支付成功页面
const order = orderData && orderData.order ? orderData.order : {};
const orderNo = order.orderNo || '';
const amount = order.totalAmount || 0;
window.location.href = `/payment-success.html?orderId=${orderId}&orderNo=${orderNo}&amount=${amount}`;
} else {
alert(d.message || '支付失败');
btn.disabled = false;
btn.textContent = '立即支付';
}
} catch(e) {
console.error(e);
alert('支付失败,请重试');
btn.disabled = false;
btn.textContent = '立即支付';
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,356 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商品详情</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
:root { --primary: #0f172a; --accent: #3b82f6; --bg-gray: #f8fafc; }
body { background: #fff; padding-bottom: 100px; font-family: -apple-system, sans-serif; }
/* 顶部导航 Header */
.nav-header {
position: fixed; top: 0; left: 0; right: 0; height: 50px;
background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px);
display: flex; align-items: center; justify-content: space-between;
padding: 0 16px; z-index: 1000; box-shadow: 0 1px 0 rgba(0,0,0,0.05);
}
.nav-title { font-size: 16px; font-weight: 600; color: var(--primary); }
.btn-back { border: none; background: transparent; font-size: 20px; color: var(--primary); padding: 0; }
/* 顶部大图 */
.hero-img {
width: 100%; height: 320px; object-fit: cover; background: #f1f5f9;
margin-top: 50px; /* 避开Header */
}
/* 内容容器 - 上浮圆角 */
.content-box {
position: relative; top: -24px; border-radius: 24px 24px 0 0;
background: #fff; padding: 24px 20px; margin-bottom: -24px;
}
.prod-title { font-size: 22px; font-weight: 800; color: var(--primary); margin-bottom: 8px; }
.prod-desc { font-size: 13px; color: #64748b; line-height: 1.6; margin-bottom: 24px; }
/* 选项组 */
.opt-group { margin-bottom: 24px; }
.opt-title { font-size: 14px; font-weight: 700; color: var(--primary); margin-bottom: 12px; }
/* 扁平化 Chips */
.chips-wrap { display: flex; flex-wrap: wrap; gap: 10px; }
.chip-input { display: none; } /* Hide default checkbox/radio */
.chip-label {
padding: 8px 16px; border-radius: 8px; background: var(--bg-gray);
font-size: 13px; color: #475569; transition: all 0.2s; border: 1px solid transparent;
cursor: pointer;
}
/* 选中态 */
.chip-input:checked + .chip-label {
background: #eff6ff; color: var(--accent); border-color: var(--accent); font-weight: 600;
}
/* 数量步进器 */
.stepper {
display: inline-flex; align-items: center; background: var(--bg-gray); border-radius: 8px; padding: 4px;
}
.step-btn {
width: 32px; height: 32px; border: none; background: #fff; border-radius: 6px;
box-shadow: 0 1px 2px rgba(0,0,0,0.05); color: var(--primary); font-size: 18px; display: flex; align-items: center; justify-content: center;
}
.step-val { width: 40px; text-align: center; font-size: 15px; font-weight: 600; background: transparent; border: none; }
/* 底部固定栏 */
.bottom-bar {
position: fixed; bottom: 0; left: 0; right: 0;
background: #fff; padding: 12px 20px 30px; /* Adapt to safe area */
box-shadow: 0 -4px 16px rgba(0,0,0,0.04);
display: flex; justify-content: space-between; align-items: center; z-index: 100;
}
.price-area { display: flex; flex-direction: column; }
.price-label { font-size: 11px; color: #94a3b8; }
.price-val { font-size: 24px; font-weight: 800; color: var(--primary); }
.btn-add {
background: var(--primary); color: #fff; border: none; padding: 14px 40px;
border-radius: 12px; font-weight: 600; font-size: 16px;
}
.btn-add:active { transform: scale(0.98); }
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="nav-header">
<button class="btn-back" onclick="history.back()"><i class="bi bi-arrow-left"></i></button>
<span class="nav-title">商品详情</span>
<div style="width: 24px;"></div> <!-- 占位符确保标题居中 -->
</div>
<img id="prodImg" src="" class="hero-img">
<div class="content-box">
<h1 class="prod-title" id="prodName">Loading...</h1>
<p class="prod-desc" id="prodDesc"></p>
<form id="addForm">
<input type="hidden" id="pid">
<!-- 动态渲染选项 -->
<div id="dynamicOptions"></div>
<div class="opt-group">
<div class="opt-title">数量</div>
<div class="stepper">
<button type="button" class="step-btn" onclick="changeQty(-1)">-</button>
<input type="number" class="step-val" id="qty" value="1" readonly>
<button type="button" class="step-btn" onclick="changeQty(1)">+</button>
</div>
</div>
</form>
</div>
<div class="bottom-bar">
<div class="price-area">
<span class="price-label">总计金额</span>
<span class="price-val" id="totalPrice">¥0.00</span>
</div>
<button class="btn-add" onclick="submitCart()">加入购物袋</button>
</div>
<script src="/static/js/ui-enhancements.js"></script>
<script src="/static/js/cookie-utils.js"></script>
<script>
let basePrice = 0;
let productData = null;
let currentProductId = null;
let isLoggedIn = false;
// 检查用户是否登录
async function checkLoginStatus() {
try {
const res = await UIEnhancements.enhancedFetch('/cart', { credentials: 'include' });
isLoggedIn = res.data.code === 200;
} catch(e) {
isLoggedIn = false;
}
return isLoggedIn;
}
// 记录浏览行为
async function recordView(productId) {
const loggedIn = await checkLoginStatus();
if (loggedIn) {
// 已登录:调用后端接口记录(后端会自动记录)
// 这里不需要额外调用因为getProductDetail已经记录了
} else {
// 未登录存储到cookies
let viewHistory = CookieUtils.getJSON('viewHistory') || [];
// 检查是否已存在该商品的浏览记录
const exists = viewHistory.some(v => v.productId === productId);
if (!exists) {
viewHistory.push({
productId: productId,
timestamp: Date.now()
});
// 限制浏览历史数量避免cookies过大最多保存50条
if (viewHistory.length > 50) {
viewHistory = viewHistory.slice(-50);
}
CookieUtils.setJSON('viewHistory', viewHistory, 7); // 7天过期
}
}
}
// 初始化
(async () => {
const id = new URLSearchParams(location.search).get('id');
if(!id) return alert('商品不存在');
currentProductId = parseInt(id);
try {
const res = await UIEnhancements.enhancedFetch(`/products/${id}`, { credentials: 'include' });
const json = res.data;
if(json.code === 200) {
productData = json.data;
render(productData);
// 记录浏览行为
await recordView(currentProductId);
} else {
alert(json.message);
}
} catch(e) { console.error(e); }
})();
function render(data) {
const p = data.product;
basePrice = p.basePrice;
document.getElementById('pid').value = p.id;
document.getElementById('prodImg').src = p.image || '/static/images/default.jpg';
document.getElementById('prodName').innerText = p.name;
document.getElementById('prodDesc').innerText = p.description || '';
let html = '';
// 1. 规格 (Radio)
if (data.specs && data.specs.length) {
html += `<div class="opt-group"><div class="opt-title">规格</div><div class="chips-wrap">`;
data.specs.forEach((s, i) => {
html += `
<input type="radio" name="spec" id="sp_${s.id}" value="${s.id}" class="chip-input"
data-price="${s.priceAdjust}" ${i===0?'checked':''} onchange="calcPrice()">
<label for="sp_${s.id}" class="chip-label">${s.specName}</label>
`;
});
html += `</div></div>`;
}
// 2. 甜度 (Radio)
if (data.customs?.sweetness?.length) {
html += `<div class="opt-group"><div class="opt-title">甜度</div><div class="chips-wrap">`;
data.customs.sweetness.forEach((o, i) => {
html += `
<input type="radio" name="sweetness" id="sw_${i}" value="${o.optionValue}" class="chip-input" ${i===0?'checked':''}>
<label for="sw_${i}" class="chip-label">${o.optionValue}</label>
`;
});
html += `</div></div>`;
}
// 3. 冰度 (Radio)
if (data.customs?.ice?.length) {
html += `<div class="opt-group"><div class="opt-title">温度</div><div class="chips-wrap">`;
data.customs.ice.forEach((o, i) => {
html += `
<input type="radio" name="ice" id="ic_${i}" value="${o.optionValue}" class="chip-input" ${i===0?'checked':''}>
<label for="ic_${i}" class="chip-label">${o.optionValue}</label>
`;
});
html += `</div></div>`;
}
// 4. 配料 (Checkbox)
if (data.toppings && data.toppings.length) {
html += `<div class="opt-group"><div class="opt-title">加料</div><div class="chips-wrap">`;
data.toppings.forEach(t => {
html += `
<input type="checkbox" name="topping" id="tp_${t.id}" value="${t.id}" class="chip-input"
data-price="${t.price}" onchange="calcPrice()">
<label for="tp_${t.id}" class="chip-label">${t.toppingName} (+¥${t.price})</label>
`;
});
html += `</div></div>`;
}
document.getElementById('dynamicOptions').innerHTML = html;
calcPrice();
}
function changeQty(d) {
const input = document.getElementById('qty');
let v = parseInt(input.value) + d;
if(v < 1) v = 1;
input.value = v;
calcPrice();
}
function calcPrice() {
let total = parseFloat(basePrice);
// 规格加价
const spec = document.querySelector('input[name="spec"]:checked');
if (spec) total += parseFloat(spec.dataset.price || 0);
// 配料加价
document.querySelectorAll('input[name="topping"]:checked').forEach(el => {
total += parseFloat(el.dataset.price || 0);
});
const qty = parseInt(document.getElementById('qty').value);
document.getElementById('totalPrice').innerText = '¥' + (total * qty).toFixed(2);
}
async function submitCart() {
const btn = document.querySelector('.btn-add');
btn.innerText = '添加中...';
btn.disabled = true;
try {
const payload = {
productId: parseInt(document.getElementById('pid').value),
specId: document.querySelector('input[name="spec"]:checked')?.value ?
parseInt(document.querySelector('input[name="spec"]:checked').value) : null,
customSweetness: document.querySelector('input[name="sweetness"]:checked')?.value || null,
customIce: document.querySelector('input[name="ice"]:checked')?.value || null,
quantity: parseInt(document.getElementById('qty').value),
toppings: JSON.stringify(
Array.from(document.querySelectorAll('input[name="topping"]:checked')).map(el => parseInt(el.value))
)
};
const loggedIn = await checkLoginStatus();
if (loggedIn) {
// 已登录:调用后端接口
const res = await fetch('/cart/add', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
credentials: 'include'
});
const json = await res.json();
if(json.code === 200) {
window.location.href = '/cart.html';
} else {
alert(json.message);
btn.disabled = false;
btn.innerText = '加入购物袋';
}
} else {
// 未登录存储到cookies
let cartItems = CookieUtils.getJSON('cartItems') || [];
// 检查是否已存在相同配置的商品
const existingIndex = cartItems.findIndex(item =>
item.productId === payload.productId &&
item.specId === payload.specId &&
item.customSweetness === payload.customSweetness &&
item.customIce === payload.customIce &&
item.toppings === payload.toppings
);
if (existingIndex >= 0) {
// 合并数量
cartItems[existingIndex].quantity += payload.quantity;
} else {
// 添加新商品
cartItems.push(payload);
}
// 限制购物车数量避免cookies过大最多保存30个商品
if (cartItems.length > 30) {
cartItems = cartItems.slice(-30);
}
CookieUtils.setJSON('cartItems', cartItems, 7); // 7天过期
// 提示并跳转
if (confirm('商品已添加到购物袋,是否前往购物袋?')) {
window.location.href = '/cart.html';
} else {
btn.disabled = false;
btn.innerText = '加入购物袋';
}
}
} catch(e) {
console.error(e);
alert('添加失败');
btn.disabled = false;
btn.innerText = '加入购物袋';
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,401 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>个人中心</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
:root {
--bg-body: #f8fafc;
--primary: #0f172a;
--accent: #3b82f6;
}
body {
background: var(--bg-body);
padding-bottom: 90px;
font-family: -apple-system, sans-serif;
color: #334155;
}
/* 头部展示 */
.profile-card {
background: #fff; padding: 30px 20px; margin-bottom: 16px;
display: flex; flex-direction: column; align-items: center; text-align: center;
}
.avatar-placeholder {
width: 80px; height: 80px; border-radius: 50%; background: #f1f5f9; color: #cbd5e1;
font-size: 32px; display: flex; align-items: center; justify-content: center; margin-bottom: 16px;
}
.user-name-disp { font-size: 20px; font-weight: 800; color: var(--primary); margin-bottom: 4px; }
.user-phone-disp { font-size: 14px; color: #64748b; }
.btn-edit-profile {
margin-top: 16px; font-size: 12px; padding: 6px 16px; border-radius: 20px;
border: 1px solid #e2e8f0; background: transparent; color: #475569;
}
/* 菜单组 */
.menu-group { background: #fff; padding: 0 20px; margin-bottom: 16px; }
.menu-item {
padding: 16px 0; border-bottom: 1px solid #f1f5f9; display: flex; align-items: center;
color: var(--primary); font-size: 15px; cursor: pointer; text-decoration: none;
}
.menu-item:last-child { border-bottom: none; }
.menu-icon { margin-right: 12px; font-size: 18px; color: #94a3b8; }
.menu-arrow { margin-left: auto; color: #cbd5e1; font-size: 14px; }
/* 模态框扁平化 */
.modal-content { border-radius: 16px; border: none; }
.modal-header { border-bottom: 1px solid #f1f5f9; }
/* 表单输入 */
.flat-input {
width: 100%; padding: 12px 0; border: none; border-bottom: 1px solid #e2e8f0;
outline: none; transition: border-color 0.2s;
}
.flat-input:focus { border-color: var(--primary); }
.flat-input[readonly] { color: #94a3b8; border-bottom-style: dashed; }
.btn-black {
background: var(--primary); color: #fff; border: none; border-radius: 8px;
padding: 10px 20px; font-size: 14px; font-weight: 600; width: 100%;
}
/* 地址项 */
.addr-item {
border: 1px solid #f1f5f9; border-radius: 12px; padding: 16px; margin-bottom: 12px;
background: #fff; position: relative;
}
.addr-main { font-weight: 600; color: var(--primary); font-size: 15px; margin-bottom: 4px; }
.addr-sub { font-size: 13px; color: #64748b; }
.addr-tag { font-size: 10px; padding: 2px 6px; background: var(--primary); color: #fff; border-radius: 4px; margin-left: 8px; }
.addr-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 12px; padding-top: 12px; border-top: 1px solid #f8fafc; }
.act-btn { font-size: 12px; color: #64748b; border: none; background: none; padding: 0; }
/* 底部导航 */
.mobile-tabbar {
position: fixed; bottom: 0; left: 0; right: 0; background: #fff;
padding: 8px 0 20px; display: flex; border-top: 1px solid #f1f5f9; z-index: 99;
}
.tab-item { flex: 1; text-align: center; color: #94a3b8; text-decoration: none; font-size: 10px; }
.tab-item.active { color: var(--accent); }
.tab-item i { display: block; font-size: 24px; margin-bottom: 2px; }
</style>
</head>
<body>
<!-- 头部展示 -->
<div class="profile-card">
<div class="avatar-placeholder"><i class="bi bi-person"></i></div>
<div class="user-name-disp" id="dispName">--</div>
<div class="user-phone-disp" id="dispPhone">--</div>
<button class="btn-edit-profile" onclick="openProfileModal()">编辑资料</button>
</div>
<!-- 菜单 -->
<div class="menu-group">
<div class="menu-item" onclick="openAddressModalList()">
<i class="bi bi-geo-alt menu-icon"></i>
<span>地址管理</span>
<i class="bi bi-chevron-right menu-arrow"></i>
</div>
<a href="/orders.html" class="menu-item">
<i class="bi bi-receipt menu-icon"></i>
<span>我的订单</span>
<i class="bi bi-chevron-right menu-arrow"></i>
</a>
</div>
<div class="px-4 mt-4">
<button class="btn btn-outline-danger w-100 border-0 bg-white py-3" onclick="logout()">退出登录</button>
</div>
<!-- 编辑资料模态框 -->
<div class="modal fade" id="profileModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold">编辑资料</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="profileForm">
<div class="mb-3">
<label class="small text-muted">用户名</label>
<input type="text" class="flat-input" id="username" readonly>
</div>
<div class="mb-3">
<label class="small text-muted">姓名</label>
<input type="text" class="flat-input" id="name" placeholder="设置昵称">
</div>
<div class="mb-3">
<label class="small text-muted">手机号</label>
<input type="tel" class="flat-input" id="phone" placeholder="绑定手机">
</div>
<div class="mb-3">
<label class="small text-muted">邮箱</label>
<input type="email" class="flat-input" id="email" placeholder="绑定邮箱">
</div>
<button type="submit" class="btn-black mt-3">保存修改</button>
</form>
</div>
</div>
</div>
</div>
<!-- 地址列表模态框 (简单起见,用全屏 Modal 或普通 Modal 模拟新页面) -->
<div class="modal fade" id="addressListModal" tabindex="-1">
<div class="modal-dialog modal-fullscreen-sm-down modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold">我的地址</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body bg-light">
<div id="addressListContent"></div>
<button class="btn-black mt-3" onclick="openAddressEditModal()">+ 新增地址</button>
</div>
</div>
</div>
</div>
<!-- 地址编辑模态框 -->
<div class="modal fade" id="addressEditModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold" id="addrModalTitle">新增地址</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addressForm">
<input type="hidden" id="addrId">
<div class="mb-3">
<input type="text" class="flat-input" id="recipient" placeholder="收货人" required>
</div>
<div class="mb-3">
<input type="tel" class="flat-input" id="addressPhone" placeholder="联系电话" required>
</div>
<div class="mb-3">
<textarea class="flat-input" id="address" rows="2" placeholder="详细地址" required></textarea>
</div>
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" id="isDefault">
<label class="form-check-label small text-muted" for="isDefault">设为默认</label>
</div>
</form>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-dark" onclick="saveAddress()">保存</button>
</div>
</div>
</div>
</div>
<!-- 底部导航 -->
<div class="mobile-tabbar">
<a href="/index.html" class="tab-item"><i class="bi bi-cup-hot"></i>点餐</a>
<a href="/cart.html" class="tab-item"><i class="bi bi-bag"></i>购物袋</a>
<a href="/orders.html" class="tab-item"><i class="bi bi-receipt"></i>订单</a>
<a href="/profile.html" class="tab-item active"><i class="bi bi-person-fill"></i>我的</a>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/custom-modal.js"></script>
<script>
// Init
loadProfile();
async function loadProfile() {
try {
const res = await fetch('/profile', { credentials: 'include' });
const data = await res.json();
if (data.code === 200) {
const u = data.data;
// 展示
document.getElementById('dispName').textContent = u.name || u.username;
document.getElementById('dispPhone').textContent = u.phone || u.email || '未绑定手机';
// 填充表单
document.getElementById('username').value = u.username || '';
document.getElementById('name').value = u.name || '';
document.getElementById('phone').value = u.phone || '';
document.getElementById('email').value = u.email || '';
} else if (data.message?.includes('登录')) {
location.href = '/login.html';
}
} catch (e) { console.error(e); }
}
function openProfileModal() {
// 使用自定义模态框
const modalContent = document.getElementById('profileForm').outerHTML;
const modal = new CustomModal({
title: '编辑资料',
content: modalContent,
size: 'medium',
showFooter: false,
onClose: () => {
// 模态框关闭时的处理
}
});
modal.show();
// 绑定表单提交事件
const form = modal.modal.querySelector('#profileForm');
if (form) {
form.onsubmit = async (e) => {
e.preventDefault();
const btn = form.querySelector('button[type="submit"]');
const old = btn.textContent;
btn.textContent = '保存中...'; btn.disabled = true;
try {
const res = await fetch('/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
name: form.querySelector('#name').value,
phone: form.querySelector('#phone').value,
email: form.querySelector('#email').value
})
});
const d = await res.json();
if(d.code===200) {
modal.hide();
loadProfile();
} else alert(d.message);
} catch(e) { alert('失败'); }
btn.textContent = old; btn.disabled = false;
};
}
}
document.getElementById('profileForm').onsubmit = async (e) => {
e.preventDefault();
const btn = e.target.querySelector('button');
const old = btn.textContent;
btn.textContent = '保存中...'; btn.disabled = true;
try {
const res = await fetch('/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
name: document.getElementById('name').value,
phone: document.getElementById('phone').value,
email: document.getElementById('email').value
})
});
const d = await res.json();
if(d.code===200) {
// 如果使用自定义模态框这里已经通过onClose处理
loadProfile(); // 刷新展示
} else alert(d.message);
} catch(e) { alert('失败'); }
btn.textContent = old; btn.disabled = false;
};
// 地址管理相关
async function openAddressModalList() {
const listModal = new bootstrap.Modal(document.getElementById('addressListModal'));
listModal.show();
loadAddressesInternal();
}
async function loadAddressesInternal() {
const box = document.getElementById('addressListContent');
box.innerHTML = '<div class="text-center py-3 text-muted">加载中...</div>';
try {
const res = await fetch('/address', { credentials: 'include' });
const d = await res.json();
if (d.code === 200 && d.data?.length) {
box.innerHTML = d.data.map(a => `
<div class="addr-item">
<div class="addr-main">
${a.contactName||a.recipient} <span class="ms-2 small fw-normal text-muted">${a.contactPhone||a.phone}</span>
${a.isDefault?'<span class="addr-tag">默认</span>':''}
</div>
<div class="addr-sub">${a.address}</div>
<div class="addr-actions">
<button class="act-btn" onclick="openAddressEditModal(${a.id})">编辑</button>
<button class="act-btn text-danger" onclick="deleteAddress(${a.id})">删除</button>
</div>
</div>
`).join('');
} else {
box.innerHTML = '<div class="text-center py-5 text-muted">暂无地址</div>';
}
} catch(e) { box.innerHTML = '加载失败'; }
}
async function openAddressEditModal(id = null) {
// 如果是在列表模态框之上打开Bootstrap会自动处理层级
const modal = new bootstrap.Modal(document.getElementById('addressEditModal'));
document.getElementById('addressForm').reset();
document.getElementById('addrId').value = '';
document.getElementById('addrModalTitle').textContent = '新增地址';
if (id) {
document.getElementById('addrModalTitle').textContent = '编辑地址';
try {
const res = await fetch('/address', { credentials: 'include' });
const d = await res.json();
const addr = d.data.find(a => a.id === id);
if (addr) {
document.getElementById('addrId').value = addr.id;
document.getElementById('recipient').value = addr.contactName || '';
document.getElementById('addressPhone').value = addr.contactPhone || '';
document.getElementById('address').value = addr.address || '';
document.getElementById('isDefault').checked = addr.isDefault === 1;
}
} catch(e) {}
}
modal.show();
}
async function saveAddress() {
const id = document.getElementById('addrId').value;
const payload = {
contactName: document.getElementById('recipient').value.trim(),
contactPhone: document.getElementById('addressPhone').value.trim(),
address: document.getElementById('address').value.trim(),
isDefault: document.getElementById('isDefault').checked ? 1 : 0
};
// 验证必填字段
if (!payload.contactName || !payload.contactPhone || !payload.address) {
alert('请填写完整信息');
return;
}
try {
const res = await fetch(id ? `/address/${id}` : '/address', {
method: id ? 'PUT' : 'POST',
headers: {'Content-Type': 'application/json'},
credentials: 'include',
body: JSON.stringify(payload)
});
const d = await res.json();
if(d.code === 200) {
bootstrap.Modal.getInstance(document.getElementById('addressEditModal')).hide();
loadAddressesInternal(); // 刷新列表
} else alert(d.message);
} catch(e) { alert('保存失败'); }
}
async function deleteAddress(id) {
if(!confirm('确认删除?')) return;
await fetch(`/address/${id}`, { method: 'DELETE', credentials: 'include' });
loadAddressesInternal();
}
function logout() {
location.href = '/login.html';
}
</script>
</body>
</html>

View File

@@ -0,0 +1,184 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>注册</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
:root {
--primary-color: #0f172a;
--bg-body: #ffffff;
--input-bg: #f1f5f9;
--text-muted: #94a3b8;
}
body {
background: var(--bg-body);
min-height: 100vh;
padding: 40px;
font-family: -apple-system, "SF Pro Text", "Helvetica Neue", sans-serif;
color: var(--primary-color);
display: flex;
flex-direction: column;
justify-content: center;
}
.header { margin-bottom: 40px; }
.title { font-size: 28px; font-weight: 800; letter-spacing: -0.5px; margin-bottom: 8px; }
.subtitle { color: #64748b; font-size: 14px; }
/* 现代输入框容器 - 保持与登录页一致的设计语言 */
.input-group-modern {
background: var(--input-bg);
border-radius: 16px;
padding: 4px 16px;
display: flex;
align-items: center;
margin-bottom: 16px; /* 注册页项多,间距稍小一点 */
border: 1px solid transparent;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.input-group-modern:focus-within {
background: #fff;
border-color: var(--primary-color);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
transform: translateY(-1px);
}
.input-icon {
font-size: 20px;
color: var(--text-muted);
margin-right: 12px;
transition: color 0.3s;
}
.input-group-modern:focus-within .input-icon {
color: var(--primary-color);
}
.modern-input {
border: none;
background: transparent;
width: 100%;
padding: 12px 0;
font-size: 15px;
outline: none;
color: var(--primary-color);
font-weight: 500;
}
.modern-input::placeholder { color: #cbd5e1; font-weight: 400; }
.btn-modern {
width: 100%;
background: var(--primary-color);
color: white;
padding: 18px;
border-radius: 16px;
font-weight: 600;
font-size: 16px;
border: none;
margin-top: 24px;
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.2);
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-modern:active { transform: scale(0.98); box-shadow: none; }
.btn-modern:disabled { opacity: 0.7; cursor: not-allowed; }
.link-area { text-align: center; margin-top: 24px; }
.link-area a {
color: #64748b; text-decoration: none; font-size: 14px;
font-weight: 500; display: inline-flex; align-items: center;
}
</style>
</head>
<body>
<div class="header">
<div class="title">创建账号</div>
<div class="subtitle">填写以下信息完成注册</div>
</div>
<form id="regForm">
<!-- 用户名 -->
<div class="input-group-modern">
<i class="bi bi-person-fill input-icon"></i>
<input type="text" class="modern-input" id="username" placeholder="用户名 (必填)" required>
</div>
<!-- 密码 -->
<div class="input-group-modern">
<i class="bi bi-shield-lock-fill input-icon"></i>
<input type="password" class="modern-input" id="password" placeholder="密码 (必填)" required>
</div>
<!-- 昵称 -->
<div class="input-group-modern">
<i class="bi bi-emoji-smile-fill input-icon"></i>
<input type="text" class="modern-input" id="name" placeholder="昵称 (选填)">
</div>
<!-- 手机号 -->
<div class="input-group-modern">
<i class="bi bi-phone-fill input-icon"></i>
<input type="tel" class="modern-input" id="phone" placeholder="手机号 (选填)">
</div>
<!-- 邮箱 -->
<div class="input-group-modern">
<i class="bi bi-envelope-fill input-icon"></i>
<input type="email" class="modern-input" id="email" placeholder="电子邮箱 (选填)">
</div>
<button type="submit" class="btn-modern">立即注册</button>
</form>
<div class="link-area">
<a href="/login.html">已有账号? <span style="color: #3b82f6; margin-left: 4px;">去登录</span></a>
</div>
<script>
document.getElementById('regForm').onsubmit = async (e) => {
e.preventDefault();
const btn = document.querySelector('.btn-modern');
const originalText = btn.textContent;
btn.disabled = true;
btn.textContent = '注册中...';
const data = {
username: document.getElementById('username').value,
password: document.getElementById('password').value,
name: document.getElementById('name').value,
phone: document.getElementById('phone').value,
email: document.getElementById('email').value
};
try {
const res = await fetch('/register', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const json = await res.json();
if(json.code === 200) {
// 可以加一个简单的toast提示这里直接跳转
window.location.href = '/login.html';
} else {
alert(json.message || '注册失败');
btn.disabled = false;
btn.textContent = originalText;
}
} catch(error) {
console.error('Error:', error);
alert('网络请求失败');
btn.disabled = false;
btn.textContent = originalText;
}
};
</script>
</body>
</html>