
前端页面--新增:
<!-- 文件上传区域 -->
<div class="form-group">
<label class="col-sm-3 control-label is-required">照片文件:</label>
<div class="col-sm-8">
<input type="hidden" name="photoPath" id="photoPathHidden">
<!-- 使用 label 原生触发,覆盖整个区域 -->
<div id="drop-area" style="border: 2px dashed #ccc; padding: 20px; text-align: center; margin-bottom: 10px; cursor: pointer;">
<label for="fileElem" style="width:100%; height:100%; display:block;">
<p>拖拽图片到这里,或点击选择文件</p>
</label>
<input type="file" id="fileElem" accept="image/*" style="display:none;" />
</div>
<div id="preview" style="margin-top: 10px;"></div>
<button type="button" class="btn btn-primary btn-sm" id="uploadBtn" disabled>上传图片</button>
<span id="uploadStatus" style="margin-left: 10px; color: green;"></span>
<button type="button" class="btn btn-danger btn-sm" id="deleteBtn" style="display:none; margin-left:10px;">删除图片</button>
</div>
</div>
<!-- 提交按钮 -->
<div class="form-group">
<div class="col-sm-offset-3 col-sm-8">
<button type="button" class="btn btn-success" onclick="submitHandler()">确定</button>
</div>
</div>
<script th:inline="javascript">
var prefix = ctx + "system/record";
// 初始化日期控件
$("input[name='photoDate']").datetimepicker({
format: "yyyy-mm-dd",
minView: "month",
autoclose: true
});
// 表单验证 & 提交
$("#form-record-add").validate({
focusCleanup: true
});
function submitHandler() {
if (!$('#photoPathHidden').val()) {
alert('请先上传照片!');
return;
}
if ($.validate.form()) {
console.log('准备提交到:', prefix + "/add");
$.operate.save(prefix + "/add", $('#form-record-add').serialize());
}
}
// ========== 文件上传逻辑 ==========
const dropArea = document.getElementById('drop-area');
const fileElem = document.getElementById('fileElem');
const preview = document.getElementById('preview');
const uploadBtn = document.getElementById('uploadBtn');
const uploadStatus = document.getElementById('uploadStatus');
const deleteBtn = document.getElementById('deleteBtn');
let selectedFile = null;
// 文件选择变化
fileElem.addEventListener('change', handleFiles);
// 拖拽事件
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, unhighlight, false);
});
function highlight() {
dropArea.style.borderColor = '#007bff';
}
function unhighlight() {
dropArea.style.borderColor = '#ccc';
}
dropArea.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
// 手动构造 event 对象传给 handleFiles
handleFiles({ target: { files: files } });
}
function handleFiles(e) {
const files = e.target.files;
if (files.length === 0) return;
selectedFile = files[0];
// 验证类型
if (!selectedFile.type.match('image.*')) {
alert('请选择图片文件(jpg/png/jpeg)');
selectedFile = null;
uploadBtn.disabled = true;
// 清空 input,以便下次可重新选择(包括同名文件)
fileElem.value = '';
return;
}
// 预览缩略图
preview.innerHTML = '';
const img = document.createElement('img');
img.src = URL.createObjectURL(selectedFile);
img.style.maxWidth = '200px';
img.style.maxHeight = '200px';
preview.appendChild(img);
uploadBtn.disabled = false;
uploadStatus.textContent = '';
// 因为可能用户想重新上传同一文件(但通常不会),所以暂不在此清空
}
// 上传按钮点击
uploadBtn.addEventListener('click', () => {
if (!selectedFile) return;
const formData = new FormData();
formData.append("file", selectedFile);
// ✅ 关键修复:使用 ctx 上下文路径,而非硬编码 /
fetch(ctx + 'common/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.code === 0) {
$('#photoPathHidden').val(data.fileName);
uploadStatus.textContent = '✅ 上传成功';
uploadBtn.disabled = true;
deleteBtn.style.display = 'inline-block';
// 上传成功后清空 file input,避免后续干扰
fileElem.value = '';
} else {
alert('上传失败:' + (data.msg || '未知错误'));
uploadStatus.textContent = '❌ 上传失败';
// 失败时也清空,允许重试
fileElem.value = '';
}
})
.catch(err => {
console.error('上传出错:', err);
alert('网络错误,请重试');
uploadStatus.textContent = '❌ 网络错误';
fileElem.value = ''; // 允许重试
});
});
// 删除按钮点击事件
deleteBtn.addEventListener('click', function() {
if (!confirm('确定要删除这张照片吗?')) return;
const photoPath = $('#photoPathHidden').val();
if (photoPath) {
fetch(ctx + 'common/deleteFile?fileName=' + encodeURIComponent(photoPath), {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.code !== 0) {
alert('服务器删除失败:' + (data.msg || ''));
}
clearPhotoPreview();
})
.catch(err => {
console.error('删除请求失败:', err);
alert('网络错误,但本地预览已清除');
clearPhotoPreview();
});
} else {
clearPhotoPreview();
}
});
// 清空预览和状态的函数
function clearPhotoPreview() {
selectedFile = null;
$('#photoPathHidden').val('');
preview.innerHTML = '';
uploadStatus.textContent = '';
uploadBtn.disabled = true;
deleteBtn.style.display = 'none';
// 同时清空文件输入框
fileElem.value = '';
}
</script>
修改com.ruoyi.project.common.CommonController的/deleteFile方法:
/**
* 删除文件
* @param fileName
* @return
*/
@PostMapping("/deleteFile")
@ResponseBody
public AjaxResult deleteFile(String fileName) {
try {
if (StringUtils.isBlank(fileName)) {
return error("文件名为空");
}
if (fileName.contains("..") || !fileName.startsWith("/profile/")) {
return error("非法文件路径");
}
String profilePath = RuoYiConfig.getProfile();
String relativePath = fileName.substring("/profile/".length());
// ✅ 使用 Paths.get 自动处理路径分隔符
String realPath = Paths.get(profilePath, relativePath).toString();
File file = new File(realPath);
if (file.exists()) {
if (file.delete()) {
return success();
} else {
return error("文件删除失败,可能被占用或无权限");
}
}
return success(); // 文件不存在也视为成功(幂等)
} catch (Exception e) {
log.error("删除文件异常", e);
return error("系统异常:" + e.getMessage());
}
}
前端页面---编辑:
<!-- 文件上传区域(复用新增页逻辑) -->
<div class="form-group">
<label class="col-sm-3 control-label is-required">照片文件:</label>
<div class="col-sm-8">
<!-- 隐藏域:存储服务器路径 -->
<input type="hidden" name="photoPath" id="photoPathHidden" th:value="*{photoPath}">
<!-- 拖拽区域 -->
<div id="drop-area" style="border: 2px dashed #ccc; padding: 20px; text-align: center; margin-bottom: 10px; cursor: pointer;">
<label for="fileElem" style="width: 100%; height: 100%; display: block;">
<p>拖拽图片到这里,或点击选择文件</p>
</label>
<input type="file" id="fileElem" accept="image/*" style="display:none;" />
</div>
<!-- 缩略图预览 -->
<div id="preview" style="margin-top: 10px;"></div>
<!-- 操作按钮 -->
<button type="button" class="btn btn-primary btn-sm" id="uploadBtn" disabled>上传新图片</button>
<span id="uploadStatus" style="margin-left: 10px; color: green;"></span>
<button type="button" class="btn btn-danger btn-sm" id="deleteBtn" style="display:none; margin-left:10px;">删除当前图片</button>
</div>
</div>
<script th:inline="javascript">
var prefix = ctx + "system/record";
// 初始化日期控件
$("input[name='photoDate']").datetimepicker({
format: "yyyy-mm-dd",
minView: "month",
autoclose: true
});
// 表单验证
$("#form-record-edit").validate({ focusCleanup: true });
function submitHandler() {
if (!$('#photoPathHidden').val()) {
alert('请保留或上传一张照片后再提交!');
return;
}
if ($.validate.form()) {
$.operate.save(prefix + "/edit", $('#form-record-edit').serialize());
}
}
// ========== 文件上传逻辑 ==========
const dropArea = document.getElementById('drop-area');
const fileElem = document.getElementById('fileElem');
const preview = document.getElementById('preview');
const uploadBtn = document.getElementById('uploadBtn');
const uploadStatus = document.getElementById('uploadStatus');
const deleteBtn = document.getElementById('deleteBtn');
let selectedFile = null;
// 获取初始 photoPath(Thymeleaf 渲染)
const initialPhotoPath = /*[[${photoRecord.photoPath}]]*/'';
// 初始化预览(如果有已有图片)
function initPreview() {
if (initialPhotoPath) {
preview.innerHTML = '';
const img = document.createElement('img');
// ⭐ 关键:如果 photoPath 是相对路径(如 upload/xxx.jpg),需拼接 ctx 或 /
// 假设你的文件是通过 /common/upload 上传,且访问路径为 /upload/...
// 如果后端返回的是完整 URL,则无需处理;否则建议统一前缀
img.src = initialPhotoPath.startsWith('/') ? initialPhotoPath : ctx + initialPhotoPath;
img.style.maxWidth = '200px';
img.style.maxHeight = '200px';
preview.appendChild(img);
deleteBtn.style.display = 'inline-block';
}
}
initPreview();
// 文件选择变化
fileElem.addEventListener('change', handleFiles);
// 拖拽事件
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, unhighlight, false);
});
function highlight() {
dropArea.style.borderColor = '#007bff';
}
function unhighlight() {
dropArea.style.borderColor = '#ccc';
}
dropArea.addEventListener('drop', (e) => {
preventDefaults(e);
const files = e.dataTransfer.files;
handleFiles({ target: { files } });
});
function handleFiles(e) {
const files = e.target.files;
if (files.length === 0) return;
selectedFile = files[0];
if (!selectedFile.type.match('image.*')) {
alert('请选择图片文件(jpg/png/jpeg)');
selectedFile = null;
uploadBtn.disabled = true;
fileElem.value = ''; // 清空,允许重新选择
return;
}
// 更新预览
preview.innerHTML = '';
const img = document.createElement('img');
img.src = URL.createObjectURL(selectedFile);
img.style.maxWidth = '200px';
img.style.maxHeight = '200px';
preview.appendChild(img);
uploadBtn.disabled = false;
uploadStatus.textContent = '';
}
// 上传新图片
uploadBtn.addEventListener('click', () => {
if (!selectedFile) return;
const formData = new FormData();
formData.append("file", selectedFile);
// ✅ 使用 ctx 上下文路径
fetch(ctx + 'common/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.code === 0) {
$('#photoPathHidden').val(data.fileName);
uploadStatus.textContent = '✅ 上传成功';
uploadBtn.disabled = true;
deleteBtn.style.display = 'inline-block';
// 上传成功后清空 input
fileElem.value = '';
} else {
alert('上传失败:' + (data.msg || '未知错误'));
uploadStatus.textContent = '❌ 上传失败';
fileElem.value = ''; // 允许重试
}
})
.catch(err => {
console.error('上传出错:', err);
alert('网络错误,请重试');
uploadStatus.textContent = '❌ 网络错误';
fileElem.value = '';
});
});
// 删除当前图片
deleteBtn.addEventListener('click', function() {
if (!confirm('确定要删除这张照片吗?删除后无法恢复!')) return;
const photoPath = $('#photoPathHidden').val();
if (photoPath) {
// ✅ 使用 ctx
fetch(ctx + 'common/deleteFile?fileName=' + encodeURIComponent(photoPath), {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.code !== 0) {
alert('服务器删除失败:' + (data.msg || ''));
}
clearPhotoPreview();
})
.catch(err => {
console.error('删除出错:', err);
alert('网络错误,请重试');
clearPhotoPreview(); // 仍清除前端
});
} else {
alert('当前没有照片可删除');
clearPhotoPreview();
}
});
function clearPhotoPreview() {
selectedFile = null;
$('#photoPathHidden').val('');
preview.innerHTML = '';
uploadStatus.textContent = '';
uploadBtn.disabled = true;
deleteBtn.style.display = 'none';
fileElem.value = ''; // 关键:重置文件输入框
}
</script>
以上是添加记录过程中对于图片的上传与删除。
下面是删除数据库记录时,同步删除文件的主要过程。
/remove控制器
@PostMapping( "/remove")
@ResponseBody
public AjaxResult remove(String ids)
{
return toAjax(photoRecordService.deletePhotoRecordByIds(ids));
}
/remove方法所对应的services
@Override
public int deletePhotoRecordByIds(String ids) {
Long[] idArray = Convert.toLongArray(ids);
// 查询这些记录
List<PhotoRecord> records = photoRecordMapper.selectPhotoRecordByIds(idArray);
// 删除文件
for (PhotoRecord r : records) {
if (StringUtils.isNotBlank(r.getPhotoPath())) {
try {
String realPath = RuoYiConfig.getProfile() +
r.getPhotoPath().replaceFirst("^/profile", "");
Path p = Paths.get(realPath);
if (Files.exists(p)) Files.delete(p);
} catch (Exception e) {
log.warn("批量删除文件失败: {}", r.getPhotoPath(), e);
}
}
}
// 批量删除数据库
return photoRecordMapper.deletePhotoRecordByIds(idArray);
}
mapper
List<PhotoRecord> selectPhotoRecordByIds(@Param("ids") Long[] ids);
int deletePhotoRecordByIds(Long[] ids);
mapper.xml
<select id="selectPhotoRecordByIds" resultType="PhotoRecord" resultMap="PhotoRecordResult">
SELECT * FROM sys_photo_record
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<delete id="deletePhotoRecordByIds" parameterType="String">
delete from sys_photo_record where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>