照片展示

This commit is contained in:
2025-07-30 10:09:35 +08:00
parent c02e3421ad
commit 923e877759
11 changed files with 657 additions and 78 deletions

View File

@@ -8,6 +8,7 @@ public class FlashSaleSystemApplication {
public static void main(String[] args) {
SpringApplication.run(FlashSaleSystemApplication.class, args);
System.out.println("http://localhost:8080");
}
}

View File

@@ -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);
}
/**

View File

@@ -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);
}
}
}

View File

@@ -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";
}
}
}

View File

@@ -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:
# 每个用户每个商品最大购买数量

View File

@@ -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;
}
// 如果是完整的URLhttp或https直接返回
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
return imageUrl;
}
// 其他情况,当作相对路径处理
return '${pageContext.request.contextPath}/images/' + imageUrl;
}
</script>
<%@ include file="../common/footer.jsp" %>

View File

@@ -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;
}
// 如果是完整的URLhttp或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" %>

View File

@@ -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;
}
// 如果是完整的URLhttp或https直接返回
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
return imageUrl;
}
// 其他情况,当作相对路径处理
return '${pageContext.request.contextPath}/images/' + imageUrl;
}
</script>
<style>

View File

@@ -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;
}
// 如果是完整的URLhttp或https直接返回
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
return imageUrl;
}
// 其他情况,当作相对路径处理
return '${pageContext.request.contextPath}/images/' + imageUrl;
}
</script>
<style>

View File

@@ -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;
}
// 如果是完整的URLhttp或https直接返回
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
return imageUrl;
}
// 其他情况,当作相对路径处理
return '${pageContext.request.contextPath}/images/' + imageUrl;
}
// 参与秒杀(首页版)
function participateFlashSale(flashSaleId) {
<c:choose>

View File

@@ -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;
}
// 如果是完整的URLhttp或https直接返回
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
return imageUrl;
}
// 其他情况,当作相对路径处理
return '${pageContext.request.contextPath}/images/' + imageUrl;
}
</script>
<style>