Initial commit: AIGC项目完整代码
This commit is contained in:
44
demo/src/main/resources/application-dev.properties
Normal file
44
demo/src/main/resources/application-dev.properties
Normal file
@@ -0,0 +1,44 @@
|
||||
# MySQL DataSource (DEV) - 使用环境变量
|
||||
spring.datasource.url=${DB_URL:jdbc:mysql://localhost:3306/aigc?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true}
|
||||
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
|
||||
spring.datasource.username=${DB_USERNAME:root}
|
||||
spring.datasource.password=${DB_PASSWORD:177615}
|
||||
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
spring.jpa.show-sql=true
|
||||
spring.jpa.properties.hibernate.format_sql=true
|
||||
|
||||
# 初始化脚本仅在开发环境开启
|
||||
spring.sql.init.mode=always
|
||||
spring.sql.init.platform=mysql
|
||||
|
||||
# 支付宝配置 (开发环境 - 沙箱测试)
|
||||
alipay.app-id=2021000000000000
|
||||
alipay.private-key=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...
|
||||
alipay.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
|
||||
alipay.gateway-url=https://openapi.alipaydev.com/gateway.do
|
||||
alipay.charset=UTF-8
|
||||
alipay.sign-type=RSA2
|
||||
alipay.notify-url=http://localhost:8080/api/payments/alipay/notify
|
||||
alipay.return-url=http://localhost:8080/api/payments/alipay/return
|
||||
|
||||
# PayPal配置 (开发环境 - 沙箱模式)
|
||||
paypal.client-id=your_paypal_sandbox_client_id
|
||||
paypal.client-secret=your_paypal_sandbox_client_secret
|
||||
paypal.mode=sandbox
|
||||
paypal.return-url=http://localhost:8080/api/payments/paypal/return
|
||||
paypal.cancel-url=http://localhost:8080/api/payments/paypal/cancel
|
||||
|
||||
# JWT配置 - 使用环境变量
|
||||
jwt.secret=${JWT_SECRET:aigc-demo-secret-key-for-jwt-token-generation-very-long-secret-key}
|
||||
jwt.expiration=${JWT_EXPIRATION:604800000}
|
||||
|
||||
# 日志配置
|
||||
logging.level.com.example.demo.security.JwtAuthenticationFilter=DEBUG
|
||||
logging.level.com.example.demo.util.JwtUtils=DEBUG
|
||||
logging.level.org.springframework.security=DEBUG
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
52
demo/src/main/resources/application-prod.properties
Normal file
52
demo/src/main/resources/application-prod.properties
Normal file
@@ -0,0 +1,52 @@
|
||||
# 生产环境配置
|
||||
spring.h2.console.enabled=false
|
||||
|
||||
# MySQL DataSource (PROD) - 使用环境变量
|
||||
spring.datasource.url=${DB_URL}
|
||||
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
|
||||
spring.datasource.username=${DB_USERNAME}
|
||||
spring.datasource.password=${DB_PASSWORD}
|
||||
|
||||
# 强烈建议生产环境禁用自动建表
|
||||
spring.jpa.hibernate.ddl-auto=validate
|
||||
spring.jpa.show-sql=false
|
||||
spring.jpa.properties.hibernate.format_sql=false
|
||||
|
||||
# 禁用 SQL 脚本自动运行
|
||||
spring.sql.init.mode=never
|
||||
|
||||
# Thymeleaf 可启用缓存
|
||||
spring.thymeleaf.cache=true
|
||||
|
||||
# 支付宝配置 (生产环境)
|
||||
alipay.app-id=${ALIPAY_APP_ID}
|
||||
alipay.private-key=${ALIPAY_PRIVATE_KEY}
|
||||
alipay.public-key=${ALIPAY_PUBLIC_KEY}
|
||||
alipay.gateway-url=https://openapi.alipay.com/gateway.do
|
||||
alipay.charset=UTF-8
|
||||
alipay.sign-type=RSA2
|
||||
alipay.notify-url=${ALIPAY_NOTIFY_URL}
|
||||
alipay.return-url=${ALIPAY_RETURN_URL}
|
||||
|
||||
# PayPal配置 (生产环境)
|
||||
paypal.client-id=${PAYPAL_CLIENT_ID}
|
||||
paypal.client-secret=${PAYPAL_CLIENT_SECRET}
|
||||
paypal.mode=live
|
||||
paypal.return-url=${PAYPAL_RETURN_URL}
|
||||
paypal.cancel-url=${PAYPAL_CANCEL_URL}
|
||||
|
||||
# JWT配置 - 使用环境变量
|
||||
jwt.secret=${JWT_SECRET}
|
||||
jwt.expiration=${JWT_EXPIRATION:604800000}
|
||||
|
||||
# 生产环境日志配置
|
||||
logging.level.root=INFO
|
||||
logging.level.com.example.demo=INFO
|
||||
logging.level.org.springframework.security=WARN
|
||||
logging.file.name=${LOG_FILE_PATH:./logs/application.log}
|
||||
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
4
demo/src/main/resources/application.properties
Normal file
4
demo/src/main/resources/application.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
spring.application.name=demo
|
||||
spring.messages.basename=messages
|
||||
spring.thymeleaf.cache=false
|
||||
spring.profiles.active=dev
|
||||
19
demo/src/main/resources/data.sql
Normal file
19
demo/src/main/resources/data.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- 示例用户数据
|
||||
-- 用户名: demo, 密码: demo
|
||||
INSERT IGNORE INTO users (username, email, password_hash, role, points) VALUES ('demo', 'demo@example.com', 'demo', 'ROLE_USER', 100);
|
||||
|
||||
-- 用户名: admin, 密码: admin123
|
||||
INSERT IGNORE INTO users (username, email, password_hash, role, points) VALUES ('admin', 'admin@example.com', 'admin123', 'ROLE_ADMIN', 200);
|
||||
|
||||
-- 测试用户1: 用户名: testuser, 密码: test123
|
||||
INSERT IGNORE INTO users (username, email, password_hash, role, points) VALUES ('testuser', 'testuser@example.com', 'test123', 'ROLE_USER', 75);
|
||||
|
||||
-- 测试用户2: 用户名: mingzi_FBx7foZYDS7inLQb, 密码: 123456 (对应个人主页的用户名)
|
||||
INSERT IGNORE INTO users (username, email, password_hash, role, points) VALUES ('mingzi_FBx7foZYDS7inLQb', 'mingzi@example.com', '123456', 'ROLE_USER', 25);
|
||||
|
||||
-- 手机号测试用户: 用户名: 15538239326, 密码: 0627
|
||||
INSERT IGNORE INTO users (username, email, password_hash, role, points) VALUES ('15538239326', '15538239326@example.com', '0627', 'ROLE_USER', 50);
|
||||
|
||||
-- 更新现有用户的积分(如果还没有设置)
|
||||
UPDATE users SET points = 50 WHERE points IS NULL OR points = 0;
|
||||
|
||||
28
demo/src/main/resources/messages.properties
Normal file
28
demo/src/main/resources/messages.properties
Normal file
@@ -0,0 +1,28 @@
|
||||
title.login=登录
|
||||
title.register=注册
|
||||
title.home=首页
|
||||
|
||||
label.username=用户名
|
||||
label.email=邮箱
|
||||
label.password=密码
|
||||
label.password.confirm=确认密码
|
||||
label.remember=记住我
|
||||
|
||||
btn.login=登录
|
||||
btn.register=注册
|
||||
btn.logout=退出
|
||||
|
||||
hint.noaccount=还没有账号?去注册
|
||||
hint.haveaccount=已有账号?去登录
|
||||
hint.redirect.home=登录成功将跳转到 /
|
||||
hint.h2=开发调试可访问 /h2-console
|
||||
|
||||
msg.register.success=注册成功,请登录
|
||||
msg.login.error=用户名或密码错误
|
||||
msg.logout.success=您已退出登录
|
||||
|
||||
register.password.mismatch=两次输入的密码不一致
|
||||
javax.validation.constraints.NotBlank.message=不能为空
|
||||
javax.validation.constraints.Size.message=长度不符合要求
|
||||
javax.validation.constraints.Email.message=邮箱格式不正确
|
||||
|
||||
29
demo/src/main/resources/messages_en.properties
Normal file
29
demo/src/main/resources/messages_en.properties
Normal file
@@ -0,0 +1,29 @@
|
||||
title.login=Login
|
||||
title.register=Sign up
|
||||
title.home=Home
|
||||
|
||||
label.username=Username
|
||||
label.email=Email
|
||||
label.password=Password
|
||||
label.password.confirm=Confirm password
|
||||
label.remember=Remember me
|
||||
|
||||
btn.login=Login
|
||||
btn.register=Sign up
|
||||
btn.logout=Logout
|
||||
|
||||
hint.noaccount=No account? Sign up
|
||||
hint.haveaccount=Have an account? Login
|
||||
hint.redirect.home=After login, you will be redirected to /
|
||||
hint.h2=Dev console: /h2-console
|
||||
|
||||
msg.register.success=Registered successfully, please login
|
||||
msg.login.error=Incorrect username or password
|
||||
msg.logout.success=You have been logged out
|
||||
|
||||
register.password.mismatch=Passwords do not match
|
||||
javax.validation.constraints.NotBlank.message=Must not be blank
|
||||
javax.validation.constraints.Size.message=Length is out of range
|
||||
javax.validation.constraints.Email.message=Invalid email format
|
||||
|
||||
|
||||
19
demo/src/main/resources/migration_add_created_at.sql
Normal file
19
demo/src/main/resources/migration_add_created_at.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- 数据库迁移脚本:为users表添加created_at字段
|
||||
-- 如果users表不存在created_at字段,则添加它
|
||||
|
||||
-- 检查字段是否存在,如果不存在则添加
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'users'
|
||||
AND COLUMN_NAME = 'created_at') = 0,
|
||||
'ALTER TABLE users ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP',
|
||||
'SELECT "created_at column already exists" as message'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 为现有用户设置创建时间(如果为NULL)
|
||||
UPDATE users SET created_at = CURRENT_TIMESTAMP WHERE created_at IS NULL;
|
||||
64
demo/src/main/resources/schema.sql
Normal file
64
demo/src/main/resources/schema.sql
Normal file
@@ -0,0 +1,64 @@
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
email VARCHAR(100) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(100) NOT NULL,
|
||||
role VARCHAR(30) NOT NULL DEFAULT 'ROLE_USER',
|
||||
points INT NOT NULL DEFAULT 50,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payments (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
order_id VARCHAR(50) NOT NULL UNIQUE,
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
currency VARCHAR(3) NOT NULL DEFAULT 'CNY',
|
||||
payment_method VARCHAR(20) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||
description VARCHAR(500),
|
||||
external_transaction_id VARCHAR(100),
|
||||
callback_url VARCHAR(1000),
|
||||
return_url VARCHAR(1000),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
paid_at TIMESTAMP NULL,
|
||||
user_id BIGINT,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
order_number VARCHAR(50) NOT NULL UNIQUE,
|
||||
total_amount DECIMAL(10,2) NOT NULL,
|
||||
currency VARCHAR(3) NOT NULL DEFAULT 'CNY',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||
order_type VARCHAR(20) NOT NULL DEFAULT 'PRODUCT',
|
||||
description VARCHAR(500),
|
||||
notes TEXT,
|
||||
shipping_address TEXT,
|
||||
billing_address TEXT,
|
||||
contact_phone VARCHAR(20),
|
||||
contact_email VARCHAR(100),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
paid_at TIMESTAMP NULL,
|
||||
shipped_at TIMESTAMP NULL,
|
||||
delivered_at TIMESTAMP NULL,
|
||||
cancelled_at TIMESTAMP NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS order_items (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
product_name VARCHAR(100) NOT NULL,
|
||||
product_description VARCHAR(500),
|
||||
product_sku VARCHAR(200),
|
||||
unit_price DECIMAL(10,2) NOT NULL,
|
||||
quantity INT NOT NULL,
|
||||
subtotal DECIMAL(10,2) NOT NULL,
|
||||
product_image VARCHAR(100),
|
||||
order_id BIGINT NOT NULL,
|
||||
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
194
demo/src/main/resources/templates/home.html
Normal file
194
demo/src/main/resources/templates/home.html
Normal file
@@ -0,0 +1,194 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::title}, ~{::content})}">
|
||||
<head>
|
||||
<title>首页</title>
|
||||
</head>
|
||||
<body>
|
||||
<div th:fragment="content">
|
||||
<!-- Welcome Section -->
|
||||
<div class="row mb-5">
|
||||
<div class="col-12">
|
||||
<div class="card bg-primary text-white">
|
||||
<div class="card-body text-center py-5">
|
||||
<h1 class="display-4 mb-3">
|
||||
<i class="fas fa-rocket me-3"></i>欢迎使用 AIGC Demo
|
||||
</h1>
|
||||
<p class="lead mb-4">现代化的Spring Boot应用,集成用户管理、支付系统等功能</p>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="row text-center">
|
||||
<div class="col-md-4 mb-3">
|
||||
<i class="fas fa-users fa-2x mb-2"></i>
|
||||
<h5>用户管理</h5>
|
||||
<small>完整的用户注册、登录、权限管理</small>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<i class="fas fa-credit-card fa-2x mb-2"></i>
|
||||
<h5>支付系统</h5>
|
||||
<small>支持支付宝、PayPal多种支付方式</small>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<i class="fas fa-shield-alt fa-2x mb-2"></i>
|
||||
<h5>安全可靠</h5>
|
||||
<small>Spring Security安全框架保护</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="row mb-5">
|
||||
<div class="col-12">
|
||||
<h3 class="mb-4">
|
||||
<i class="fas fa-bolt me-2"></i>快速操作
|
||||
</h3>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card h-100 text-center">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-credit-card fa-3x text-primary mb-3"></i>
|
||||
<h5 class="card-title">支付接入</h5>
|
||||
<p class="card-text">创建新的支付订单,支持支付宝和PayPal</p>
|
||||
<a th:href="@{/payment/create}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-2"></i>创建支付
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3" sec:authorize="hasRole('ADMIN')">
|
||||
<div class="card h-100 text-center">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-users fa-3x text-success mb-3"></i>
|
||||
<h5 class="card-title">用户管理</h5>
|
||||
<p class="card-text">管理系统用户,查看用户列表和权限</p>
|
||||
<a th:href="@{/users}" class="btn btn-success">
|
||||
<i class="fas fa-cog me-2"></i>管理用户
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3" sec:authorize="isAuthenticated()">
|
||||
<div class="card h-100 text-center">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-history fa-3x text-info mb-3"></i>
|
||||
<h5 class="card-title">支付记录</h5>
|
||||
<p class="card-text">查看您的支付历史和交易详情</p>
|
||||
<a th:href="@{/payment/history}" class="btn btn-info">
|
||||
<i class="fas fa-list me-2"></i>查看记录
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Stats -->
|
||||
<div class="row mb-5" sec:authorize="hasRole('ADMIN')">
|
||||
<div class="col-12">
|
||||
<h3 class="mb-4">
|
||||
<i class="fas fa-chart-bar me-2"></i>系统统计
|
||||
</h3>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stats-card">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-1">总用户数</h6>
|
||||
<h3 class="mb-0" th:text="${userCount ?: 0}">0</h3>
|
||||
</div>
|
||||
<i class="fas fa-users stats-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stats-card" style="background: linear-gradient(135deg, #198754, #146c43);">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-1">支付订单</h6>
|
||||
<h3 class="mb-0" th:text="${paymentCount ?: 0}">0</h3>
|
||||
</div>
|
||||
<i class="fas fa-credit-card stats-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stats-card" style="background: linear-gradient(135deg, #ffc107, #e0a800);">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-1">成功支付</h6>
|
||||
<h3 class="mb-0" th:text="${successPaymentCount ?: 0}">0</h3>
|
||||
</div>
|
||||
<i class="fas fa-check-circle stats-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stats-card" style="background: linear-gradient(135deg, #0dcaf0, #0aa2c0);">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-1">在线用户</h6>
|
||||
<h3 class="mb-0" th:text="${onlineUserCount ?: 0}">0</h3>
|
||||
</div>
|
||||
<i class="fas fa-user-check stats-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activities -->
|
||||
<div class="row" sec:authorize="isAuthenticated()">
|
||||
<div class="col-12">
|
||||
<h3 class="mb-4">
|
||||
<i class="fas fa-clock me-2"></i>最近活动
|
||||
</h3>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-muted mb-3">最近支付</h6>
|
||||
<div th:if="${recentPayments != null and !recentPayments.empty}">
|
||||
<div th:each="payment : ${recentPayments}" class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<small class="text-muted" th:text="${payment.orderId}"></small>
|
||||
<div>
|
||||
<span th:text="${payment.currency}"></span>
|
||||
<span th:text="${payment.amount}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<span th:if="${payment.status.name() == 'SUCCESS'}" class="badge bg-success" th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'PENDING'}" class="badge bg-warning" th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'FAILED'}" class="badge bg-danger" th:text="${payment.status.displayName}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div th:if="${recentPayments == null or recentPayments.empty}" class="text-muted">
|
||||
<i class="fas fa-inbox me-2"></i>暂无支付记录
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-muted mb-3">系统信息</h6>
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">当前用户:</small>
|
||||
<span sec:authentication="name" class="fw-bold"></span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">用户角色:</small>
|
||||
<span sec:authentication="authorities" class="fw-bold"></span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">登录时间:</small>
|
||||
<span class="fw-bold" th:text="${#temporals.format(#temporals.createNow(), 'yyyy-MM-dd HH:mm:ss')}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
354
demo/src/main/resources/templates/layout.html
Normal file
354
demo/src/main/resources/templates/layout.html
Normal file
@@ -0,0 +1,354 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title th:text="${pageTitle} + ' - ' + ${siteName}">AIGC Demo</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<!-- Custom CSS -->
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #0d6efd;
|
||||
--secondary-color: #6c757d;
|
||||
--success-color: #198754;
|
||||
--danger-color: #dc3545;
|
||||
--warning-color: #ffc107;
|
||||
--info-color: #0dcaf0;
|
||||
--light-color: #f8f9fa;
|
||||
--dark-color: #212529;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: 600;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link {
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link:hover {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
min-height: calc(100vh - 200px);
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
transition: box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: var(--dark-color);
|
||||
color: white;
|
||||
padding: 2rem 0;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
color: var(--secondary-color);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 0.25rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover,
|
||||
.sidebar .nav-link.active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background: linear-gradient(135deg, var(--primary-color), #0056b3);
|
||||
color: white;
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stats-card .stats-icon {
|
||||
font-size: 2.5rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.table {
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
background-color: var(--light-color);
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Page specific styles -->
|
||||
<th:block th:fragment="page-styles">
|
||||
<!-- Additional page-specific styles -->
|
||||
</th:block>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" th:href="@{/}">
|
||||
<i class="fas fa-rocket me-2"></i>AIGC Demo
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" th:href="@{/}" th:classappend="${#httpServletRequest.requestURI == '/'} ? 'active' : ''">
|
||||
<i class="fas fa-home me-1"></i>首页
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" sec:authorize="hasRole('ADMIN')">
|
||||
<a class="nav-link" th:href="@{/settings}" th:classappend="${#strings.startsWith(#httpServletRequest.requestURI, '/settings')} ? 'active' : ''">
|
||||
<i class="fas fa-gear me-1"></i>系统设置
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" sec:authorize="hasRole('ADMIN')">
|
||||
<a class="nav-link" th:href="@{/users}" th:classappend="${#strings.startsWith(#httpServletRequest.requestURI, '/users')} ? 'active' : ''">
|
||||
<i class="fas fa-users me-1"></i>用户管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" sec:authorize="isAuthenticated()">
|
||||
<a class="nav-link" th:href="@{/payment/create}" th:classappend="${#strings.startsWith(#httpServletRequest.requestURI, '/payment')} ? 'active' : ''">
|
||||
<i class="fas fa-credit-card me-1"></i>支付管理
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item dropdown" sec:authorize="isAuthenticated()">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-user me-1"></i>
|
||||
<span sec:authentication="name">用户</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" th:href="@{/payment/history}">
|
||||
<i class="fas fa-history me-2"></i>支付记录
|
||||
</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form th:action="@{/logout}" method="post" class="d-inline">
|
||||
<button type="submit" class="dropdown-item">
|
||||
<i class="fas fa-sign-out-alt me-2"></i>退出登录
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item" sec:authorize="!isAuthenticated()">
|
||||
<a class="nav-link" th:href="@{/login}">
|
||||
<i class="fas fa-sign-in-alt me-1"></i>登录
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<div class="container">
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" th:if="${breadcrumbs}">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item" th:each="crumb, iterStat : ${breadcrumbs}"
|
||||
th:classappend="${iterStat.last} ? 'active' : ''">
|
||||
<a th:if="${!iterStat.last}" th:href="${crumb.url}" th:text="${crumb.name}"></a>
|
||||
<span th:if="${iterStat.last}" th:text="${crumb.name}"></span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="row mb-4" th:if="${pageTitle}">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h1 class="h3 mb-0" th:text="${pageTitle}">页面标题</h1>
|
||||
<div th:fragment="page-actions">
|
||||
<!-- Page specific action buttons -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alerts -->
|
||||
<div th:if="${success}" class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
<span th:text="${success}"></span>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
|
||||
<div th:if="${error}" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<span th:text="${error}"></span>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
|
||||
<div th:if="${info}" class="alert alert-info alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<span th:text="${info}"></span>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div th:fragment="content">
|
||||
<!-- Page specific content will be inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5><i class="fas fa-rocket me-2"></i>AIGC Demo</h5>
|
||||
<p class="mb-0">现代化的Spring Boot应用演示</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end">
|
||||
<p class="mb-0">
|
||||
<i class="fas fa-code me-1"></i>
|
||||
基于 Spring Boot 3.5.6 + JDK 21
|
||||
</p>
|
||||
<small class="text-muted">© 2024 AIGC Demo. All rights reserved.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Custom JavaScript -->
|
||||
<script>
|
||||
// Global JavaScript functions
|
||||
function showLoading(element) {
|
||||
const loading = element.querySelector('.loading');
|
||||
if (loading) {
|
||||
loading.classList.add('show');
|
||||
}
|
||||
}
|
||||
|
||||
function hideLoading(element) {
|
||||
const loading = element.querySelector('.loading');
|
||||
if (loading) {
|
||||
loading.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
function confirmAction(message, callback) {
|
||||
if (confirm(message)) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-hide alerts after 5 seconds
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const alerts = document.querySelectorAll('.alert');
|
||||
alerts.forEach(function(alert) {
|
||||
setTimeout(function() {
|
||||
const bsAlert = new bootstrap.Alert(alert);
|
||||
bsAlert.close();
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
|
||||
// Form validation
|
||||
function validateForm(form) {
|
||||
const requiredFields = form.querySelectorAll('[required]');
|
||||
let isValid = true;
|
||||
|
||||
requiredFields.forEach(function(field) {
|
||||
if (!field.value.trim()) {
|
||||
field.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
field.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Page specific scripts -->
|
||||
<th:block th:fragment="page-scripts">
|
||||
<!-- Additional page-specific scripts -->
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
292
demo/src/main/resources/templates/login.html
Normal file
292
demo/src/main/resources/templates/login.html
Normal file
@@ -0,0 +1,292 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户登录 - AIGC Demo</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
padding: 3rem;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.login-header h2 {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-floating {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 10px;
|
||||
padding: 1rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 2rem;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: white;
|
||||
width: 100%;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-check-input:checked {
|
||||
background-color: #667eea;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: linear-gradient(135deg, #d4edda, #c3e6cb);
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: linear-gradient(135deg, #f8d7da, #f5c6cb);
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.login-footer a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.login-footer a:hover {
|
||||
color: #764ba2;
|
||||
}
|
||||
|
||||
.language-switcher {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.language-switcher a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
margin: 0 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.language-switcher a:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h2><i class="fas fa-rocket me-2"></i>AIGC Demo</h2>
|
||||
<p>欢迎回来,请登录您的账户</p>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div id="msg" class="alert alert-success" style="display: none;"></div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div th:if="${param.error}" class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<span th:text="#{msg.login.error}">用户名或密码错误</span>
|
||||
</div>
|
||||
|
||||
<!-- Logout Message -->
|
||||
<div th:if="${param.logout}" class="alert alert-success">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
<span th:text="#{msg.logout.success}">您已退出登录</span>
|
||||
</div>
|
||||
|
||||
<form th:action="@{/login}" method="post" id="loginForm">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
placeholder="用户名" required autocomplete="username">
|
||||
<label for="username">
|
||||
<i class="fas fa-user me-2"></i>用户名
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating">
|
||||
<input type="password" class="form-control" id="password" name="password"
|
||||
placeholder="密码" required autocomplete="current-password">
|
||||
<label for="password">
|
||||
<i class="fas fa-lock me-2"></i>密码
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" class="form-check-input" id="remember-me" name="remember-me">
|
||||
<label class="form-check-label" for="remember-me">
|
||||
<i class="fas fa-clock me-2"></i>记住我
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-login" id="loginBtn">
|
||||
<span class="loading">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>登录中...
|
||||
</span>
|
||||
<span class="login-text">
|
||||
<i class="fas fa-sign-in-alt me-2"></i>登录
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="login-footer">
|
||||
<p class="mb-2">
|
||||
<span th:text="#{hint.noaccount}">还没有账号?</span>
|
||||
<a th:href="@{/register}">立即注册</a>
|
||||
</p>
|
||||
|
||||
<div class="language-switcher">
|
||||
<a th:href="@{|?lang=zh_CN|}">中文</a> |
|
||||
<a th:href="@{|?lang=en|}">English</a>
|
||||
</div>
|
||||
|
||||
<div class="demo-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>演示账户:</strong><br>
|
||||
用户名: demo, 密码: demo<br>
|
||||
用户名: admin, 密码: admin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Show registration success message
|
||||
function fromQuery(name) {
|
||||
const url = new URL(window.location.href);
|
||||
return url.searchParams.get(name);
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
const registered = fromQuery('registered');
|
||||
if (registered) {
|
||||
const msgDiv = document.getElementById('msg');
|
||||
msgDiv.textContent = '注册成功,请登录';
|
||||
msgDiv.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
// Form submission with loading state
|
||||
document.getElementById('loginForm').addEventListener('submit', function(e) {
|
||||
const loginBtn = document.getElementById('loginBtn');
|
||||
const loading = loginBtn.querySelector('.loading');
|
||||
const loginText = loginBtn.querySelector('.login-text');
|
||||
|
||||
// Show loading state
|
||||
loading.classList.add('show');
|
||||
loginText.style.display = 'none';
|
||||
loginBtn.disabled = true;
|
||||
});
|
||||
|
||||
// Auto-hide alerts after 5 seconds
|
||||
setTimeout(function() {
|
||||
const alerts = document.querySelectorAll('.alert');
|
||||
alerts.forEach(function(alert) {
|
||||
alert.style.transition = 'opacity 0.5s ease';
|
||||
alert.style.opacity = '0';
|
||||
setTimeout(function() {
|
||||
alert.style.display = 'none';
|
||||
}, 500);
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
// Form validation
|
||||
document.getElementById('loginForm').addEventListener('submit', function(e) {
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value.trim();
|
||||
|
||||
if (!username || !password) {
|
||||
e.preventDefault();
|
||||
alert('请填写用户名和密码');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
563
demo/src/main/resources/templates/orders/admin.html
Normal file
563
demo/src/main/resources/templates/orders/admin.html
Normal file
@@ -0,0 +1,563 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::title}, ~{::content})}">
|
||||
<head>
|
||||
<title>订单管理 - 管理员</title>
|
||||
</head>
|
||||
<body>
|
||||
<div th:fragment="content">
|
||||
<!-- Page Actions -->
|
||||
<div th:fragment="page-actions">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-filter me-2"></i>筛选
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" th:href="@{/orders/admin}">全部订单</a></li>
|
||||
<li><a class="dropdown-item" th:href="@{/orders/admin(status=PENDING)}">待支付</a></li>
|
||||
<li><a class="dropdown-item" th:href="@{/orders/admin(status=PAID)}">已支付</a></li>
|
||||
<li><a class="dropdown-item" th:href="@{/orders/admin(status=SHIPPED)}">已发货</a></li>
|
||||
<li><a class="dropdown-item" th:href="@{/orders/admin(status=COMPLETED)}">已完成</a></li>
|
||||
<li><a class="dropdown-item" th:href="@{/orders/admin(status=CANCELLED)}">已取消</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-success" onclick="exportOrders()">
|
||||
<i class="fas fa-download me-2"></i>导出
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Orders Statistics -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-primary text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6 class="mb-1">总订单数</h6>
|
||||
<h3 class="mb-0" th:text="${totalElements}">0</h3>
|
||||
</div>
|
||||
<i class="fas fa-shopping-cart fa-2x opacity-75"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-warning text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6 class="mb-1">待支付</h6>
|
||||
<h3 class="mb-0" th:text="${pendingCount ?: 0}">0</h3>
|
||||
</div>
|
||||
<i class="fas fa-clock fa-2x opacity-75"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-success text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6 class="mb-1">已完成</h6>
|
||||
<h3 class="mb-0" th:text="${completedCount ?: 0}">0</h3>
|
||||
</div>
|
||||
<i class="fas fa-check-circle fa-2x opacity-75"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-info text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6 class="mb-1">今日订单</h6>
|
||||
<h3 class="mb-0" th:text="${todayCount ?: 0}">0</h3>
|
||||
</div>
|
||||
<i class="fas fa-calendar-day fa-2x opacity-75"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Orders Table -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-list me-2"></i>订单管理
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="row g-2">
|
||||
<div class="col">
|
||||
<select class="form-select form-select-sm" id="statusFilter">
|
||||
<option value="">全部状态</option>
|
||||
<option th:each="status : ${orderStatuses}"
|
||||
th:value="${status.name()}"
|
||||
th:text="${status.displayName}"
|
||||
th:selected="${status == param.status}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="input-group input-group-sm" style="max-width: 200px;">
|
||||
<input type="text" class="form-control" placeholder="搜索订单号..." id="searchInput">
|
||||
<button class="btn btn-outline-secondary" type="button">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0" id="ordersTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="120">订单号</th>
|
||||
<th width="100">用户</th>
|
||||
<th width="100">金额</th>
|
||||
<th width="100">状态</th>
|
||||
<th width="100">类型</th>
|
||||
<th width="120">创建时间</th>
|
||||
<th width="200">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="order : ${orders.content}" class="order-row">
|
||||
<td>
|
||||
<a th:href="@{|/orders/${order.id}|}" class="text-decoration-none">
|
||||
<span th:text="${order.orderNumber}">ORD123456789</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar-circle me-2">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold" th:text="${order.user.username}">用户名</div>
|
||||
<small class="text-muted" th:text="${order.user.email}">邮箱</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span th:text="${order.currency}">CNY</span>
|
||||
<span th:text="${order.totalAmount}">0.00</span>
|
||||
</td>
|
||||
<td>
|
||||
<span th:if="${order.status.name() == 'PENDING'}" class="badge bg-warning" th:text="${order.status.displayName}">待支付</span>
|
||||
<span th:if="${order.status.name() == 'CONFIRMED'}" class="badge bg-info" th:text="${order.status.displayName}">已确认</span>
|
||||
<span th:if="${order.status.name() == 'PAID'}" class="badge bg-primary" th:text="${order.status.displayName}">已支付</span>
|
||||
<span th:if="${order.status.name() == 'PROCESSING'}" class="badge bg-secondary" th:text="${order.status.displayName}">处理中</span>
|
||||
<span th:if="${order.status.name() == 'SHIPPED'}" class="badge bg-success" th:text="${order.status.displayName}">已发货</span>
|
||||
<span th:if="${order.status.name() == 'DELIVERED'}" class="badge bg-success" th:text="${order.status.displayName}">已送达</span>
|
||||
<span th:if="${order.status.name() == 'COMPLETED'}" class="badge bg-success" th:text="${order.status.displayName}">已完成</span>
|
||||
<span th:if="${order.status.name() == 'CANCELLED'}" class="badge bg-danger" th:text="${order.status.displayName}">已取消</span>
|
||||
<span th:if="${order.status.name() == 'REFUNDED'}" class="badge bg-secondary" th:text="${order.status.displayName}">已退款</span>
|
||||
</td>
|
||||
<td>
|
||||
<span th:text="${order.orderType.displayName}">商品订单</span>
|
||||
</td>
|
||||
<td>
|
||||
<small th:text="${#temporals.format(order.createdAt, 'yyyy-MM-dd HH:mm')}">2024-01-01 10:00</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a th:href="@{|/orders/${order.id}|}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-cog"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li th:if="${order.canShip()}" class="dropdown-item" style="cursor: pointer;"
|
||||
th:onclick="'shipOrder(' + ${order.id} + ')'">
|
||||
<i class="fas fa-truck me-2"></i>发货
|
||||
</li>
|
||||
<li th:if="${order.canComplete()}" class="dropdown-item" style="cursor: pointer;"
|
||||
th:onclick="'completeOrder(' + ${order.id} + ')'">
|
||||
<i class="fas fa-check me-2"></i>完成订单
|
||||
</li>
|
||||
<li th:if="${order.canCancel()}" class="dropdown-item" style="cursor: pointer;"
|
||||
th:onclick="'cancelOrder(' + ${order.id} + ')'">
|
||||
<i class="fas fa-times me-2"></i>取消订单
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="dropdown-item" style="cursor: pointer;"
|
||||
th:onclick="'updateStatus(' + ${order.id} + ')'">
|
||||
<i class="fas fa-edit me-2"></i>更新状态
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<small class="text-muted">
|
||||
共 <span th:text="${totalElements}">0</span> 个订单,
|
||||
第 <span th:text="${currentPage + 1}">1</span> 页,
|
||||
共 <span th:text="${totalPages}">1</span> 页
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<nav aria-label="订单分页">
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item" th:classappend="${currentPage == 0} ? 'disabled' : ''">
|
||||
<a class="page-link" th:href="@{/orders/admin(page=${currentPage - 1}, size=${orders.size}, sortBy=${sortBy}, sortDir=${sortDir}, status=${param.status})}">上一页</a>
|
||||
</li>
|
||||
<li class="page-item" th:each="i : ${#numbers.sequence(0, totalPages - 1)}"
|
||||
th:classappend="${i == currentPage} ? 'active' : ''">
|
||||
<a class="page-link" th:href="@{/orders/admin(page=${i}, size=${orders.size}, sortBy=${sortBy}, sortDir=${sortDir}, status=${param.status})}"
|
||||
th:text="${i + 1}">1</a>
|
||||
</li>
|
||||
<li class="page-item" th:classappend="${currentPage == totalPages - 1} ? 'disabled' : ''">
|
||||
<a class="page-link" th:href="@{/orders/admin(page=${currentPage + 1}, size=${orders.size}, sortBy=${sortBy}, sortDir=${sortDir}, status=${param.status})}">下一页</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update Status Modal -->
|
||||
<div class="modal fade" id="updateStatusModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-edit me-2"></i>更新订单状态
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="updateStatusForm">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="newStatus" class="form-label">新状态</label>
|
||||
<select class="form-select" id="newStatus" required>
|
||||
<option th:each="status : ${orderStatuses}"
|
||||
th:value="${status.name()}"
|
||||
th:text="${status.displayName}">状态</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="statusNotes" class="form-label">备注(可选)</label>
|
||||
<textarea class="form-control" id="statusNotes" rows="3"
|
||||
placeholder="请输入状态更新备注..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="submit" class="btn btn-primary">确认更新</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cancel Order Modal -->
|
||||
<div class="modal fade" id="cancelOrderModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>取消订单
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="cancelOrderForm">
|
||||
<div class="modal-body">
|
||||
<p>确定要取消此订单吗?此操作不可撤销。</p>
|
||||
<div class="mb-3">
|
||||
<label for="cancelReason" class="form-label">取消原因(可选)</label>
|
||||
<textarea class="form-control" id="cancelReason" rows="3"
|
||||
placeholder="请输入取消原因..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="submit" class="btn btn-danger">确认取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ship Order Modal -->
|
||||
<div class="modal fade" id="shipOrderModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-truck me-2"></i>订单发货
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="shipOrderForm">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="trackingNumber" class="form-label">物流单号(可选)</label>
|
||||
<input type="text" class="form-control" id="trackingNumber"
|
||||
placeholder="请输入物流单号...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="submit" class="btn btn-info">确认发货</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.avatar-circle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.order-row:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.btn-group .btn:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.table th {
|
||||
border-top: none;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentOrderId = null;
|
||||
|
||||
// Status filter
|
||||
document.getElementById('statusFilter').addEventListener('change', function() {
|
||||
const status = this.value;
|
||||
const url = new URL(window.location);
|
||||
if (status) {
|
||||
url.searchParams.set('status', status);
|
||||
} else {
|
||||
url.searchParams.delete('status');
|
||||
}
|
||||
url.searchParams.set('page', '0'); // Reset to first page
|
||||
window.location.href = url.toString();
|
||||
});
|
||||
|
||||
// Search functionality
|
||||
document.getElementById('searchInput').addEventListener('input', function() {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
const rows = document.querySelectorAll('.order-row');
|
||||
|
||||
rows.forEach(function(row) {
|
||||
const orderNumber = row.cells[0].textContent.toLowerCase();
|
||||
const username = row.cells[1].textContent.toLowerCase();
|
||||
|
||||
if (orderNumber.includes(searchTerm) || username.includes(searchTerm)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Update status
|
||||
function updateStatus(orderId) {
|
||||
currentOrderId = orderId;
|
||||
const modal = new bootstrap.Modal(document.getElementById('updateStatusModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Cancel order
|
||||
function cancelOrder(orderId) {
|
||||
currentOrderId = orderId;
|
||||
const modal = new bootstrap.Modal(document.getElementById('cancelOrderModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Ship order
|
||||
function shipOrder(orderId) {
|
||||
currentOrderId = orderId;
|
||||
const modal = new bootstrap.Modal(document.getElementById('shipOrderModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Complete order
|
||||
function completeOrder(orderId) {
|
||||
if (confirm('确定要完成此订单吗?')) {
|
||||
fetch(`/orders/${orderId}/complete`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('完成订单失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('完成订单失败');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export orders
|
||||
function exportOrders() {
|
||||
const url = new URL(window.location);
|
||||
url.pathname = '/orders/export';
|
||||
window.open(url.toString(), '_blank');
|
||||
}
|
||||
|
||||
// Update status form submission
|
||||
document.getElementById('updateStatusForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentOrderId) return;
|
||||
|
||||
const status = document.getElementById('newStatus').value;
|
||||
const notes = document.getElementById('statusNotes').value;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('status', status);
|
||||
if (notes) {
|
||||
formData.append('notes', notes);
|
||||
}
|
||||
|
||||
fetch(`/orders/${currentOrderId}/status`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('更新状态失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('更新状态失败');
|
||||
});
|
||||
});
|
||||
|
||||
// Cancel order form submission
|
||||
document.getElementById('cancelOrderForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentOrderId) return;
|
||||
|
||||
const reason = document.getElementById('cancelReason').value;
|
||||
const formData = new FormData();
|
||||
if (reason) {
|
||||
formData.append('reason', reason);
|
||||
}
|
||||
|
||||
fetch(`/orders/${currentOrderId}/cancel`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('取消订单失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('取消订单失败');
|
||||
});
|
||||
});
|
||||
|
||||
// Ship order form submission
|
||||
document.getElementById('shipOrderForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentOrderId) return;
|
||||
|
||||
const trackingNumber = document.getElementById('trackingNumber').value;
|
||||
const formData = new FormData();
|
||||
if (trackingNumber) {
|
||||
formData.append('trackingNumber', trackingNumber);
|
||||
}
|
||||
|
||||
fetch(`/orders/${currentOrderId}/ship`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('发货失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('发货失败');
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
setInterval(function() {
|
||||
// Only refresh if no modal is open and no form is being edited
|
||||
if (!document.querySelector('.modal.show') && !document.querySelector('form:focus')) {
|
||||
location.reload();
|
||||
}
|
||||
}, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
479
demo/src/main/resources/templates/orders/detail.html
Normal file
479
demo/src/main/resources/templates/orders/detail.html
Normal file
@@ -0,0 +1,479 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::title}, ~{::content})}">
|
||||
<head>
|
||||
<title>订单详情</title>
|
||||
</head>
|
||||
<body>
|
||||
<div th:fragment="content">
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a th:href="@{/}">首页</a></li>
|
||||
<li class="breadcrumb-item"><a th:href="@{/orders}">订单管理</a></li>
|
||||
<li class="breadcrumb-item active" th:text="${order.orderNumber}">订单详情</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Order Details -->
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<!-- Order Info Card -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-shopping-cart me-2"></i>订单信息
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<span th:if="${order.status.name() == 'PENDING'}" class="badge bg-warning fs-6" th:text="${order.status.displayName}">待支付</span>
|
||||
<span th:if="${order.status.name() == 'CONFIRMED'}" class="badge bg-info fs-6" th:text="${order.status.displayName}">已确认</span>
|
||||
<span th:if="${order.status.name() == 'PAID'}" class="badge bg-primary fs-6" th:text="${order.status.displayName}">已支付</span>
|
||||
<span th:if="${order.status.name() == 'PROCESSING'}" class="badge bg-secondary fs-6" th:text="${order.status.displayName}">处理中</span>
|
||||
<span th:if="${order.status.name() == 'SHIPPED'}" class="badge bg-success fs-6" th:text="${order.status.displayName}">已发货</span>
|
||||
<span th:if="${order.status.name() == 'DELIVERED'}" class="badge bg-success fs-6" th:text="${order.status.displayName}">已送达</span>
|
||||
<span th:if="${order.status.name() == 'COMPLETED'}" class="badge bg-success fs-6" th:text="${order.status.displayName}">已完成</span>
|
||||
<span th:if="${order.status.name() == 'CANCELLED'}" class="badge bg-danger fs-6" th:text="${order.status.displayName}">已取消</span>
|
||||
<span th:if="${order.status.name() == 'REFUNDED'}" class="badge bg-secondary fs-6" th:text="${order.status.displayName}">已退款</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<table class="table table-sm table-borderless">
|
||||
<tr>
|
||||
<td class="fw-bold">订单号:</td>
|
||||
<td th:text="${order.orderNumber}">ORD123456789</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">订单类型:</td>
|
||||
<td th:text="${order.orderType.displayName}">商品订单</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">创建时间:</td>
|
||||
<td th:text="${#temporals.format(order.createdAt, 'yyyy-MM-dd HH:mm:ss')}">2024-01-01 10:00:00</td>
|
||||
</tr>
|
||||
<tr th:if="${order.paidAt}">
|
||||
<td class="fw-bold">支付时间:</td>
|
||||
<td th:text="${#temporals.format(order.paidAt, 'yyyy-MM-dd HH:mm:ss')}">2024-01-01 10:30:00</td>
|
||||
</tr>
|
||||
<tr th:if="${order.shippedAt}">
|
||||
<td class="fw-bold">发货时间:</td>
|
||||
<td th:text="${#temporals.format(order.shippedAt, 'yyyy-MM-dd HH:mm:ss')}">2024-01-02 09:00:00</td>
|
||||
</tr>
|
||||
<tr th:if="${order.deliveredAt}">
|
||||
<td class="fw-bold">送达时间:</td>
|
||||
<td th:text="${#temporals.format(order.deliveredAt, 'yyyy-MM-dd HH:mm:ss')}">2024-01-03 14:00:00</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<table class="table table-sm table-borderless">
|
||||
<tr>
|
||||
<td class="fw-bold">订单金额:</td>
|
||||
<td class="fs-5 text-primary">
|
||||
<span th:text="${order.currency}">CNY</span>
|
||||
<span th:text="${order.totalAmount}">0.00</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${order.description}">
|
||||
<td class="fw-bold">订单描述:</td>
|
||||
<td th:text="${order.description}">订单描述</td>
|
||||
</tr>
|
||||
<tr th:if="${order.contactEmail}">
|
||||
<td class="fw-bold">联系邮箱:</td>
|
||||
<td th:text="${order.contactEmail}">user@example.com</td>
|
||||
</tr>
|
||||
<tr th:if="${order.contactPhone}">
|
||||
<td class="fw-bold">联系电话:</td>
|
||||
<td th:text="${order.contactPhone}">13800138000</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Notes -->
|
||||
<div th:if="${order.notes}" class="mt-3">
|
||||
<h6 class="fw-bold">订单备注:</h6>
|
||||
<div class="bg-light p-3 rounded">
|
||||
<pre th:text="${order.notes}" class="mb-0">订单备注内容</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Items -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-list me-2"></i>订单商品
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>商品名称</th>
|
||||
<th width="100">单价</th>
|
||||
<th width="80">数量</th>
|
||||
<th width="100">小计</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="item : ${order.orderItems}">
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div th:if="${item.productImage}" class="me-3">
|
||||
<img th:src="${item.productImage}" class="img-thumbnail"
|
||||
style="width: 50px; height: 50px; object-fit: cover;">
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold" th:text="${item.productName}">商品名称</div>
|
||||
<div th:if="${item.productDescription}" class="text-muted small"
|
||||
th:text="${item.productDescription}">商品描述</div>
|
||||
<div th:if="${item.productSku}" class="text-muted small">
|
||||
SKU: <span th:text="${item.productSku}">SKU123</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span th:text="${order.currency}">CNY</span>
|
||||
<span th:text="${item.unitPrice}">0.00</span>
|
||||
</td>
|
||||
<td th:text="${item.quantity}">1</td>
|
||||
<td>
|
||||
<span th:text="${order.currency}">CNY</span>
|
||||
<span th:text="${item.subtotal}">0.00</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="table-active">
|
||||
<th colspan="3" class="text-end">订单总计:</th>
|
||||
<th class="text-primary">
|
||||
<span th:text="${order.currency}">CNY</span>
|
||||
<span th:text="${order.totalAmount}">0.00</span>
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address Information -->
|
||||
<div class="card mb-4" th:if="${order.shippingAddress or order.billingAddress}">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-map-marker-alt me-2"></i>地址信息
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6" th:if="${order.shippingAddress}">
|
||||
<h6 class="fw-bold">收货地址:</h6>
|
||||
<div class="bg-light p-3 rounded">
|
||||
<pre th:text="${order.shippingAddress}" class="mb-0">收货地址</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6" th:if="${order.billingAddress}">
|
||||
<h6 class="fw-bold">账单地址:</h6>
|
||||
<div class="bg-light p-3 rounded">
|
||||
<pre th:text="${order.billingAddress}" class="mb-0">账单地址</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Order Actions -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-cogs me-2"></i>订单操作
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
<!-- Pay Button -->
|
||||
<div th:if="${order.canPay()}" class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-success dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-credit-card me-2"></i>立即支付
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" th:href="@{|/orders/${order.id}/pay?paymentMethod=ALIPAY|}">
|
||||
<i class="fab fa-alipay me-2"></i>支付宝支付
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" th:href="@{|/orders/${order.id}/pay?paymentMethod=PAYPAL|}">
|
||||
<i class="fab fa-paypal me-2"></i>PayPal支付
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Cancel Button -->
|
||||
<button th:if="${order.canCancel()}" type="button" class="btn btn-danger"
|
||||
th:onclick="'cancelOrder(' + ${order.id} + ')'">
|
||||
<i class="fas fa-times me-2"></i>取消订单
|
||||
</button>
|
||||
|
||||
<!-- Admin Actions -->
|
||||
<div sec:authorize="hasRole('ADMIN')" class="mt-3">
|
||||
<hr>
|
||||
<h6 class="fw-bold">管理员操作</h6>
|
||||
|
||||
<!-- Ship Button -->
|
||||
<button th:if="${order.canShip()}" type="button" class="btn btn-info mb-2"
|
||||
th:onclick="'shipOrder(' + ${order.id} + ')'">
|
||||
<i class="fas fa-truck me-2"></i>发货
|
||||
</button>
|
||||
|
||||
<!-- Complete Button -->
|
||||
<button th:if="${order.canComplete()}" type="button" class="btn btn-success mb-2"
|
||||
th:onclick="'completeOrder(' + ${order.id} + ')'">
|
||||
<i class="fas fa-check me-2"></i>完成订单
|
||||
</button>
|
||||
|
||||
<!-- Status Update -->
|
||||
<div class="mt-3">
|
||||
<label class="form-label">更新状态</label>
|
||||
<div class="input-group">
|
||||
<select class="form-select" id="statusSelect">
|
||||
<option th:each="status : ${orderStatuses}"
|
||||
th:value="${status.name()}"
|
||||
th:text="${status.displayName}"
|
||||
th:selected="${status == order.status}"></option>
|
||||
</select>
|
||||
<button class="btn btn-outline-primary" type="button"
|
||||
th:onclick="'updateStatus(' + ${order.id} + ')'">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment History -->
|
||||
<div class="card" th:if="${order.payments != null and !order.payments.empty}">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-history me-2"></i>支付记录
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div th:each="payment : ${order.payments}" class="border-bottom pb-2 mb-2">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span th:text="${payment.paymentMethod.displayName}">支付宝</span>
|
||||
<span th:if="${payment.status.name() == 'SUCCESS'}" class="badge bg-success" th:text="${payment.status.displayName}">成功</span>
|
||||
<span th:if="${payment.status.name() == 'PENDING'}" class="badge bg-warning" th:text="${payment.status.displayName}">待支付</span>
|
||||
<span th:if="${payment.status.name() == 'FAILED'}" class="badge bg-danger" th:text="${payment.status.displayName}">失败</span>
|
||||
</div>
|
||||
<div class="small text-muted">
|
||||
<span th:text="${payment.currency}">CNY</span>
|
||||
<span th:text="${payment.amount}">0.00</span>
|
||||
<span th:text="${#temporals.format(payment.createdAt, 'MM-dd HH:mm')}">01-01 10:00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cancel Order Modal -->
|
||||
<div class="modal fade" id="cancelOrderModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>取消订单
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="cancelOrderForm">
|
||||
<div class="modal-body">
|
||||
<p>确定要取消此订单吗?此操作不可撤销。</p>
|
||||
<div class="mb-3">
|
||||
<label for="cancelReason" class="form-label">取消原因(可选)</label>
|
||||
<textarea class="form-control" id="cancelReason" rows="3"
|
||||
placeholder="请输入取消原因..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="submit" class="btn btn-danger">确认取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ship Order Modal -->
|
||||
<div class="modal fade" id="shipOrderModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-truck me-2"></i>订单发货
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="shipOrderForm">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="trackingNumber" class="form-label">物流单号(可选)</label>
|
||||
<input type="text" class="form-control" id="trackingNumber"
|
||||
placeholder="请输入物流单号...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="submit" class="btn btn-info">确认发货</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentOrderId = null;
|
||||
|
||||
// Cancel order
|
||||
function cancelOrder(orderId) {
|
||||
currentOrderId = orderId;
|
||||
const modal = new bootstrap.Modal(document.getElementById('cancelOrderModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Ship order
|
||||
function shipOrder(orderId) {
|
||||
currentOrderId = orderId;
|
||||
const modal = new bootstrap.Modal(document.getElementById('shipOrderModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Complete order
|
||||
function completeOrder(orderId) {
|
||||
if (confirm('确定要完成此订单吗?')) {
|
||||
fetch(`/orders/${orderId}/complete`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('完成订单失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('完成订单失败');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update status
|
||||
function updateStatus(orderId) {
|
||||
const status = document.getElementById('statusSelect').value;
|
||||
const notes = prompt('请输入备注(可选):');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('status', status);
|
||||
if (notes) {
|
||||
formData.append('notes', notes);
|
||||
}
|
||||
|
||||
fetch(`/orders/${orderId}/status`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('更新状态失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('更新状态失败');
|
||||
});
|
||||
}
|
||||
|
||||
// Cancel order form submission
|
||||
document.getElementById('cancelOrderForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentOrderId) return;
|
||||
|
||||
const reason = document.getElementById('cancelReason').value;
|
||||
const formData = new FormData();
|
||||
if (reason) {
|
||||
formData.append('reason', reason);
|
||||
}
|
||||
|
||||
fetch(`/orders/${currentOrderId}/cancel`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('取消订单失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('取消订单失败');
|
||||
});
|
||||
});
|
||||
|
||||
// Ship order form submission
|
||||
document.getElementById('shipOrderForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentOrderId) return;
|
||||
|
||||
const trackingNumber = document.getElementById('trackingNumber').value;
|
||||
const formData = new FormData();
|
||||
if (trackingNumber) {
|
||||
formData.append('trackingNumber', trackingNumber);
|
||||
}
|
||||
|
||||
fetch(`/orders/${currentOrderId}/ship`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('发货失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('发货失败');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
518
demo/src/main/resources/templates/orders/form.html
Normal file
518
demo/src/main/resources/templates/orders/form.html
Normal file
@@ -0,0 +1,518 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::title}, ~{::content})}">
|
||||
<head>
|
||||
<title>创建订单</title>
|
||||
</head>
|
||||
<body>
|
||||
<div th:fragment="content">
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a th:href="@{/}">首页</a></li>
|
||||
<li class="breadcrumb-item"><a th:href="@{/orders}">订单管理</a></li>
|
||||
<li class="breadcrumb-item active">创建订单</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Order Form -->
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<form th:action="@{/orders/create}" method="post" th:object="${order}" id="orderForm">
|
||||
<!-- Order Basic Info -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>订单基本信息
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="orderType" class="form-label">
|
||||
<i class="fas fa-tag me-2"></i>订单类型
|
||||
</label>
|
||||
<select class="form-select" id="orderType" th:field="*{orderType}" required>
|
||||
<option th:each="type : ${orderTypes}"
|
||||
th:value="${type.name()}"
|
||||
th:text="${type.displayName}">商品订单</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="currency" class="form-label">
|
||||
<i class="fas fa-coins me-2"></i>货币
|
||||
</label>
|
||||
<select class="form-select" id="currency" th:field="*{currency}">
|
||||
<option value="CNY">人民币 (CNY)</option>
|
||||
<option value="USD">美元 (USD)</option>
|
||||
<option value="EUR">欧元 (EUR)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">
|
||||
<i class="fas fa-file-text me-2"></i>订单描述
|
||||
</label>
|
||||
<textarea class="form-control" id="description" th:field="*{description}"
|
||||
rows="3" placeholder="请输入订单描述..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Items -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-shopping-basket me-2"></i>订单商品
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addItemBtn">
|
||||
<i class="fas fa-plus me-1"></i>添加商品
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="orderItems">
|
||||
<div class="order-item border rounded p-3 mb-3" data-index="0">
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">商品名称 *</label>
|
||||
<input type="text" class="form-control item-name" name="orderItems[0].productName"
|
||||
placeholder="请输入商品名称" required>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">SKU</label>
|
||||
<input type="text" class="form-control item-sku" name="orderItems[0].productSku"
|
||||
placeholder="商品SKU">
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<label class="form-label">单价 *</label>
|
||||
<input type="number" class="form-control item-price" name="orderItems[0].unitPrice"
|
||||
step="0.01" min="0" placeholder="0.00" required>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<label class="form-label">数量 *</label>
|
||||
<input type="number" class="form-control item-quantity" name="orderItems[0].quantity"
|
||||
min="1" value="1" required>
|
||||
</div>
|
||||
<div class="col-md-1 mb-3">
|
||||
<label class="form-label"> </label>
|
||||
<button type="button" class="btn btn-outline-danger d-block remove-item"
|
||||
style="display: none;">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8 mb-3">
|
||||
<label class="form-label">商品描述</label>
|
||||
<textarea class="form-control item-description" name="orderItems[0].productDescription"
|
||||
rows="2" placeholder="请输入商品描述..."></textarea>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">小计</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text currency-symbol">CNY</span>
|
||||
<input type="text" class="form-control item-subtotal" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Total -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-8"></div>
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-bold">订单总计:</span>
|
||||
<span class="fs-5 text-primary">
|
||||
<span class="currency-symbol">CNY</span>
|
||||
<span id="totalAmount">0.00</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Information -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-user me-2"></i>联系信息
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="contactEmail" class="form-label">
|
||||
<i class="fas fa-envelope me-2"></i>联系邮箱
|
||||
</label>
|
||||
<input type="email" class="form-control" id="contactEmail" th:field="*{contactEmail}"
|
||||
placeholder="请输入联系邮箱">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="contactPhone" class="form-label">
|
||||
<i class="fas fa-phone me-2"></i>联系电话
|
||||
</label>
|
||||
<input type="tel" class="form-control" id="contactPhone" th:field="*{contactPhone}"
|
||||
placeholder="请输入联系电话">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address Information -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-map-marker-alt me-2"></i>地址信息
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="shippingAddress" class="form-label">
|
||||
<i class="fas fa-truck me-2"></i>收货地址
|
||||
</label>
|
||||
<textarea class="form-control" id="shippingAddress" th:field="*{shippingAddress}"
|
||||
rows="3" placeholder="请输入收货地址..."></textarea>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="billingAddress" class="form-label">
|
||||
<i class="fas fa-file-invoice me-2"></i>账单地址
|
||||
</label>
|
||||
<textarea class="form-control" id="billingAddress" th:field="*{billingAddress}"
|
||||
rows="3" placeholder="请输入账单地址..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a th:href="@{/orders}" class="btn btn-secondary me-md-2">
|
||||
<i class="fas fa-times me-2"></i>取消
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<span class="loading">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>创建中...
|
||||
</span>
|
||||
<span class="submit-text">
|
||||
<i class="fas fa-save me-2"></i>创建订单
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Order Summary -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-calculator me-2"></i>订单摘要
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="orderSummary">
|
||||
<div class="text-muted text-center py-3">
|
||||
<i class="fas fa-shopping-basket fa-2x mb-2"></i>
|
||||
<p>请添加商品到订单</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-question-circle me-2"></i>帮助信息
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="small">
|
||||
<h6>订单类型说明:</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><span class="badge bg-primary me-2">商品订单</span>实体商品订单</li>
|
||||
<li><span class="badge bg-info me-2">服务订单</span>服务类订单</li>
|
||||
<li><span class="badge bg-success me-2">订阅订单</span>订阅服务订单</li>
|
||||
<li><span class="badge bg-warning me-2">数字商品</span>数字产品订单</li>
|
||||
<li><span class="badge bg-secondary me-2">实体商品</span>实体产品订单</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="mt-3">注意事项:</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li>• 订单创建后状态为"待支付"</li>
|
||||
<li>• 支持支付宝和PayPal支付</li>
|
||||
<li>• 订单超时未支付将自动取消</li>
|
||||
<li>• 请确保联系信息准确无误</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.order-item {
|
||||
background-color: #f8f9fa;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.order-item:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.item-subtotal {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let itemIndex = 0;
|
||||
|
||||
// Add new order item
|
||||
document.getElementById('addItemBtn').addEventListener('click', function() {
|
||||
itemIndex++;
|
||||
const template = document.querySelector('.order-item').cloneNode(true);
|
||||
|
||||
// Update indices
|
||||
template.setAttribute('data-index', itemIndex);
|
||||
template.querySelectorAll('input, textarea').forEach(function(input) {
|
||||
const name = input.getAttribute('name');
|
||||
if (name) {
|
||||
input.setAttribute('name', name.replace('[0]', '[' + itemIndex + ']'));
|
||||
}
|
||||
});
|
||||
|
||||
// Clear values
|
||||
template.querySelectorAll('input, textarea').forEach(function(input) {
|
||||
if (input.type !== 'number' || input.classList.contains('item-quantity')) {
|
||||
input.value = '';
|
||||
}
|
||||
if (input.classList.contains('item-quantity')) {
|
||||
input.value = '1';
|
||||
}
|
||||
});
|
||||
|
||||
// Show remove button
|
||||
template.querySelector('.remove-item').style.display = 'block';
|
||||
|
||||
// Add event listeners
|
||||
addItemEventListeners(template);
|
||||
|
||||
document.getElementById('orderItems').appendChild(template);
|
||||
updateOrderSummary();
|
||||
});
|
||||
|
||||
// Remove order item
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.remove-item')) {
|
||||
const item = e.target.closest('.order-item');
|
||||
item.remove();
|
||||
updateOrderSummary();
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listeners to order item
|
||||
function addItemEventListeners(item) {
|
||||
const priceInput = item.querySelector('.item-price');
|
||||
const quantityInput = item.querySelector('.item-quantity');
|
||||
const subtotalInput = item.querySelector('.item-subtotal');
|
||||
|
||||
function updateSubtotal() {
|
||||
const price = parseFloat(priceInput.value) || 0;
|
||||
const quantity = parseInt(quantityInput.value) || 0;
|
||||
const subtotal = price * quantity;
|
||||
subtotalInput.value = subtotal.toFixed(2);
|
||||
updateOrderSummary();
|
||||
}
|
||||
|
||||
priceInput.addEventListener('input', updateSubtotal);
|
||||
quantityInput.addEventListener('input', updateSubtotal);
|
||||
}
|
||||
|
||||
// Add event listeners to existing items
|
||||
document.querySelectorAll('.order-item').forEach(addItemEventListeners);
|
||||
|
||||
// Update order summary
|
||||
function updateOrderSummary() {
|
||||
const items = document.querySelectorAll('.order-item');
|
||||
const summary = document.getElementById('orderSummary');
|
||||
let total = 0;
|
||||
let itemCount = 0;
|
||||
|
||||
items.forEach(function(item) {
|
||||
const price = parseFloat(item.querySelector('.item-price').value) || 0;
|
||||
const quantity = parseInt(item.querySelector('.item-quantity').value) || 0;
|
||||
const subtotal = price * quantity;
|
||||
|
||||
if (price > 0 && quantity > 0) {
|
||||
total += subtotal;
|
||||
itemCount++;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('totalAmount').textContent = total.toFixed(2);
|
||||
|
||||
if (itemCount > 0) {
|
||||
let summaryHtml = '<div class="mb-3">';
|
||||
items.forEach(function(item, index) {
|
||||
const name = item.querySelector('.item-name').value;
|
||||
const price = parseFloat(item.querySelector('.item-price').value) || 0;
|
||||
const quantity = parseInt(item.querySelector('.item-quantity').value) || 0;
|
||||
const subtotal = price * quantity;
|
||||
|
||||
if (name && price > 0 && quantity > 0) {
|
||||
summaryHtml += `
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<div class="fw-bold">${name}</div>
|
||||
<small class="text-muted">${price.toFixed(2)} × ${quantity}</small>
|
||||
</div>
|
||||
<span class="fw-bold">${subtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
summaryHtml += '</div>';
|
||||
summaryHtml += `
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-bold">总计:</span>
|
||||
<span class="fs-5 text-primary">${total.toFixed(2)}</span>
|
||||
</div>
|
||||
`;
|
||||
summary.innerHTML = summaryHtml;
|
||||
} else {
|
||||
summary.innerHTML = `
|
||||
<div class="text-muted text-center py-3">
|
||||
<i class="fas fa-shopping-basket fa-2x mb-2"></i>
|
||||
<p>请添加商品到订单</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Currency change
|
||||
document.getElementById('currency').addEventListener('change', function() {
|
||||
const currency = this.value;
|
||||
document.querySelectorAll('.currency-symbol').forEach(function(symbol) {
|
||||
symbol.textContent = currency;
|
||||
});
|
||||
});
|
||||
|
||||
// Form submission
|
||||
document.getElementById('orderForm').addEventListener('submit', function(e) {
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const loading = submitBtn.querySelector('.loading');
|
||||
const submitText = submitBtn.querySelector('.submit-text');
|
||||
|
||||
// Show loading state
|
||||
loading.classList.add('show');
|
||||
submitText.style.display = 'none';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
// Validate form
|
||||
const items = document.querySelectorAll('.order-item');
|
||||
let hasValidItem = false;
|
||||
|
||||
items.forEach(function(item) {
|
||||
const name = item.querySelector('.item-name').value.trim();
|
||||
const price = parseFloat(item.querySelector('.item-price').value) || 0;
|
||||
const quantity = parseInt(item.querySelector('.item-quantity').value) || 0;
|
||||
|
||||
if (name && price > 0 && quantity > 0) {
|
||||
hasValidItem = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasValidItem) {
|
||||
e.preventDefault();
|
||||
alert('请至少添加一个有效商品');
|
||||
loading.classList.remove('show');
|
||||
submitText.style.display = 'inline-block';
|
||||
submitBtn.disabled = false;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-save draft
|
||||
const formData = {};
|
||||
const form = document.getElementById('orderForm');
|
||||
|
||||
form.addEventListener('input', function(e) {
|
||||
if (e.target.name && e.target.name.includes('orderItems')) {
|
||||
return; // Skip order items for now
|
||||
}
|
||||
|
||||
formData[e.target.name] = e.target.value;
|
||||
localStorage.setItem('orderFormDraft', JSON.stringify(formData));
|
||||
});
|
||||
|
||||
// Restore draft on page load
|
||||
const savedDraft = localStorage.getItem('orderFormDraft');
|
||||
if (savedDraft) {
|
||||
try {
|
||||
const draft = JSON.parse(savedDraft);
|
||||
Object.keys(draft).forEach(function(key) {
|
||||
const field = form.querySelector(`[name="${key}"]`);
|
||||
if (field && draft[key]) {
|
||||
field.value = draft[key];
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to restore form draft:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear draft on successful submission
|
||||
form.addEventListener('submit', function() {
|
||||
localStorage.removeItem('orderFormDraft');
|
||||
});
|
||||
|
||||
// Initial update
|
||||
updateOrderSummary();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
152
demo/src/main/resources/templates/payment/detail.html
Normal file
152
demo/src/main/resources/templates/payment/detail.html
Normal file
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" 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 href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.detail-card {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.info-row {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.status-badge {
|
||||
font-size: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card detail-card shadow">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h4 class="mb-0"><i class="fas fa-receipt me-2"></i>支付详情</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div th:if="${error}" class="alert alert-danger" role="alert">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
|
||||
<div th:if="${payment}">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="info-row">
|
||||
<strong>订单号:</strong>
|
||||
<span th:text="${payment.orderId}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="info-row">
|
||||
<strong>支付状态:</strong>
|
||||
<span th:if="${payment.status.name() == 'SUCCESS'}"
|
||||
class="badge bg-success status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'FAILED'}"
|
||||
class="badge bg-danger status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'PENDING'}"
|
||||
class="badge bg-warning status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'PROCESSING'}"
|
||||
class="badge bg-info status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'CANCELLED'}"
|
||||
class="badge bg-secondary status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'REFUNDED'}"
|
||||
class="badge bg-dark status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="info-row">
|
||||
<strong>支付金额:</strong>
|
||||
<span class="h5 text-primary">
|
||||
<span th:text="${payment.currency}"></span>
|
||||
<span th:text="${payment.amount}"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="info-row">
|
||||
<strong>支付方式:</strong>
|
||||
<span th:text="${payment.paymentMethod.displayName}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div th:if="${payment.description}" class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="info-row">
|
||||
<strong>支付描述:</strong>
|
||||
<span th:text="${payment.description}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div th:if="${payment.externalTransactionId}" class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="info-row">
|
||||
<strong>外部交易ID:</strong>
|
||||
<span th:text="${payment.externalTransactionId}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="info-row">
|
||||
<strong>创建时间:</strong>
|
||||
<span th:text="${#temporals.format(payment.createdAt, 'yyyy-MM-dd HH:mm:ss')}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="info-row">
|
||||
<strong>更新时间:</strong>
|
||||
<span th:text="${#temporals.format(payment.updatedAt, 'yyyy-MM-dd HH:mm:ss')}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div th:if="${payment.paidAt}" class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="info-row">
|
||||
<strong>支付时间:</strong>
|
||||
<span th:text="${#temporals.format(payment.paidAt, 'yyyy-MM-dd HH:mm:ss')}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a th:href="@{/payment/history}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i>返回列表
|
||||
</a>
|
||||
<a th:href="@{/payment/create}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-2"></i>新建支付
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
157
demo/src/main/resources/templates/payment/form.html
Normal file
157
demo/src/main/resources/templates/payment/form.html
Normal file
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" 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 href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.payment-card {
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
.payment-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
|
||||
}
|
||||
.payment-card.selected {
|
||||
border-color: #007bff;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.payment-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.alipay-icon {
|
||||
color: #1677ff;
|
||||
}
|
||||
.paypal-icon {
|
||||
color: #0070ba;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h3 class="mb-0"><i class="fas fa-credit-card me-2"></i>支付接入</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form th:action="@{/payment/create}" th:object="${payment}" method="post">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card payment-card h-100" onclick="selectPaymentMethod('ALIPAY')">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-mobile-alt payment-icon alipay-icon"></i>
|
||||
<h5 class="card-title">支付宝</h5>
|
||||
<p class="card-text">安全便捷的移动支付</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card payment-card h-100" onclick="selectPaymentMethod('PAYPAL')">
|
||||
<div class="card-body text-center">
|
||||
<i class="fab fa-paypal payment-icon paypal-icon"></i>
|
||||
<h5 class="card-title">PayPal</h5>
|
||||
<p class="card-text">全球领先的在线支付</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="amount" class="form-label">支付金额</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">¥</span>
|
||||
<input type="number" class="form-control" id="amount" name="amount"
|
||||
th:field="*{amount}" step="0.01" min="0.01" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="currency" class="form-label">货币类型</label>
|
||||
<select class="form-select" id="currency" name="currency" th:field="*{currency}">
|
||||
<option value="CNY">人民币 (CNY)</option>
|
||||
<option value="USD">美元 (USD)</option>
|
||||
<option value="EUR">欧元 (EUR)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">支付描述</label>
|
||||
<textarea class="form-control" id="description" name="description"
|
||||
th:field="*{description}" rows="3" placeholder="请输入支付描述..."></textarea>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="paymentMethod" name="paymentMethod" th:field="*{paymentMethod}">
|
||||
|
||||
<div th:if="${error}" class="alert alert-danger" role="alert">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-lg" id="submitBtn" disabled>
|
||||
<i class="fas fa-lock me-2"></i>确认支付
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
function selectPaymentMethod(method) {
|
||||
// 移除所有选中状态
|
||||
document.querySelectorAll('.payment-card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
|
||||
// 添加选中状态
|
||||
event.currentTarget.classList.add('selected');
|
||||
|
||||
// 设置隐藏字段值
|
||||
document.getElementById('paymentMethod').value = method;
|
||||
|
||||
// 启用提交按钮
|
||||
document.getElementById('submitBtn').disabled = false;
|
||||
|
||||
// 根据支付方式调整货币选项
|
||||
const currencySelect = document.getElementById('currency');
|
||||
if (method === 'ALIPAY') {
|
||||
currencySelect.value = 'CNY';
|
||||
currencySelect.options[0].selected = true;
|
||||
} else if (method === 'PAYPAL') {
|
||||
currencySelect.value = 'USD';
|
||||
currencySelect.options[1].selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证
|
||||
document.querySelector('form').addEventListener('submit', function(e) {
|
||||
const amount = document.getElementById('amount').value;
|
||||
const paymentMethod = document.getElementById('paymentMethod').value;
|
||||
|
||||
if (!paymentMethod) {
|
||||
e.preventDefault();
|
||||
alert('请选择支付方式');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!amount || parseFloat(amount) <= 0) {
|
||||
e.preventDefault();
|
||||
alert('请输入有效的支付金额');
|
||||
return;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
118
demo/src/main/resources/templates/payment/history.html
Normal file
118
demo/src/main/resources/templates/payment/history.html
Normal file
@@ -0,0 +1,118 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" 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 href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.payment-card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.payment-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
.status-badge {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="fas fa-history me-2"></i>支付记录</h2>
|
||||
<a th:href="@{/payment/create}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-2"></i>新建支付
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div th:if="${error}" class="alert alert-danger" role="alert">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
|
||||
<div th:if="${payments != null and !payments.empty}">
|
||||
<div class="row">
|
||||
<div th:each="payment : ${payments}" class="col-md-6 col-lg-4 mb-3">
|
||||
<div class="card payment-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<h6 class="card-title mb-0" th:text="${payment.orderId}"></h6>
|
||||
<span th:if="${payment.status.name() == 'SUCCESS'}"
|
||||
class="badge bg-success status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'FAILED'}"
|
||||
class="badge bg-danger status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'PENDING'}"
|
||||
class="badge bg-warning status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'PROCESSING'}"
|
||||
class="badge bg-info status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'CANCELLED'}"
|
||||
class="badge bg-secondary status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'REFUNDED'}"
|
||||
class="badge bg-dark status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<strong class="text-primary" th:text="${payment.currency}"></strong>
|
||||
<span class="h5" th:text="${payment.amount}"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-credit-card me-1"></i>
|
||||
<span th:text="${payment.paymentMethod.displayName}"></span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div th:if="${payment.description}" class="mb-2">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-file-text me-1"></i>
|
||||
<span th:text="${payment.description}"></span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-clock me-1"></i>
|
||||
<span th:text="${#temporals.format(payment.createdAt, 'yyyy-MM-dd HH:mm')}"></span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<a th:href="@{/payment/detail/{id}(id=${payment.id})}"
|
||||
class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-eye me-1"></i>查看详情
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div th:if="${payments == null or payments.empty}" class="text-center py-5">
|
||||
<i class="fas fa-receipt fa-3x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">暂无支付记录</h4>
|
||||
<p class="text-muted">您还没有任何支付记录</p>
|
||||
<a th:href="@{/payment/create}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-2"></i>创建支付
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
107
demo/src/main/resources/templates/payment/result.html
Normal file
107
demo/src/main/resources/templates/payment/result.html
Normal file
@@ -0,0 +1,107 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" 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 href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.result-card {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.success-icon {
|
||||
color: #28a745;
|
||||
font-size: 4rem;
|
||||
}
|
||||
.error-icon {
|
||||
color: #dc3545;
|
||||
font-size: 4rem;
|
||||
}
|
||||
.payment-info {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card result-card shadow">
|
||||
<div class="card-body text-center">
|
||||
<div th:if="${success}">
|
||||
<i class="fas fa-check-circle success-icon mb-3"></i>
|
||||
<h3 class="text-success mb-3">支付成功!</h3>
|
||||
<p class="text-muted mb-4">您的支付已完成,感谢您的使用。</p>
|
||||
</div>
|
||||
|
||||
<div th:if="${error}">
|
||||
<i class="fas fa-times-circle error-icon mb-3"></i>
|
||||
<h3 class="text-danger mb-3">支付失败</h3>
|
||||
<p class="text-muted mb-4" th:text="${error}"></p>
|
||||
</div>
|
||||
|
||||
<div th:if="${payment}" class="payment-info mb-4">
|
||||
<h5 class="mb-3">支付详情</h5>
|
||||
<div class="row text-start">
|
||||
<div class="col-6">
|
||||
<strong>订单号:</strong>
|
||||
</div>
|
||||
<div class="col-6" th:text="${payment.orderId}"></div>
|
||||
|
||||
<div class="col-6">
|
||||
<strong>支付金额:</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<span th:text="${payment.currency}"></span>
|
||||
<span th:text="${payment.amount}"></span>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<strong>支付方式:</strong>
|
||||
</div>
|
||||
<div class="col-6" th:text="${payment.paymentMethod.displayName}"></div>
|
||||
|
||||
<div class="col-6">
|
||||
<strong>支付状态:</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<span th:if="${payment.status.name() == 'SUCCESS'}" class="badge bg-success" th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'FAILED'}" class="badge bg-danger" th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'PENDING'}" class="badge bg-warning" th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'PROCESSING'}" class="badge bg-info" th:text="${payment.status.displayName}"></span>
|
||||
</div>
|
||||
|
||||
<div th:if="${payment.description}" class="col-12 mt-2">
|
||||
<strong>支付描述:</strong>
|
||||
<span th:text="${payment.description}"></span>
|
||||
</div>
|
||||
|
||||
<div th:if="${payment.paidAt}" class="col-12 mt-2">
|
||||
<strong>支付时间:</strong>
|
||||
<span th:text="${#temporals.format(payment.paidAt, 'yyyy-MM-dd HH:mm:ss')}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<a th:href="@{/payment/history}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-history me-2"></i>查看支付记录
|
||||
</a>
|
||||
<a th:href="@{/}" class="btn btn-primary">
|
||||
<i class="fas fa-home me-2"></i>返回首页
|
||||
</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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
477
demo/src/main/resources/templates/register.html
Normal file
477
demo/src/main/resources/templates/register.html
Normal file
@@ -0,0 +1,477 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户注册 - AIGC Demo</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.register-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
padding: 3rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.register-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.register-header h2 {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.register-header p {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-floating {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 10px;
|
||||
padding: 1rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
.form-control.is-invalid {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.form-control.is-valid {
|
||||
border-color: #198754;
|
||||
}
|
||||
|
||||
.btn-register {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 2rem;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: white;
|
||||
width: 100%;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn-register:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(40, 167, 69, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-register:disabled {
|
||||
opacity: 0.6;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: linear-gradient(135deg, #f8d7da, #f5c6cb);
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: #dc3545;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.valid-feedback {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: #198754;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.register-footer {
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.register-footer a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.register-footer a:hover {
|
||||
color: #764ba2;
|
||||
}
|
||||
|
||||
.language-switcher {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.language-switcher a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
margin: 0 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.language-switcher a:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.password-strength {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.strength-bar {
|
||||
height: 4px;
|
||||
background: #e9ecef;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.strength-fill {
|
||||
height: 100%;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.strength-weak { background: #dc3545; width: 25%; }
|
||||
.strength-fair { background: #ffc107; width: 50%; }
|
||||
.strength-good { background: #17a2b8; width: 75%; }
|
||||
.strength-strong { background: #28a745; width: 100%; }
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: #28a745;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
color: #dc3545;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="register-container">
|
||||
<div class="register-header">
|
||||
<h2><i class="fas fa-user-plus me-2"></i>创建账户</h2>
|
||||
<p>加入我们,开始您的AIGC之旅</p>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div th:if="${error}" class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
|
||||
<form th:action="@{/register}" method="post" th:object="${form}" id="registerForm">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="username" th:field="*{username}"
|
||||
placeholder="用户名" required minlength="3" maxlength="50" autocomplete="username">
|
||||
<label for="username">
|
||||
<i class="fas fa-user me-2"></i>用户名
|
||||
</label>
|
||||
<div class="invalid-feedback" th:if="${#fields.hasErrors('username')}" th:errors="*{username}"></div>
|
||||
<div class="valid-feedback" th:if="${!#fields.hasErrors('username') and form.username != null}">
|
||||
<i class="fas fa-check check-icon"></i>用户名可用
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-floating">
|
||||
<input type="email" class="form-control" id="email" th:field="*{email}"
|
||||
placeholder="邮箱地址" required autocomplete="email">
|
||||
<label for="email">
|
||||
<i class="fas fa-envelope me-2"></i>邮箱地址
|
||||
</label>
|
||||
<div class="invalid-feedback" th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></div>
|
||||
<div class="valid-feedback" th:if="${!#fields.hasErrors('email') and form.email != null}">
|
||||
<i class="fas fa-check check-icon"></i>邮箱格式正确
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-floating">
|
||||
<input type="password" class="form-control" id="password" th:field="*{password}"
|
||||
placeholder="密码" required minlength="6" maxlength="100" autocomplete="new-password">
|
||||
<label for="password">
|
||||
<i class="fas fa-lock me-2"></i>密码
|
||||
</label>
|
||||
<div class="password-strength">
|
||||
<div class="strength-bar">
|
||||
<div class="strength-fill" id="strengthFill"></div>
|
||||
</div>
|
||||
<small class="text-muted" id="strengthText">密码强度</small>
|
||||
</div>
|
||||
<div class="invalid-feedback" th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-floating">
|
||||
<input type="password" class="form-control" id="confirmPassword" th:field="*{confirmPassword}"
|
||||
placeholder="确认密码" required minlength="6" maxlength="100" autocomplete="new-password">
|
||||
<label for="confirmPassword">
|
||||
<i class="fas fa-lock me-2"></i>确认密码
|
||||
</label>
|
||||
<div class="invalid-feedback" th:if="${#fields.hasErrors('confirmPassword')}" th:errors="*{confirmPassword}"></div>
|
||||
<div class="valid-feedback" id="passwordMatch" style="display: none;">
|
||||
<i class="fas fa-check check-icon"></i>密码匹配
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-register" id="registerBtn">
|
||||
<span class="loading">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>注册中...
|
||||
</span>
|
||||
<span class="register-text">
|
||||
<i class="fas fa-user-plus me-2"></i>创建账户
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="register-footer">
|
||||
<p class="mb-2">
|
||||
<span th:text="#{hint.haveaccount}">已有账号?</span>
|
||||
<a th:href="@{/login}">立即登录</a>
|
||||
</p>
|
||||
|
||||
<div class="language-switcher">
|
||||
<a th:href="@{|?lang=zh_CN|}">中文</a> |
|
||||
<a th:href="@{|?lang=en|}">English</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Password strength checker
|
||||
function checkPasswordStrength(password) {
|
||||
let strength = 0;
|
||||
let strengthText = '';
|
||||
let strengthClass = '';
|
||||
|
||||
if (password.length >= 6) strength++;
|
||||
if (password.match(/[a-z]/)) strength++;
|
||||
if (password.match(/[A-Z]/)) strength++;
|
||||
if (password.match(/[0-9]/)) strength++;
|
||||
if (password.match(/[^a-zA-Z0-9]/)) strength++;
|
||||
|
||||
switch (strength) {
|
||||
case 0:
|
||||
case 1:
|
||||
strengthText = '密码强度:弱';
|
||||
strengthClass = 'strength-weak';
|
||||
break;
|
||||
case 2:
|
||||
strengthText = '密码强度:一般';
|
||||
strengthClass = 'strength-fair';
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
strengthText = '密码强度:良好';
|
||||
strengthClass = 'strength-good';
|
||||
break;
|
||||
case 5:
|
||||
strengthText = '密码强度:强';
|
||||
strengthClass = 'strength-strong';
|
||||
break;
|
||||
}
|
||||
|
||||
return { strengthText, strengthClass };
|
||||
}
|
||||
|
||||
// Password strength indicator
|
||||
document.getElementById('password').addEventListener('input', function() {
|
||||
const password = this.value;
|
||||
const strengthFill = document.getElementById('strengthFill');
|
||||
const strengthText = document.getElementById('strengthText');
|
||||
|
||||
if (password.length > 0) {
|
||||
const { strengthText: text, strengthClass: className } = checkPasswordStrength(password);
|
||||
strengthFill.className = 'strength-fill ' + className;
|
||||
strengthText.textContent = text;
|
||||
} else {
|
||||
strengthFill.className = 'strength-fill';
|
||||
strengthText.textContent = '密码强度';
|
||||
}
|
||||
});
|
||||
|
||||
// Password confirmation check
|
||||
document.getElementById('confirmPassword').addEventListener('input', function() {
|
||||
const password = document.getElementById('password').value;
|
||||
const confirmPassword = this.value;
|
||||
const passwordMatch = document.getElementById('passwordMatch');
|
||||
|
||||
if (confirmPassword.length > 0) {
|
||||
if (password === confirmPassword) {
|
||||
this.classList.remove('is-invalid');
|
||||
this.classList.add('is-valid');
|
||||
passwordMatch.style.display = 'block';
|
||||
} else {
|
||||
this.classList.remove('is-valid');
|
||||
this.classList.add('is-invalid');
|
||||
passwordMatch.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
this.classList.remove('is-valid', 'is-invalid');
|
||||
passwordMatch.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Username uniqueness check
|
||||
async function checkUsernameUniqueness(username) {
|
||||
if (!username.trim()) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/public/users/exists/username?value=' + encodeURIComponent(username));
|
||||
const data = await response.json();
|
||||
|
||||
const usernameField = document.getElementById('username');
|
||||
if (data.exists) {
|
||||
usernameField.classList.add('is-invalid');
|
||||
usernameField.classList.remove('is-valid');
|
||||
} else {
|
||||
usernameField.classList.remove('is-invalid');
|
||||
usernameField.classList.add('is-valid');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Username check failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Email uniqueness check
|
||||
async function checkEmailUniqueness(email) {
|
||||
if (!email.trim()) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/public/users/exists/email?value=' + encodeURIComponent(email));
|
||||
const data = await response.json();
|
||||
|
||||
const emailField = document.getElementById('email');
|
||||
if (data.exists) {
|
||||
emailField.classList.add('is-invalid');
|
||||
emailField.classList.remove('is-valid');
|
||||
} else {
|
||||
emailField.classList.remove('is-invalid');
|
||||
emailField.classList.add('is-valid');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Email check failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Debounce function
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Add debounced event listeners
|
||||
const debouncedUsernameCheck = debounce(checkUsernameUniqueness, 500);
|
||||
const debouncedEmailCheck = debounce(checkEmailUniqueness, 500);
|
||||
|
||||
document.getElementById('username').addEventListener('blur', function() {
|
||||
debouncedUsernameCheck(this.value);
|
||||
});
|
||||
|
||||
document.getElementById('email').addEventListener('blur', function() {
|
||||
debouncedEmailCheck(this.value);
|
||||
});
|
||||
|
||||
// Form submission with loading state
|
||||
document.getElementById('registerForm').addEventListener('submit', function(e) {
|
||||
const registerBtn = document.getElementById('registerBtn');
|
||||
const loading = registerBtn.querySelector('.loading');
|
||||
const registerText = registerBtn.querySelector('.register-text');
|
||||
|
||||
// Show loading state
|
||||
loading.classList.add('show');
|
||||
registerText.style.display = 'none';
|
||||
registerBtn.disabled = true;
|
||||
});
|
||||
|
||||
// Form validation
|
||||
document.getElementById('registerForm').addEventListener('submit', function(e) {
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value.trim();
|
||||
const confirmPassword = document.getElementById('confirmPassword').value.trim();
|
||||
|
||||
if (!username || !email || !password || !confirmPassword) {
|
||||
e.preventDefault();
|
||||
alert('请填写所有必填字段');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
e.preventDefault();
|
||||
alert('两次输入的密码不一致');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
e.preventDefault();
|
||||
alert('密码长度至少6位');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
90
demo/src/main/resources/templates/settings/form.html
Normal file
90
demo/src/main/resources/templates/settings/form.html
Normal file
@@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>系统设置</title>
|
||||
</head>
|
||||
<body>
|
||||
<th:block th:replace="layout :: content">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 mx-auto">
|
||||
<div class="card">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<i class="fas fa-gear me-2"></i>系统设置
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form th:action="@{/settings}" th:object="${settings}" method="post">
|
||||
<h5 class="mb-3">基础信息</h5>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">站点名称</label>
|
||||
<input type="text" class="form-control" th:field="*{siteName}" maxlength="100" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">站点副标题</label>
|
||||
<input type="text" class="form-control" th:field="*{siteSubtitle}" maxlength="150" required>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" th:field="*{registrationOpen}">
|
||||
<label class="form-check-label">开放注册</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" th:field="*{maintenanceMode}">
|
||||
<label class="form-check-label">维护模式</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" th:field="*{enableAlipay}">
|
||||
<label class="form-check-label">启用支付宝</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" th:field="*{enablePaypal}">
|
||||
<label class="form-check-label">启用 PayPal</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label">联系邮箱</label>
|
||||
<input type="email" class="form-control" th:field="*{contactEmail}" maxlength="120">
|
||||
</div>
|
||||
|
||||
<h5 class="mb-3">套餐与扣点</h5>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">标准版价格(元)</label>
|
||||
<input type="number" class="form-control" th:field="*{standardPriceCny}" min="0" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">专业版价格(元)</label>
|
||||
<input type="number" class="form-control" th:field="*{proPriceCny}" min="0" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">每次生成消耗资源点</label>
|
||||
<input type="number" class="form-control" th:field="*{pointsPerGeneration}" min="0" required>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i>保存
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
343
demo/src/main/resources/templates/users/form.html
Normal file
343
demo/src/main/resources/templates/users/form.html
Normal file
@@ -0,0 +1,343 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::title}, ~{::content})}">
|
||||
<head>
|
||||
<title th:text="${user.id} != null ? '编辑用户' : '新增用户'">用户表单</title>
|
||||
</head>
|
||||
<body>
|
||||
<div th:fragment="content">
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a th:href="@{/}">首页</a></li>
|
||||
<li class="breadcrumb-item"><a th:href="@{/users}">用户管理</a></li>
|
||||
<li class="breadcrumb-item active" th:text="${user.id} != null ? '编辑用户' : '新增用户'">用户表单</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- User Form -->
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-user me-2"></i>
|
||||
<span th:text="${user.id} != null ? '编辑用户' : '新增用户'">用户表单</span>
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form th:action="${user.id} != null ? @{|/users/${user.id}|} : @{/users}"
|
||||
method="post" th:object="${user}" id="userForm">
|
||||
|
||||
<!-- Username -->
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">
|
||||
<i class="fas fa-user me-2"></i>用户名
|
||||
</label>
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
th:value="${user.username}" required minlength="3" maxlength="50"
|
||||
autocomplete="username" placeholder="请输入用户名">
|
||||
<div class="form-text">用户名长度3-50个字符,支持字母、数字、下划线</div>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">
|
||||
<i class="fas fa-envelope me-2"></i>邮箱地址
|
||||
</label>
|
||||
<input type="email" class="form-control" id="email" name="email"
|
||||
th:value="${user.email}" required autocomplete="email"
|
||||
placeholder="请输入邮箱地址">
|
||||
<div class="form-text">请输入有效的邮箱地址</div>
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">
|
||||
<i class="fas fa-lock me-2"></i>密码
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" id="password" name="password"
|
||||
minlength="6" maxlength="100" autocomplete="new-password"
|
||||
placeholder="请输入密码">
|
||||
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
<span th:if="${user.id} != null">留空则不修改密码</span>
|
||||
<span th:if="${user.id} == null">密码长度6-100个字符</span>
|
||||
</div>
|
||||
|
||||
<!-- Password Strength Indicator -->
|
||||
<div class="password-strength mt-2" th:if="${user.id} == null">
|
||||
<div class="strength-bar">
|
||||
<div class="strength-fill" id="strengthFill"></div>
|
||||
</div>
|
||||
<small class="text-muted" id="strengthText">密码强度</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Role -->
|
||||
<div class="mb-4">
|
||||
<label for="role" class="form-label">
|
||||
<i class="fas fa-shield-alt me-2"></i>用户角色
|
||||
</label>
|
||||
<select class="form-select" id="role" name="role" th:value="${user.role}">
|
||||
<option value="ROLE_USER">普通用户</option>
|
||||
<option value="ROLE_MEMBER">会员用户</option>
|
||||
<option value="ROLE_ADMIN">管理员</option>
|
||||
</select>
|
||||
<div class="form-text">选择用户的权限级别</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a th:href="@{/users}" class="btn btn-secondary me-md-2">
|
||||
<i class="fas fa-times me-2"></i>取消
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<span class="loading">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>保存中...
|
||||
</span>
|
||||
<span class="submit-text">
|
||||
<i class="fas fa-save me-2"></i>保存
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Info Card (for edit mode) -->
|
||||
<div class="card mt-4" th:if="${user.id} != null">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>用户信息
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">用户ID</small>
|
||||
<div th:text="${user.id}">1</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">创建时间</small>
|
||||
<div>2024-01-01 10:00:00</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.password-strength {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.strength-bar {
|
||||
height: 4px;
|
||||
background: #e9ecef;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.strength-fill {
|
||||
height: 100%;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.strength-weak { background: #dc3545; width: 25%; }
|
||||
.strength-fair { background: #ffc107; width: 50%; }
|
||||
.strength-good { background: #17a2b8; width: 75%; }
|
||||
.strength-strong { background: #28a745; width: 100%; }
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Password strength checker
|
||||
function checkPasswordStrength(password) {
|
||||
let strength = 0;
|
||||
let strengthText = '';
|
||||
let strengthClass = '';
|
||||
|
||||
if (password.length >= 6) strength++;
|
||||
if (password.match(/[a-z]/)) strength++;
|
||||
if (password.match(/[A-Z]/)) strength++;
|
||||
if (password.match(/[0-9]/)) strength++;
|
||||
if (password.match(/[^a-zA-Z0-9]/)) strength++;
|
||||
|
||||
switch (strength) {
|
||||
case 0:
|
||||
case 1:
|
||||
strengthText = '密码强度:弱';
|
||||
strengthClass = 'strength-weak';
|
||||
break;
|
||||
case 2:
|
||||
strengthText = '密码强度:一般';
|
||||
strengthClass = 'strength-fair';
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
strengthText = '密码强度:良好';
|
||||
strengthClass = 'strength-good';
|
||||
break;
|
||||
case 5:
|
||||
strengthText = '密码强度:强';
|
||||
strengthClass = 'strength-strong';
|
||||
break;
|
||||
}
|
||||
|
||||
return { strengthText, strengthClass };
|
||||
}
|
||||
|
||||
// Password strength indicator (only for new users)
|
||||
const passwordField = document.getElementById('password');
|
||||
const strengthFill = document.getElementById('strengthFill');
|
||||
const strengthText = document.getElementById('strengthText');
|
||||
|
||||
if (strengthFill && strengthText) {
|
||||
passwordField.addEventListener('input', function() {
|
||||
const password = this.value;
|
||||
|
||||
if (password.length > 0) {
|
||||
const { strengthText: text, strengthClass: className } = checkPasswordStrength(password);
|
||||
strengthFill.className = 'strength-fill ' + className;
|
||||
strengthText.textContent = text;
|
||||
} else {
|
||||
strengthFill.className = 'strength-fill';
|
||||
strengthText.textContent = '密码强度';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle password visibility
|
||||
document.getElementById('togglePassword').addEventListener('click', function() {
|
||||
const passwordField = document.getElementById('password');
|
||||
const icon = this.querySelector('i');
|
||||
|
||||
if (passwordField.type === 'password') {
|
||||
passwordField.type = 'text';
|
||||
icon.classList.remove('fa-eye');
|
||||
icon.classList.add('fa-eye-slash');
|
||||
} else {
|
||||
passwordField.type = 'password';
|
||||
icon.classList.remove('fa-eye-slash');
|
||||
icon.classList.add('fa-eye');
|
||||
}
|
||||
});
|
||||
|
||||
// Form submission with loading state
|
||||
document.getElementById('userForm').addEventListener('submit', function(e) {
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const loading = submitBtn.querySelector('.loading');
|
||||
const submitText = submitBtn.querySelector('.submit-text');
|
||||
|
||||
// Show loading state
|
||||
loading.classList.add('show');
|
||||
submitText.style.display = 'none';
|
||||
submitBtn.disabled = true;
|
||||
});
|
||||
|
||||
// Form validation
|
||||
document.getElementById('userForm').addEventListener('submit', function(e) {
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value.trim();
|
||||
const isEdit = document.querySelector('form').action.includes('/edit');
|
||||
|
||||
if (!username || !email) {
|
||||
e.preventDefault();
|
||||
alert('请填写用户名和邮箱');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isEdit && !password) {
|
||||
e.preventDefault();
|
||||
alert('新用户必须设置密码');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password && password.length < 6) {
|
||||
e.preventDefault();
|
||||
alert('密码长度至少6位');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
e.preventDefault();
|
||||
alert('请输入有效的邮箱地址');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-save draft (for new users)
|
||||
if (!document.querySelector('form').action.includes('/edit')) {
|
||||
const formData = {};
|
||||
|
||||
['username', 'email', 'password'].forEach(function(fieldName) {
|
||||
const field = document.getElementById(fieldName);
|
||||
if (field) {
|
||||
field.addEventListener('input', function() {
|
||||
formData[fieldName] = this.value;
|
||||
localStorage.setItem('userFormDraft', JSON.stringify(formData));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Restore draft on page load
|
||||
const savedDraft = localStorage.getItem('userFormDraft');
|
||||
if (savedDraft) {
|
||||
try {
|
||||
const draft = JSON.parse(savedDraft);
|
||||
Object.keys(draft).forEach(function(key) {
|
||||
const field = document.getElementById(key);
|
||||
if (field && draft[key]) {
|
||||
field.value = draft[key];
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to restore form draft:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear draft on successful submission
|
||||
document.getElementById('userForm').addEventListener('submit', function() {
|
||||
localStorage.removeItem('userFormDraft');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
262
demo/src/main/resources/templates/users/list.html
Normal file
262
demo/src/main/resources/templates/users/list.html
Normal file
@@ -0,0 +1,262 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::title}, ~{::content})}">
|
||||
<head>
|
||||
<title>用户管理</title>
|
||||
</head>
|
||||
<body>
|
||||
<div th:fragment="content">
|
||||
<!-- Page Actions -->
|
||||
<div th:fragment="page-actions">
|
||||
<a th:href="@{/users/new}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-2"></i>新增用户
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Users Table -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-users me-2"></i>用户列表
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="input-group" style="max-width: 300px;">
|
||||
<input type="text" class="form-control" placeholder="搜索用户..." id="searchInput">
|
||||
<button class="btn btn-outline-secondary" type="button">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0" id="usersTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="80">ID</th>
|
||||
<th>用户名</th>
|
||||
<th>邮箱</th>
|
||||
<th width="120">角色</th>
|
||||
<th width="120">状态</th>
|
||||
<th width="200">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="u : ${users}" class="user-row">
|
||||
<td th:text="${u.id}">1</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar-circle me-2">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<span th:text="${u.username}">user</span>
|
||||
</div>
|
||||
</td>
|
||||
<td th:text="${u.email}">email</td>
|
||||
<td>
|
||||
<span th:if="${u.role == 'ROLE_USER'}" class="badge bg-secondary">普通用户</span>
|
||||
<span th:if="${u.role == 'ROLE_MEMBER'}" class="badge bg-info">会员用户</span>
|
||||
<span th:if="${u.role == 'ROLE_ADMIN'}" class="badge bg-danger">管理员</span>
|
||||
<span th:if="${u.role != 'ROLE_USER' and u.role != 'ROLE_MEMBER' and u.role != 'ROLE_ADMIN'}"
|
||||
class="badge bg-warning" th:text="${u.role}">其他</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-success">正常</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a th:href="@{|/users/${u.id}/edit|}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-info"
|
||||
th:onclick="'showUserDetails(' + ${u.id} + ')'">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<form th:action="@{|/users/${u.id}/delete|}" method="post" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger"
|
||||
th:onclick="'return confirmDelete(\'' + ${u.username} + '\')'">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<small class="text-muted">
|
||||
共 <span th:text="${users.size()}">0</span> 个用户
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<nav aria-label="用户分页">
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">上一页</span>
|
||||
</li>
|
||||
<li class="page-item active">
|
||||
<span class="page-link">1</span>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">下一页</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Details Modal -->
|
||||
<div class="modal fade" id="userDetailsModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-user me-2"></i>用户详情
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="userDetailsContent">
|
||||
<!-- User details will be loaded here -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.avatar-circle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.user-row:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.btn-group .btn:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.table th {
|
||||
border-top: none;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Search functionality
|
||||
document.getElementById('searchInput').addEventListener('input', function() {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
const rows = document.querySelectorAll('.user-row');
|
||||
|
||||
rows.forEach(function(row) {
|
||||
const username = row.cells[1].textContent.toLowerCase();
|
||||
const email = row.cells[2].textContent.toLowerCase();
|
||||
|
||||
if (username.includes(searchTerm) || email.includes(searchTerm)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Show user details
|
||||
function showUserDetails(userId) {
|
||||
// In a real application, you would fetch user details via AJAX
|
||||
const modal = new bootstrap.Modal(document.getElementById('userDetailsModal'));
|
||||
const content = document.getElementById('userDetailsContent');
|
||||
|
||||
// For demo purposes, show static content
|
||||
content.innerHTML = `
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<div class="avatar-circle mx-auto mb-3" style="width: 80px; height: 80px; font-size: 2rem;">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<h6>用户ID: ${userId}</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td><strong>用户名:</strong></td>
|
||||
<td>demo_user</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>邮箱:</strong></td>
|
||||
<td>demo@example.com</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>角色:</strong></td>
|
||||
<td><span class="badge bg-secondary">普通用户</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>注册时间:</strong></td>
|
||||
<td>2024-01-01 10:00:00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>最后登录:</strong></td>
|
||||
<td>2024-01-15 14:30:00</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Confirm delete
|
||||
function confirmDelete(username) {
|
||||
return confirm(`确定要删除用户 "${username}" 吗?此操作不可撤销。`);
|
||||
}
|
||||
|
||||
// Auto-refresh table every 30 seconds
|
||||
setInterval(function() {
|
||||
// In a real application, you would refresh the table data
|
||||
console.log('Auto-refreshing user table...');
|
||||
}, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user