Initial commit: AIGC项目完整代码

This commit is contained in:
AIGC Developer
2025-10-21 16:50:33 +08:00
commit 47c8e02ab0
137 changed files with 30676 additions and 0 deletions

View 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

View 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

View File

@@ -0,0 +1,4 @@
spring.application.name=demo
spring.messages.basename=messages
spring.thymeleaf.cache=false
spring.profiles.active=dev

View 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;

View 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=邮箱格式不正确

View 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

View 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;

View 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
);

View 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>

View 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>

View 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>

View 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>

View 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>

View 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">&nbsp;</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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>