照片展示
This commit is contained in:
@@ -8,6 +8,7 @@ public class FlashSaleSystemApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(FlashSaleSystemApplication.class, args);
|
||||
System.out.println("http://localhost:8080");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.org.flashsalesystem.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.ViewResolver;
|
||||
@@ -16,6 +17,12 @@ import org.springframework.web.servlet.view.JstlView;
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Value("${flashsale.upload.path}")
|
||||
private String uploadPath;
|
||||
|
||||
@Value("${flashsale.upload.url-prefix}")
|
||||
private String urlPrefix;
|
||||
|
||||
/**
|
||||
* JSP视图解析器
|
||||
*/
|
||||
@@ -56,6 +63,10 @@ public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
registry.addResourceHandler("/favicon.ico")
|
||||
.addResourceLocations("classpath:/META-INF/resources/");
|
||||
|
||||
// 添加上传文件的静态资源映射
|
||||
registry.addResourceHandler(urlPrefix + "**")
|
||||
.addResourceLocations("file:" + uploadPath);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package com.org.flashsalesystem.controller;
|
||||
|
||||
import com.org.flashsalesystem.service.AdminService;
|
||||
import com.org.flashsalesystem.service.FileUploadService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -23,6 +25,9 @@ public class AdminController {
|
||||
@Autowired
|
||||
private AdminService adminService;
|
||||
|
||||
@Autowired
|
||||
private FileUploadService fileUploadService;
|
||||
|
||||
/**
|
||||
* 获取仪表盘统计数据
|
||||
*/
|
||||
@@ -449,4 +454,38 @@ public class AdminController {
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传商品图片
|
||||
*/
|
||||
@Operation(summary = "上传商品图片")
|
||||
@PostMapping("/products/upload-image")
|
||||
public ResponseEntity<Map<String, Object>> uploadProductImage(@RequestParam("file") MultipartFile file) {
|
||||
try {
|
||||
String imageUrl = fileUploadService.uploadProductImage(file);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "图片上传成功");
|
||||
response.put("imageUrl", imageUrl);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("图片上传失败 - 参数错误", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
} catch (Exception e) {
|
||||
log.error("图片上传失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "图片上传失败: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 文件上传服务类
|
||||
* 处理商品图片等文件的上传
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class FileUploadService {
|
||||
|
||||
private static final String IMAGE_DIR = "products/";
|
||||
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
// 允许的图片格式
|
||||
private static final String[] ALLOWED_EXTENSIONS = {
|
||||
"jpg", "jpeg", "png", "gif", "webp"
|
||||
};
|
||||
@Value("${flashsale.upload.path}")
|
||||
private String uploadPath;
|
||||
@Value("${flashsale.upload.url-prefix}")
|
||||
private String urlPrefix;
|
||||
|
||||
/**
|
||||
* 初始化上传目录
|
||||
*/
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
try {
|
||||
// 创建上传根目录
|
||||
Path rootPath = Paths.get(uploadPath);
|
||||
if (!Files.exists(rootPath)) {
|
||||
Files.createDirectories(rootPath);
|
||||
log.info("创建上传根目录: {}", uploadPath);
|
||||
}
|
||||
|
||||
// 创建商品图片目录
|
||||
Path productPath = Paths.get(uploadPath + IMAGE_DIR);
|
||||
if (!Files.exists(productPath)) {
|
||||
Files.createDirectories(productPath);
|
||||
log.info("创建商品图片目录: {}", productPath);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("初始化上传目录失败", e);
|
||||
throw new RuntimeException("初始化上传目录失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传商品图片
|
||||
*
|
||||
* @param file 上传的文件
|
||||
*
|
||||
* @return 图片访问URL
|
||||
*/
|
||||
public String uploadProductImage(MultipartFile file) {
|
||||
// 检查文件是否为空
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("上传文件不能为空");
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
if (file.getSize() > MAX_FILE_SIZE) {
|
||||
throw new IllegalArgumentException("文件大小不能超过10MB");
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
if (originalFilename == null || !originalFilename.contains(".")) {
|
||||
throw new IllegalArgumentException("文件名格式不正确");
|
||||
}
|
||||
|
||||
String extension = originalFilename.substring(originalFilename.lastIndexOf(".") + 1).toLowerCase();
|
||||
|
||||
// 检查文件格式
|
||||
if (!isAllowedExtension(extension)) {
|
||||
throw new IllegalArgumentException("不支持的图片格式,仅支持: jpg, jpeg, png, gif, webp");
|
||||
}
|
||||
|
||||
try {
|
||||
// 生成新的文件名
|
||||
String newFileName = generateFileName(extension);
|
||||
|
||||
// 按日期创建子目录
|
||||
String dateDir = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
|
||||
String relativePath = IMAGE_DIR + dateDir + "/" + newFileName;
|
||||
|
||||
// 创建目标目录
|
||||
Path targetDir = Paths.get(uploadPath + IMAGE_DIR + dateDir);
|
||||
if (!Files.exists(targetDir)) {
|
||||
Files.createDirectories(targetDir);
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
Path targetPath = Paths.get(uploadPath + relativePath);
|
||||
file.transferTo(targetPath.toFile());
|
||||
|
||||
// 返回访问URL
|
||||
String imageUrl = urlPrefix + relativePath;
|
||||
log.info("文件上传成功: {} -> {}", originalFilename, imageUrl);
|
||||
|
||||
return imageUrl;
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("文件上传失败", e);
|
||||
throw new RuntimeException("文件上传失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除商品图片
|
||||
*
|
||||
* @param imageUrl 图片URL
|
||||
*/
|
||||
public void deleteProductImage(String imageUrl) {
|
||||
if (imageUrl == null || imageUrl.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 只处理本地上传的图片
|
||||
if (!imageUrl.startsWith(urlPrefix)) {
|
||||
log.warn("非本地图片,跳过删除: {}", imageUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 从URL中提取相对路径
|
||||
String relativePath = imageUrl.substring(urlPrefix.length());
|
||||
Path filePath = Paths.get(uploadPath + relativePath);
|
||||
|
||||
if (Files.exists(filePath)) {
|
||||
Files.delete(filePath);
|
||||
log.info("删除图片成功: {}", imageUrl);
|
||||
} else {
|
||||
log.warn("图片文件不存在: {}", filePath);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("删除图片失败: {}", imageUrl, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一的文件名
|
||||
*
|
||||
* @param extension 文件扩展名
|
||||
*
|
||||
* @return 新文件名
|
||||
*/
|
||||
private String generateFileName(String extension) {
|
||||
return UUID.randomUUID().toString().replace("-", "") + "." + extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件扩展名是否允许
|
||||
*
|
||||
* @param extension 扩展名
|
||||
*
|
||||
* @return 是否允许
|
||||
*/
|
||||
private boolean isAllowedExtension(String extension) {
|
||||
for (String allowed : ALLOWED_EXTENSIONS) {
|
||||
if (allowed.equalsIgnoreCase(extension)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件的MIME类型
|
||||
*
|
||||
* @param extension 文件扩展名
|
||||
*
|
||||
* @return MIME类型
|
||||
*/
|
||||
public String getMimeType(String extension) {
|
||||
switch (extension.toLowerCase()) {
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
return "image/jpeg";
|
||||
case "png":
|
||||
return "image/png";
|
||||
case "gif":
|
||||
return "image/gif";
|
||||
case "webp":
|
||||
return "image/webp";
|
||||
default:
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,14 @@ spring:
|
||||
prefix: /WEB-INF/views/
|
||||
suffix: .jsp
|
||||
|
||||
# 文件上传配置
|
||||
servlet:
|
||||
multipart:
|
||||
enabled: true
|
||||
max-file-size: 10MB
|
||||
max-request-size: 10MB
|
||||
location: ${java.io.tmpdir}
|
||||
|
||||
# JSON配置
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
@@ -85,6 +93,13 @@ logging:
|
||||
|
||||
# 自定义配置
|
||||
flashsale:
|
||||
# 文件上传配置
|
||||
upload:
|
||||
# 文件上传路径
|
||||
path: ${user.home}/flashsale-uploads/
|
||||
# 访问URL前缀
|
||||
url-prefix: /uploads/
|
||||
|
||||
# 秒杀配置
|
||||
seckill:
|
||||
# 每个用户每个商品最大购买数量
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>图片</th>
|
||||
<th>活动名称</th>
|
||||
<th>商品</th>
|
||||
<th>原价/秒杀价</th>
|
||||
@@ -109,7 +110,7 @@
|
||||
</thead>
|
||||
<tbody id="flashSalesTableBody">
|
||||
<tr>
|
||||
<td colspan="9" class="text-center">
|
||||
<td colspan="10" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
@@ -575,7 +576,7 @@
|
||||
currentPage = page;
|
||||
|
||||
// 显示加载状态
|
||||
$('#flashSalesTableBody').html('<tr><td colspan="9" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
$('#flashSalesTableBody').html('<tr><td colspan="10" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
|
||||
// 构建查询参数
|
||||
const queryData = {
|
||||
@@ -616,12 +617,12 @@
|
||||
renderFlashSalesTable(response.data.content || response.data.flashSales || []);
|
||||
renderPagination(response.data.totalElements || response.data.total || 0, pageSize);
|
||||
} else {
|
||||
$('#flashSalesTableBody').html('<tr><td colspan="9" class="text-center text-danger">获取秒杀数据失败: ' + response.message + '</td></tr>');
|
||||
$('#flashSalesTableBody').html('<tr><td colspan="10" class="text-center text-danger">获取秒杀数据失败: ' + response.message + '</td></tr>');
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error('获取秒杀列表失败:', error);
|
||||
$('#flashSalesTableBody').html('<tr><td colspan="9" class="text-center text-danger">网络请求失败,请稍后重试</td></tr>');
|
||||
$('#flashSalesTableBody').html('<tr><td colspan="10" class="text-center text-danger">网络请求失败,请稍后重试</td></tr>');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -630,7 +631,7 @@
|
||||
let html = '';
|
||||
|
||||
if (flashSales.length === 0) {
|
||||
html = '<tr><td colspan="9" class="text-center">暂无秒杀活动</td></tr>';
|
||||
html = '<tr><td colspan="10" class="text-center">暂无秒杀活动</td></tr>';
|
||||
} else {
|
||||
flashSales.forEach(flashSale => {
|
||||
const statusText = getStatusText(flashSale.status);
|
||||
@@ -639,6 +640,12 @@
|
||||
html += `
|
||||
<tr>
|
||||
<td>` + flashSale.id + `</td>
|
||||
<td>
|
||||
<img src="` + getProductImageUrl(flashSale.productImageUrl) + `"
|
||||
class="img-thumbnail" alt="` + (flashSale.productName || '商品') + `"
|
||||
style="width: 50px; height: 50px; object-fit: cover;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">` + (flashSale.productName || '秒杀活动') + `</div>
|
||||
<small class="text-muted">` + (flashSale.statusDescription || '') + `</small>
|
||||
@@ -739,7 +746,7 @@
|
||||
}
|
||||
|
||||
function refreshFlashSales() {
|
||||
$('#flashSalesTableBody').html('<tr><td colspan="9" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
$('#flashSalesTableBody').html('<tr><td colspan="10" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
loadFlashSales(currentPage);
|
||||
}
|
||||
|
||||
@@ -1173,6 +1180,32 @@
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取商品图片URL
|
||||
function getProductImageUrl(imageUrl) {
|
||||
// 如果没有图片URL或为空,返回默认图片
|
||||
if (!imageUrl || imageUrl.trim() === '') {
|
||||
return '${pageContext.request.contextPath}/images/default-product.svg';
|
||||
}
|
||||
|
||||
// 如果是相对路径,添加上下文路径
|
||||
if (imageUrl.startsWith('/images/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是上传的图片(以/uploads/开头)
|
||||
if (imageUrl.startsWith('/uploads/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是完整的URL(http或https),直接返回
|
||||
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// 其他情况,当作相对路径处理
|
||||
return '${pageContext.request.contextPath}/images/' + imageUrl;
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="../common/footer.jsp" %>
|
||||
|
||||
@@ -216,9 +216,18 @@
|
||||
<textarea class="form-control" id="productDescription" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="productImage" class="form-label">商品图片URL</label>
|
||||
<input type="url" class="form-control" id="productImage"
|
||||
placeholder="https://example.com/image.jpg">
|
||||
<label for="productImage" class="form-label">商品图片</label>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<input type="file" class="form-control" id="productImageFile" accept="image/*"
|
||||
onchange="previewImage(this, 'addProductImagePreview')">
|
||||
<input type="hidden" id="productImage" name="productImage">
|
||||
<small class="text-muted">支持格式: JPG, JPEG, PNG, GIF, WEBP (最大10MB)</small>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<img id="addProductImagePreview" class="image-preview d-none" alt="预览图片">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -281,8 +290,22 @@
|
||||
<textarea class="form-control" id="editProductDescription" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editProductImage" class="form-label">商品图片URL</label>
|
||||
<input type="url" class="form-control" id="editProductImage">
|
||||
<label for="editProductImage" class="form-label">商品图片</label>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<input type="file" class="form-control" id="editProductImageFile" accept="image/*"
|
||||
onchange="previewImage(this, 'editProductImagePreview')">
|
||||
<input type="hidden" id="editProductImage" name="editProductImage">
|
||||
<small class="text-muted">支持格式: JPG, JPEG, PNG, GIF, WEBP (最大10MB)</small>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<img id="editProductImagePreview" class="image-preview d-none" alt="预览图片">
|
||||
<div id="currentImageContainer" class="mt-2 d-none">
|
||||
<small class="text-muted">当前图片:</small><br>
|
||||
<img id="currentProductImage" class="image-preview" alt="当前图片">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -631,25 +654,63 @@
|
||||
loadProducts(1);
|
||||
}
|
||||
|
||||
function saveProduct() {
|
||||
const productData = {
|
||||
name: $('#productName').val(),
|
||||
price: $('#productPrice').val(),
|
||||
stock: $('#productStock').val(),
|
||||
status: $('#productStatus').val(),
|
||||
description: $('#productDescription').val(),
|
||||
imageUrl: $('#productImage').val()
|
||||
};
|
||||
async function saveProduct() {
|
||||
try {
|
||||
// 显示加载状态
|
||||
const saveBtn = $('#addProductModal .btn-primary');
|
||||
const originalText = saveBtn.text();
|
||||
saveBtn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 保存中...');
|
||||
|
||||
console.log('保存商品:', productData);
|
||||
// 检查是否有选择图片
|
||||
const imageFile = $('#productImageFile')[0].files[0];
|
||||
let imageUrl = '';
|
||||
|
||||
// 模拟API调用
|
||||
setTimeout(function () {
|
||||
$('#addProductModal').modal('hide');
|
||||
$('#addProductForm')[0].reset();
|
||||
alert('商品添加成功!');
|
||||
refreshProducts();
|
||||
}, 1000);
|
||||
if (imageFile) {
|
||||
try {
|
||||
imageUrl = await uploadImage(imageFile);
|
||||
} catch (error) {
|
||||
alert('图片上传失败: ' + error);
|
||||
saveBtn.prop('disabled', false).text(originalText);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const productData = {
|
||||
name: $('#productName').val(),
|
||||
price: parseFloat($('#productPrice').val()),
|
||||
stock: parseInt($('#productStock').val()),
|
||||
status: parseInt($('#productStatus').val()),
|
||||
description: $('#productDescription').val(),
|
||||
imageUrl: imageUrl
|
||||
};
|
||||
|
||||
// 调用API保存商品
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/admin/products',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(productData),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
$('#addProductModal').modal('hide');
|
||||
$('#addProductForm')[0].reset();
|
||||
$('#addProductImagePreview').addClass('d-none');
|
||||
showAlert('success', '商品添加成功!');
|
||||
loadProducts(currentPage);
|
||||
} else {
|
||||
alert('保存失败: ' + response.message);
|
||||
}
|
||||
saveBtn.prop('disabled', false).text(originalText);
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
alert('网络错误: ' + error);
|
||||
saveBtn.prop('disabled', false).text(originalText);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('保存商品失败:', error);
|
||||
alert('保存失败: ' + error);
|
||||
}
|
||||
}
|
||||
|
||||
function editProduct(id) {
|
||||
@@ -708,8 +769,21 @@
|
||||
<textarea class="form-control" id="editProductDescription" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editProductImage" class="form-label">商品图片URL</label>
|
||||
<input type="url" class="form-control" id="editProductImage">
|
||||
<label for="editProductImage" class="form-label">商品图片</label>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<input type="file" class="form-control" id="editProductImageFile" accept="image/*" onchange="previewImage(this, 'editProductImagePreview')">
|
||||
<input type="hidden" id="editProductImage" name="editProductImage">
|
||||
<small class="text-muted">支持格式: JPG, JPEG, PNG, GIF, WEBP (最大10MB)</small>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<img id="editProductImagePreview" class="image-preview d-none" alt="预览图片">
|
||||
<div id="currentImageContainer" class="mt-2 d-none">
|
||||
<small class="text-muted">当前图片:</small><br>
|
||||
<img id="currentProductImage" class="image-preview" alt="当前图片">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
`);
|
||||
@@ -722,6 +796,13 @@
|
||||
$('#editProductStatus').val(product.status);
|
||||
$('#editProductDescription').val(product.description || '');
|
||||
$('#editProductImage').val(product.imageUrl || '');
|
||||
|
||||
// 显示当前图片
|
||||
if (product.imageUrl) {
|
||||
const imageUrl = getProductImageUrl(product.imageUrl);
|
||||
$('#currentProductImage').attr('src', imageUrl);
|
||||
$('#currentImageContainer').removeClass('d-none');
|
||||
}
|
||||
} else {
|
||||
$('#editProductModal .modal-body').html('<div class="alert alert-danger">获取商品信息失败: ' + response.message + '</div>');
|
||||
}
|
||||
@@ -731,47 +812,63 @@
|
||||
});
|
||||
}
|
||||
|
||||
function updateProduct() {
|
||||
const productId = $('#editProductId').val();
|
||||
const productData = {
|
||||
name: $('#editProductName').val(),
|
||||
price: parseFloat($('#editProductPrice').val()),
|
||||
stock: parseInt($('#editProductStock').val()),
|
||||
status: parseInt($('#editProductStatus').val()),
|
||||
description: $('#editProductDescription').val(),
|
||||
imageUrl: $('#editProductImage').val()
|
||||
};
|
||||
async function updateProduct() {
|
||||
try {
|
||||
const productId = $('#editProductId').val();
|
||||
|
||||
console.log('更新商品:', productData);
|
||||
// 显示加载状态
|
||||
const updateBtn = $('#editProductModal .btn-primary');
|
||||
const originalText = updateBtn.text();
|
||||
updateBtn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 更新中...');
|
||||
|
||||
// 显示加载状态
|
||||
const updateBtn = $('#editProductModal .btn-primary');
|
||||
const originalText = updateBtn.text();
|
||||
updateBtn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 更新中...');
|
||||
// 检查是否有新选择的图片
|
||||
const imageFile = $('#editProductImageFile')[0].files[0];
|
||||
let imageUrl = $('#editProductImage').val(); // 保持原有图片URL
|
||||
|
||||
// 调用真实API
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/admin/products/' + productId,
|
||||
type: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(productData),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
$('#editProductModal').modal('hide');
|
||||
showAlert('success', '商品更新成功!');
|
||||
// 只更新当前页面,不刷新整个列表
|
||||
loadProducts(currentPage);
|
||||
} else {
|
||||
showAlert('danger', '更新失败: ' + response.message);
|
||||
if (imageFile) {
|
||||
try {
|
||||
imageUrl = await uploadImage(imageFile);
|
||||
} catch (error) {
|
||||
alert('图片上传失败: ' + error);
|
||||
updateBtn.prop('disabled', false).text(originalText);
|
||||
return;
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showAlert('danger', '网络请求失败,请稍后重试');
|
||||
},
|
||||
complete: function () {
|
||||
updateBtn.prop('disabled', false).text(originalText);
|
||||
}
|
||||
});
|
||||
|
||||
const productData = {
|
||||
name: $('#editProductName').val(),
|
||||
price: parseFloat($('#editProductPrice').val()),
|
||||
stock: parseInt($('#editProductStock').val()),
|
||||
status: parseInt($('#editProductStatus').val()),
|
||||
description: $('#editProductDescription').val(),
|
||||
imageUrl: imageUrl
|
||||
};
|
||||
|
||||
// 调用真实API
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/admin/products/' + productId,
|
||||
type: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(productData),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
$('#editProductModal').modal('hide');
|
||||
showAlert('success', '商品更新成功!');
|
||||
loadProducts(currentPage);
|
||||
} else {
|
||||
alert('更新失败: ' + response.message);
|
||||
}
|
||||
updateBtn.prop('disabled', false).text(originalText);
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
alert('网络错误: ' + error);
|
||||
updateBtn.prop('disabled', false).text(originalText);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新商品失败:', error);
|
||||
alert('更新失败: ' + error);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteProduct(id) {
|
||||
@@ -1171,6 +1268,11 @@
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是上传的图片(以/uploads/开头)
|
||||
if (imageUrl.startsWith('/uploads/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是完整的URL(http或https),直接返回
|
||||
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||
return imageUrl;
|
||||
@@ -1179,6 +1281,61 @@
|
||||
// 其他情况,当作相对路径处理
|
||||
return '${pageContext.request.contextPath}/images/' + imageUrl;
|
||||
}
|
||||
|
||||
// 图片预览功能
|
||||
function previewImage(input, previewId) {
|
||||
if (input.files && input.files[0]) {
|
||||
const file = input.files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
// 检查文件大小
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
alert('图片大小不能超过10MB');
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
alert('不支持的图片格式,仅支持: JPG, JPEG, PNG, GIF, WEBP');
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
reader.onload = function (e) {
|
||||
$('#' + previewId).attr('src', e.target.result).removeClass('d-none');
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
// 上传图片到服务器
|
||||
function uploadImage(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/admin/products/upload-image',
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
resolve(response.imageUrl);
|
||||
} else {
|
||||
reject(response.message || '图片上传失败');
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
reject('网络错误:' + error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="../common/footer.jsp" %>
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<img src="` + (item.productImageUrl || '${pageContext.request.contextPath}/images/default-product.svg') + `"
|
||||
<img src="` + getProductImageUrl(item.productImageUrl) + `"
|
||||
class="img-fluid rounded" alt="` + item.productName + `" style="max-height: 80px;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
</div>
|
||||
@@ -499,7 +499,7 @@
|
||||
html += `
|
||||
<div class="col-lg-3 col-md-6 mb-3">
|
||||
<div class="card h-100">
|
||||
<img src="` + (product.imageUrl || '${pageContext.request.contextPath}/images/default-product.svg') + `"
|
||||
<img src="` + getProductImageUrl(product.imageUrl) + `"
|
||||
class="card-img-top" alt="` + product.name + `" style="height: 200px; object-fit: cover;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
<div class="card-body">
|
||||
@@ -593,6 +593,32 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取商品图片URL
|
||||
function getProductImageUrl(imageUrl) {
|
||||
// 如果没有图片URL或为空,返回默认图片
|
||||
if (!imageUrl || imageUrl.trim() === '') {
|
||||
return '${pageContext.request.contextPath}/images/default-product.svg';
|
||||
}
|
||||
|
||||
// 如果是相对路径,添加上下文路径
|
||||
if (imageUrl.startsWith('/images/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是上传的图片(以/uploads/开头)
|
||||
if (imageUrl.startsWith('/uploads/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是完整的URL(http或https),直接返回
|
||||
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// 其他情况,当作相对路径处理
|
||||
return '${pageContext.request.contextPath}/images/' + imageUrl;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -244,7 +244,7 @@
|
||||
<div class="col-lg-4 col-md-6 mb-4">
|
||||
<div class="card h-100 flashsale-card" data-flashsale-id="` + flashSale.id + `">
|
||||
<div class="position-relative">
|
||||
<img src="` + (flashSale.productImageUrl || '${pageContext.request.contextPath}/images/default-product.svg') + `"
|
||||
<img src="` + getProductImageUrl(flashSale.productImageUrl) + `"
|
||||
class="card-img-top" alt="` + flashSale.productName + `" style="height: 220px; object-fit: cover;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
|
||||
@@ -744,6 +744,32 @@
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 获取商品图片URL
|
||||
function getProductImageUrl(imageUrl) {
|
||||
// 如果没有图片URL或为空,返回默认图片
|
||||
if (!imageUrl || imageUrl.trim() === '') {
|
||||
return '${pageContext.request.contextPath}/images/default-product.svg';
|
||||
}
|
||||
|
||||
// 如果是相对路径,添加上下文路径
|
||||
if (imageUrl.startsWith('/images/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是上传的图片(以/uploads/开头)
|
||||
if (imageUrl.startsWith('/uploads/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是完整的URL(http或https),直接返回
|
||||
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// 其他情况,当作相对路径处理
|
||||
return '${pageContext.request.contextPath}/images/' + imageUrl;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -223,12 +223,13 @@ function renderFlashSales(flashSales) {
|
||||
|
||||
flashSales.forEach(function(flashSale) {
|
||||
const discountPercent = Math.round((1 - flashSale.flashPrice / flashSale.originalPrice) * 100);
|
||||
const imageUrl = getProductImageUrl(flashSale.productImageUrl);
|
||||
|
||||
html += `
|
||||
<div class="col-lg-3 col-md-6 mb-4">
|
||||
<div class="card h-100 border-danger">
|
||||
<div class="position-relative">
|
||||
<img src="` + (flashSale.productImageUrl || '${pageContext.request.contextPath}/images/default-product.svg') + `"
|
||||
<img src="` + imageUrl + `"
|
||||
class="card-img-top" alt="` + flashSale.productName + `" style="height: 200px; object-fit: cover;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
<div class="position-absolute top-0 start-0 bg-danger text-white px-2 py-1 rounded-end">
|
||||
@@ -308,10 +309,11 @@ function renderHotProducts(products) {
|
||||
let html = '';
|
||||
|
||||
products.forEach(function(product) {
|
||||
const imageUrl = getProductImageUrl(product.imageUrl);
|
||||
html += `
|
||||
<div class="col-lg-3 col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<img src="` + (product.imageUrl || '${pageContext.request.contextPath}/images/default-product.svg') + `"
|
||||
<img src="` + imageUrl + `"
|
||||
class="card-img-top" alt="` + product.name + `" style="height: 200px; object-fit: cover;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
<div class="card-body">
|
||||
@@ -335,6 +337,32 @@ function renderHotProducts(products) {
|
||||
$('#hotProducts').html(html);
|
||||
}
|
||||
|
||||
// 获取商品图片URL
|
||||
function getProductImageUrl(imageUrl) {
|
||||
// 如果没有图片URL或为空,返回默认图片
|
||||
if (!imageUrl || imageUrl.trim() === '') {
|
||||
return '${pageContext.request.contextPath}/images/default-product.svg';
|
||||
}
|
||||
|
||||
// 如果是相对路径,添加上下文路径
|
||||
if (imageUrl.startsWith('/images/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是上传的图片(以/uploads/开头)
|
||||
if (imageUrl.startsWith('/uploads/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是完整的URL(http或https),直接返回
|
||||
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// 其他情况,当作相对路径处理
|
||||
return '${pageContext.request.contextPath}/images/' + imageUrl;
|
||||
}
|
||||
|
||||
// 参与秒杀(首页版)
|
||||
function participateFlashSale(flashSaleId) {
|
||||
<c:choose>
|
||||
|
||||
@@ -212,6 +212,11 @@
|
||||
html += `
|
||||
<div class="order-item border rounded mb-3 p-3">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-1">
|
||||
<img src="` + getProductImageUrl(order.productImageUrl) + `"
|
||||
class="img-fluid rounded" alt="` + (order.productName || '商品') + `" style="max-height: 60px;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div>
|
||||
<small class="text-muted">订单号</small>
|
||||
@@ -236,7 +241,7 @@
|
||||
<div class="fw-bold text-danger">¥` + (order.totalPrice ? order.totalPrice.toFixed(2) : '0.00') + `</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-1">
|
||||
<div>
|
||||
<small class="text-muted">状态</small>
|
||||
<div>
|
||||
@@ -469,12 +474,21 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>商品信息</h6>
|
||||
<table class="table table-sm">
|
||||
<tr><td>商品名称:</td><td>` + (order.productName || '商品信息') + `</td></tr>
|
||||
<tr><td>购买数量:</td><td>` + order.quantity + ` 件</td></tr>
|
||||
<tr><td>商品单价:</td><td>¥` + (order.totalPrice / order.quantity).toFixed(2) + `</td></tr>
|
||||
<tr><td>订单总价:</td><td class="text-danger fw-bold">¥` + (order.totalPrice ? order.totalPrice.toFixed(2) : '0.00') + `</td></tr>
|
||||
</table>
|
||||
<div class="row mb-3">
|
||||
<div class="col-4">
|
||||
<img src="` + getProductImageUrl(order.productImageUrl) + `"
|
||||
class="img-fluid rounded" alt="` + (order.productName || '商品') + `"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<table class="table table-sm">
|
||||
<tr><td>商品名称:</td><td>` + (order.productName || '商品信息') + `</td></tr>
|
||||
<tr><td>购买数量:</td><td>` + order.quantity + ` 件</td></tr>
|
||||
<tr><td>商品单价:</td><td>¥` + (order.totalPrice / order.quantity).toFixed(2) + `</td></tr>
|
||||
<tr><td>订单总价:</td><td class="text-danger fw-bold">¥` + (order.totalPrice ? order.totalPrice.toFixed(2) : '0.00') + `</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -721,6 +735,32 @@
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 获取商品图片URL
|
||||
function getProductImageUrl(imageUrl) {
|
||||
// 如果没有图片URL或为空,返回默认图片
|
||||
if (!imageUrl || imageUrl.trim() === '') {
|
||||
return '${pageContext.request.contextPath}/images/default-product.svg';
|
||||
}
|
||||
|
||||
// 如果是相对路径,添加上下文路径
|
||||
if (imageUrl.startsWith('/images/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是上传的图片(以/uploads/开头)
|
||||
if (imageUrl.startsWith('/uploads/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是完整的URL(http或https),直接返回
|
||||
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// 其他情况,当作相对路径处理
|
||||
return '${pageContext.request.contextPath}/images/' + imageUrl;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user