Commit 755bba6d by DaiJiezhang

Refine phone image handling flow

parent 1b367d0d
package com.xyw.console.phone.entity;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
......@@ -25,10 +26,10 @@ public class WxPhoneEntity {
@TableField("city")
private String city;
@TableField("image_attachment_1")
@TableField(value = "image_attachment_1", updateStrategy = FieldStrategy.ALWAYS)
private String imageAttachment1;
@TableField("image_attachment_2")
@TableField(value = "image_attachment_2", updateStrategy = FieldStrategy.ALWAYS)
private String imageAttachment2;
@TableField("card_status")
......
......@@ -2,6 +2,18 @@ package com.xyw.console.phone.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xyw.console.phone.entity.WxPhoneEntity;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
public interface WxPhoneMapper extends BaseMapper<WxPhoneEntity> {
/**
* 显式更新图片字段,确保 null 值能写入 DB。
* FieldStrategy.ALWAYS 在自定义 SqlSessionFactory 下可能不生效,
* 这里用原生 SQL 绕过,保证删除图片时字段被置为 NULL。
*/
@Update("UPDATE wx_phone SET image_attachment_1 = #{imageAttachment1}, image_attachment_2 = #{imageAttachment2} WHERE id = #{id}")
int updateImages(@Param("id") Long id,
@Param("imageAttachment1") String imageAttachment1,
@Param("imageAttachment2") String imageAttachment2);
}
......@@ -6,8 +6,10 @@ import com.xyw.console.phone.entity.WxPhoneEntity;
import com.xyw.console.phone.mapper.WxPhoneMapper;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class WxPhoneService {
......@@ -41,13 +43,44 @@ public class WxPhoneService {
/**
* 浠g爜浣滅敤锛堢櫧璇濓級锛氭洿鏂版寚瀹氭墜鏈哄彿鍗★紝骞舵妸鏁版嵁搴撻噷鐨勬渶鏂拌褰曡繑鍥炵粰鍓嶇銆? * 鍏宠仈鏂囦欢锛欶:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/controller/WxPhoneController.java銆丗:/Project/xyw_console/src/modules/phone/phone-add-dialog.js銆? * 鍏宠仈閫昏緫锛堣皟鐢ㄩ摼/娑堟伅閾?鏁版嵁娴侊級锛歅UT /api/wx-phones/{id} -> updateRecord() -> Mapper.updateById() -> 鍓嶇鍒锋柊鍒楄〃銆? */
public WxPhoneEntity updateRecord(Long id, WxPhoneSaveRequest request) {
log.info("更新请求:id={}, request.imageAttachment1={}, request.imageAttachment2={}",
id,
request.getImageAttachment1() == null ? "null" : "长度=" + request.getImageAttachment1().length(),
request.getImageAttachment2() == null ? "null" : "长度=" + request.getImageAttachment2().length());
WxPhoneEntity entity = wxPhoneMapper.selectById(id);
if (entity == null) {
log.warn("更新手机号卡失败:未找到 id={}", id);
return null;
}
String oldImage1 = entity.getImageAttachment1();
String oldImage2 = entity.getImageAttachment2();
copyRequestToEntity(request, entity);
wxPhoneMapper.updateById(entity);
return normalizeEntity(wxPhoneMapper.selectById(id));
String newImage1 = entity.getImageAttachment1();
String newImage2 = entity.getImageAttachment2();
log.info("复制后:id={}, entity.imageAttachment1={}, entity.imageAttachment2={}",
id,
newImage1 == null ? "null" : "长度=" + newImage1.length(),
newImage2 == null ? "null" : "长度=" + newImage2.length());
if (oldImage1 != null && newImage1 == null) {
log.info("删除图片:id={}, 字段=image_attachment_1, 旧值长度={}", id, oldImage1.length());
}
if (oldImage2 != null && newImage2 == null) {
log.info("删除图片:id={}, 字段=image_attachment_2, 旧值长度={}", id, oldImage2.length());
}
int updatedRows = wxPhoneMapper.updateById(entity);
log.info("updateById 返回行数={}, id={}", updatedRows, id);
// 显式更新图片字段,绕过 FieldStrategy.ALWAYS 可能不生效的问题,确保 null 值写入 DB
int imageRows = wxPhoneMapper.updateImages(id, newImage1, newImage2);
log.info("updateImages 返回行数={}, id={}, image1={}, image2={}",
imageRows, id,
newImage1 == null ? "null" : "长度=" + newImage1.length(),
newImage2 == null ? "null" : "长度=" + newImage2.length());
WxPhoneEntity updated = wxPhoneMapper.selectById(id);
log.info("更新完成:id={}, image_attachment_1={}, image_attachment_2={}",
id,
updated.getImageAttachment1() != null ? "存在(长度=" + updated.getImageAttachment1().length() + ")" : "null",
updated.getImageAttachment2() != null ? "存在(长度=" + updated.getImageAttachment2().length() + ")" : "null");
return normalizeEntity(updated);
}
/**
......
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
......@@ -7,7 +7,7 @@
<title>学有为资产管理台</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%230f766e'/%3E%3Ctext x='50%25' y='55%25' text-anchor='middle' font-size='26' font-family='Arial' fill='white'%3EXY%3C/text%3E%3C/svg%3E" />
<link rel="stylesheet" href="node_modules/element-plus/dist/index.css" />
<link rel="stylesheet" href="styles-v2.css" />
<link rel="stylesheet" href="styles-v2.css?v=20260707" />
</head>
<body>
......@@ -91,8 +91,8 @@
<script src="src/modules/wechat/wechat-list-runtime.js"></script>
<script src="src/modules/phone/phone-add-dialog.js"></script>
<script src="src/modules/phone/phone-detail-drawer.js"></script>
<script src="src/modules/phone/phone-image-cell.js"></script>
<script src="src/modules/phone/phone-list-runtime.js"></script>
<script src="src/modules/phone/phone-image-cell.js?v=20260707"></script>
<script src="src/modules/phone/phone-list-runtime.js?v=20260707"></script>
<script src="src/modules/shared/record-adapters.js"></script>
<script src="src/modules/shared/wechat-api-client.js"></script>
<script src="src/modules/shared/phone-api-client.js"></script>
......
(function attachPhoneImageCell() {
const { ref } = window.Vue;
/**
* 代码作用(白话):手机号卡列表图片列的图片单元组件,负责缩略图展示、占位图回退、预览与删除悬浮操作;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):PhoneListTable 图片列 -> <phone-card-image-cell> -> 预览/删除回调 -> 列表刷新。
*/
function handleImageError(event) {
const image = event?.target;
if (!image) {
return;
}
if (!image) { return; }
image.style.display = 'none';
const placeholder = image.nextElementSibling;
if (placeholder) {
placeholder.style.display = 'flex';
}
if (placeholder) { placeholder.style.display = 'flex'; }
}
const PhoneCardImageCell = {
......@@ -21,76 +14,52 @@
props: {
images: { type: Array, default: () => [] },
recordId: { type: [String, Number], default: null },
clearImage: { type: Function, default: null }
clearImage: { type: Function, default: null },
uploadImage: { type: Function, default: null }
},
setup(props) {
const previewVisible = ref(false);
const previewIndex = ref(0);
const imageInputRef = ref(null);
const uploading = ref(false);
function openPreview(index) {
previewIndex.value = index;
previewVisible.value = true;
}
function openPreview(index) { previewIndex.value = index; previewVisible.value = true; }
function closePreview() { previewVisible.value = false; }
function handleDelete(index) { if (typeof props.clearImage === 'function') { props.clearImage(index); } }
function closePreview() {
previewVisible.value = false;
function triggerUpload() {
if (uploading.value) { return; }
if (typeof props.uploadImage !== 'function') { return; }
imageInputRef.value?.click();
}
function handleDelete(index) {
if (typeof props.clearImage === 'function') {
props.clearImage(index);
}
async function handleFileChange(event) {
const input = event?.target;
const files = Array.from(input?.files || []);
if (!files.length) { if (input) { input.value = ''; } return; }
uploading.value = true;
try { await props.uploadImage(files); } finally { uploading.value = false; if (input) { input.value = ''; } }
}
return {
previewVisible,
previewIndex,
openPreview,
closePreview,
handleDelete,
handleImageError
};
return { previewVisible, previewIndex, imageInputRef, openPreview, closePreview, handleDelete, handleImageError, triggerUpload, handleFileChange };
},
template: `
<div class="phone-image-cell">
<div class="phone-image-cell" @click.stop @mousedown.stop @dblclick.stop>
<input ref="imageInputRef" type="file" accept="image/*" multiple class="phone-image-input" @change="handleFileChange" style="position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;" />
<template v-if="images && images.length">
<div
v-for="(image, index) in images"
:key="(recordId ?? 'row') + '-img-' + index"
class="phone-image-thumb"
>
<img :src="image" alt="号卡图片缩略图" class="phone-image-img" @error="handleImageError" />
<div class="phone-image-fallback">占位图</div>
<div v-for="(image, index) in images" :key="(recordId ?? 'row') + '-img-' + index" class="phone-image-thumb" @click.stop @mousedown.stop @dblclick.stop>
<img :src="image" alt="thumbnail" class="phone-image-img" @error="handleImageError" />
<div class="phone-image-fallback">双击上传</div>
<div class="phone-image-actions" @click.stop @mousedown.stop @dblclick.stop>
<button
type="button"
class="phone-image-action phone-image-action--preview"
title="预览图片"
@click.stop="openPreview(index)"
@mousedown.stop
><svg viewBox="0 0 1024 1024" width="16" height="16"><path fill="currentColor" d="M515.2 224c-307.2 0-492.8 313.6-492.8 313.6s214.4 304 492.8 304 492.8-304 492.8-304S822.4 224 515.2 224zM832 652.8c-102.4 86.4-211.2 140.8-320 140.8s-217.6-51.2-320-140.8c-35.2-32-70.4-64-99.2-99.2-6.4-6.4-9.6-12.8-16-19.2 3.2-6.4 9.6-12.8 12.8-19.2 25.6-35.2 57.6-70.4 92.8-102.4 99.2-89.6 208-144 329.6-144s230.4 54.4 329.6 144c35.2 32 64 67.2 92.8 102.4 3.2 6.4 9.6 12.8 12.8 19.2-3.2 6.4-9.6 12.8-16 19.2C902.4 585.6 870.4 620.8 832 652.8z"/><path fill="currentColor" d="M512 345.6c-96 0-169.6 76.8-169.6 169.6 0 96 76.8 169.6 169.6 169.6 96 0 169.6-76.8 169.6-169.6C681.6 422.4 604.8 345.6 512 345.6zM512 640c-67.2 0-121.6-54.4-121.6-121.6 0-67.2 54.4-121.6 121.6-121.6 67.2 0 121.6 54.4 121.6 121.6C633.6 582.4 579.2 640 512 640z"/></svg></button>
<button
type="button"
class="phone-image-action phone-image-action--delete"
title="删除图片"
@click.stop="handleDelete(index)"
@mousedown.stop
><svg viewBox="0 0 1024 1024" width="16" height="16"><path fill="currentColor" d="M709.469091 209.454545H930.909091a34.909091 34.909091 0 0 1 0 69.818182h-81.454546v607.185455c0 56.366545-44.311273 102.632727-99.746909 102.632727H274.292364c-55.435636 0-99.746909-46.266182-99.746909-102.632727V279.272727H93.090909a34.909091 34.909091 0 0 1 0-69.818182h244.712727a186.181818 186.181818 0 0 1 371.665455 0z m-70.050909 0a116.363636 116.363636 0 0 0-231.563637 0h231.563637z m140.218182 69.818182h-535.272728v607.185455c0 18.455273 13.730909 32.814545 29.928728 32.814545h475.415272c16.174545 0 29.928727-14.359273 29.928728-32.814545V279.272727z m-418.909091 147.2a34.909091 34.909091 0 0 1 69.818182 0v338.897455a34.909091 34.909091 0 0 1-69.818182 0V426.472727z m232.727272 0a34.909091 34.909091 0 0 1 69.818182 0v338.897455a34.909091 34.909091 0 0 1-69.818182 0V426.472727z"/></svg></button>
<button type="button" class="phone-image-action phone-image-action--preview" title="预览" @click.stop="openPreview(index)" @mousedown.stop @dblclick.stop><svg viewBox="0 0 1024 1024" width="16" height="16"><path fill="currentColor" d="M515.2 224c-307.2 0-492.8 313.6-492.8 313.6s214.4 304 492.8 304 492.8-304 492.8-304S822.4 224 515.2 224zM832 652.8c-102.4 86.4-211.2 140.8-320 140.8s-217.6-51.2-320-140.8c-35.2-32-70.4-64-99.2-99.2-6.4-6.4-9.6-12.8-16-19.2 3.2-6.4 9.6-12.8 12.8-19.2 25.6-35.2 57.6-70.4 92.8-102.4 99.2-89.6 208-144 329.6-144s230.4 54.4 329.6 144c35.2 32 64 67.2 92.8 102.4 3.2 6.4 9.6 12.8 12.8 19.2-3.2 6.4-9.6 12.8-16 19.2C902.4 585.6 870.4 620.8 832 652.8z"/><path fill="currentColor" d="M512 345.6c-96 0-169.6 76.8-169.6 169.6 0 96 76.8 169.6 169.6 169.6 96 0 169.6-76.8 169.6-169.6C681.6 422.4 604.8 345.6 512 345.6zM512 640c-67.2 0-121.6-54.4-121.6-121.6 0-67.2 54.4-121.6 121.6-121.6 67.2 0 121.6 54.4 121.6 121.6C633.6 582.4 579.2 640 512 640z"/></svg></button>
<button type="button" class="phone-image-action phone-image-action--delete" title="删除" @click.stop="handleDelete(index)" @mousedown.stop @dblclick.stop><svg viewBox="0 0 1024 1024" width="16" height="16"><path fill="currentColor" d="M709.469091 209.454545H930.909091a34.909091 34.909091 0 0 1 0 69.818182h-81.454546v607.185455c0 56.366545-44.311273 102.632727-99.746909 102.632727H274.292364c-55.435636 0-99.746909-46.266182-99.746909-102.632727V279.272727H93.090909a34.909091 34.909091 0 0 1 0-69.818182h244.712727a186.181818 186.181818 0 0 1 371.665455 0z m-70.050909 0a116.363636 116.363636 0 0 0-231.563637 0h231.563637z m140.218182 69.818182h-535.272728v607.185455c0 18.455273 13.730909 32.814545 29.928728 32.814545h475.415272c16.174545 0 29.928727-14.359273 29.928728-32.814545V279.272727z m-418.909091 147.2a34.909091 34.909091 0 0 1 69.818182 0v338.897455a34.909091 34.909091 0 0 1-69.818182 0V426.472727z m232.727272 0a34.909091 34.909091 0 0 1 69.818182 0v338.897455a34.909091 34.909091 0 0 1-69.818182 0V426.472727z"/></svg></button>
</div>
</div>
</template>
<div v-else class="phone-image-thumb phone-image-thumb--empty">
<span class="phone-image-fallback">占位图</span>
<div v-else class="phone-image-thumb phone-image-thumb--empty" title="双击上传" @dblclick.stop="triggerUpload" @mousedown.stop @click.stop>
<span class="phone-image-fallback">双击上传</span>
</div>
<el-image-viewer
v-if="previewVisible"
:url-list="images"
:initial-index="previewIndex"
:z-index="3000"
:hide-on-click-modal="true"
teleported
@close="closePreview"
/>
<el-image-viewer v-if="previewVisible" :url-list="images" :initial-index="previewIndex" :z-index="3000" :hide-on-click-modal="true" teleported @close="closePreview" />
</div>
`
};
......
(function attachPhoneApiClient() {
(function attachPhoneApiClient() {
const BASE_URL = 'http://localhost:8080/api/wx-phones';
/**
......@@ -50,12 +50,19 @@
const payload = { ...record };
const isEdit = Boolean(payload.id);
const targetUrl = isEdit ? `${BASE_URL}/${payload.id}` : BASE_URL;
console.info('[phone-api] savePhone 请求:id=', payload.id,
'imageAttachment1 长度=', (payload.imageAttachment1 || '').length,
'imageAttachment2 长度=', (payload.imageAttachment2 || '').length);
const json = await requestJson(targetUrl, {
method: isEdit ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}, '手机号保存失败,请确认本地后端已启动');
return json.data || json;
const data = json.data || json;
console.info('[phone-api] savePhone 响应:id=', payload.id,
'imageAttachment1 长度=', (data?.imageAttachment1 || data?.image_attachment_1 || '').length,
'imageAttachment2 长度=', (data?.imageAttachment2 || data?.image_attachment_2 || '').length);
return data;
}
/**
......
......@@ -1274,10 +1274,14 @@
width: 100%;
height: 100%;
font-size: 11px;
color: #94a3b8;
color: #BBBFCF !important;
background: #f8fafc;
}
.phone-table-header-nowrap span[title="双击编辑"] {
color: #BBBFCF !important;
}
.phone-image-thumb--empty .phone-image-fallback {
display: flex;
}
......@@ -1329,3 +1333,42 @@
color: #ef4444;
background: rgba(255, 255, 255, 0.18);
}
/* Hidden file input used by the phone image cell for double-click upload */
.phone-image-input {
display: none;
}
/* Empty placeholder: hint interactivity on hover and double-click */
.phone-image-thumb--empty {
cursor: pointer;
user-select: none;
}
.phone-image-thumb--empty:hover {
border-color: #BBBFCF;
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
}
/* Inline edit cell: input/select used when double-clicking an editable table cell */
.phone-inline-edit-cell {
width: 100%;
min-width: 80px;
}
.phone-inline-edit-cell .el-input__wrapper,
.phone-inline-edit-cell .el-select .el-input__wrapper {
border-color: #BBBFCF !important;
box-shadow: none !important;
background: #ffffff;
}
.phone-inline-edit-cell .el-input__wrapper:hover,
.phone-inline-edit-cell .el-select .el-input__wrapper:hover {
border-color: #9ca3af !important;
}
.phone-inline-edit-cell .el-input__inner {
font-size: 13px;
color: #111827;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment