Commit 4fa4ae75 by DaiJiezhang

Refine phone record adapter flow

parent 755bba6d
...@@ -59,6 +59,16 @@ ...@@ -59,6 +59,16 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
<release>17</release>
</configuration>
</plugin>
</plugins> </plugins>
</build> </build>
</project> </project>
...@@ -156,7 +156,8 @@ public class WxPhoneService { ...@@ -156,7 +156,8 @@ public class WxPhoneService {
return null; return null;
} }
String trimmedValue = value.trim(); String trimmedValue = value.trim();
return trimmedValue.isEmpty() ? null : trimmedValue; // 支持空值提交:空字符串表示用户主动清空,应保存为空字符串而非 null
return trimmedValue;
} }
/** /**
......
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
* 代码作用(白话):初始化手机号卡页面的分页、筛选、全字段渲染和图片列辅助能力,让壳层传入的数据与动作变成真正可交互的表格;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):renderExternalRuntime('phone') -> mountPhoneListComponent() -> PhoneListTable.setup() -> 列表渲染/弹窗/抽屉/删除。 * 代码作用(白话):初始化手机号卡页面的分页、筛选、全字段渲染和图片列辅助能力,让壳层传入的数据与动作变成真正可交互的表格;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):renderExternalRuntime('phone') -> mountPhoneListComponent() -> PhoneListTable.setup() -> 列表渲染/弹窗/抽屉/删除。
*/ */
setup(props) { setup(props) {
const { computed, ref } = window.Vue; const { computed, ref, onBeforeUnmount } = window.Vue;
const currentPage = ref(props.mountState.currentPage || 1); const currentPage = ref(props.mountState.currentPage || 1);
const pageSize = ref(props.mountState.pageSize || props.phoneTablePageSizes[0]); const pageSize = ref(props.mountState.pageSize || props.phoneTablePageSizes[0]);
const selectedRecordId = ref(props.globalState.selectedBySource.phone || null); const selectedRecordId = ref(props.globalState.selectedBySource.phone || null);
...@@ -89,6 +89,10 @@ ...@@ -89,6 +89,10 @@
* 代码作用(白话):内联编辑状态,记录当前正在编辑的单元格 { recordId, field, value },同一时刻只允许一个单元格处于编辑态;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):双击单元格 -> handleCellDblclick() -> editingCell -> 渲染输入控件 -> 失焦/回车 -> handleInlineSave() -> buildSavePayload() -> props.savePhoneRecord() -> loadAllRecords()。 * 代码作用(白话):内联编辑状态,记录当前正在编辑的单元格 { recordId, field, value },同一时刻只允许一个单元格处于编辑态;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):双击单元格 -> handleCellDblclick() -> editingCell -> 渲染输入控件 -> 失焦/回车 -> handleInlineSave() -> buildSavePayload() -> props.savePhoneRecord() -> loadAllRecords()。
*/ */
const editingCell = ref(null); const editingCell = ref(null);
/**
* 代码作用(白话):防止重复保存的标志位,避免 mousedown 和 blur 同时触发导致多次提交;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):handleInlineSave() -> inlineSaving -> 防止并发保存。
*/
let inlineSaving = false;
/** /**
* 代码作用(白话):定义哪些字段支持双击内联编辑以及对应的控件类型,避免模板里写死判断逻辑;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):handleCellDblclick() -> isFieldEditable() -> 渲染 input/select/switch。 * 代码作用(白话):定义哪些字段支持双击内联编辑以及对应的控件类型,避免模板里写死判断逻辑;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):handleCellDblclick() -> isFieldEditable() -> 渲染 input/select/switch。
...@@ -133,12 +137,18 @@ ...@@ -133,12 +137,18 @@
} }
/** /**
* 代码作用(白话):内联编辑确认后构造完整保存载荷并调用保存接口,成功后刷新列表并退出编辑态;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js、F:/Project/xyw_console/src/modules/shared/phone-api-client.js;关联逻辑(调用链/消息链/数据流):回车/失焦 -> handleInlineSave() -> buildSavePayload() -> props.savePhoneRecord() -> loadAllRecords() -> editingCell = null。 * 代码作用(白话):内联编辑确认后构造完整保存载荷并调用保存接口,成功后刷新列表并退出编辑态;支持空值提交,允许用户清空字段内容后保存空字符串;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js、F:/Project/xyw_console/src/modules/shared/phone-api-client.js;关联逻辑(调用链/消息链/数据流):回车/失焦/点击外部 -> handleInlineSave() -> buildSavePayload() -> props.savePhoneRecord() -> loadAllRecords() -> editingCell = null。
*/ */
async function handleInlineSave() { async function handleInlineSave() {
if (!editingCell.value) { if (!editingCell.value) {
return; return;
} }
// 防止重复保存(blur 和 mousedown 可能同时触发)
if (inlineSaving) {
return;
}
inlineSaving = true;
try {
const { recordId, field, value } = editingCell.value; const { recordId, field, value } = editingCell.value;
const record = allRecordsCache.value.find((item) => item.id === recordId); const record = allRecordsCache.value.find((item) => item.id === recordId);
if (!record) { if (!record) {
...@@ -149,7 +159,8 @@ ...@@ -149,7 +159,8 @@
if (field === 'outbound') { if (field === 'outbound') {
updatedRecord.outboundCall = value === '可外呼'; updatedRecord.outboundCall = value === '可外呼';
} else { } else {
updatedRecord[field] = value; // 支持空值提交:允许保存空字符串
updatedRecord[field] = value ?? '';
} }
const images = getDisplayImages(record); const images = getDisplayImages(record);
const payload = buildSavePayload(updatedRecord, images); const payload = buildSavePayload(updatedRecord, images);
...@@ -164,6 +175,26 @@ ...@@ -164,6 +175,26 @@
} finally { } finally {
editingCell.value = null; editingCell.value = null;
} }
} finally {
inlineSaving = false;
}
}
/**
* 代码作用(白话):处理 document 级 mousedown 事件,点击非内联编辑区域时自动提交保存;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):点击页面任意位置 -> handleDocumentMousedown() -> 判断是否点击编辑区域外 -> handleInlineSave()。
*/
function handleDocumentMousedown(event) {
if (!editingCell.value) {
return;
}
// 检查点击目标是否在编辑单元格内
const target = event.target;
const editingElement = document.querySelector('.phone-inline-edit-cell');
if (editingElement && editingElement.contains(target)) {
return; // 点击在编辑区域内,不处理
}
// 点击在编辑区域外,触发保存
handleInlineSave();
} }
/** /**
...@@ -405,28 +436,28 @@ ...@@ -405,28 +436,28 @@
} }
/** /**
* 代码作用(白话):按后端 WxPhoneSaveRequest 字段把列表记录拼成完整保存载荷,保证删除图片走 PUT 全量更新时不丢字段;关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/dto/WxPhoneSaveRequest.java、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):handleClearImage() -> buildSavePayload() -> props.savePhoneRecord() -> 后端全量更新。 * 代码作用(白话):按后端 WxPhoneSaveRequest 字段把列表记录拼成完整保存载荷,保证删除图片走 PUT 全量更新时不丢字段;支持空值提交,使用 ?? 或显式判断区分 undefined/null 与空字符串;关联文件:F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/dto/WxPhoneSaveRequest.java、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):handleInlineSave()/handleClearImage() -> buildSavePayload() -> props.savePhoneRecord() -> 后端全量更新。
*/ */
function buildSavePayload(record, images) { function buildSavePayload(record, images) {
return { return {
id: record.id, id: record.id,
phoneNumber: record.phoneNumber || record.phone || '', phoneNumber: record.phoneNumber ?? record.phone ?? '',
realPerson: record.realPerson || '', realPerson: record.realPerson ?? '',
iccid: record.iccid || '', iccid: record.iccid ?? '',
city: record.city || '', city: record.city ?? '',
imageAttachment1: images[0] || '', imageAttachment1: images[0] ?? '',
imageAttachment2: images[1] || '', imageAttachment2: images[1] ?? '',
cardStatus: record.cardStatus || record.status || '正常', cardStatus: record.cardStatus ?? record.status ?? '正常',
cardUsageLocation: record.cardUsageLocation || record.usageLocation || '', cardUsageLocation: record.cardUsageLocation ?? record.usageLocation ?? '',
wecom: record.wecom === true, wecom: record.wecom === true,
wechat: record.wechat === true, wechat: record.wechat === true,
outboundCall: record.outboundCall === true, outboundCall: record.outboundCall === true,
douyinAccount: record.douyinAccount || '', douyinAccount: record.douyinAccount ?? '',
miniProgramFiling: record.miniProgramFiling === true, miniProgramFiling: record.miniProgramFiling === true,
packageChange5yuan: record.packageChange5yuan === true, packageChange5yuan: record.packageChange5yuan === true,
channelOperator: record.channelOperator || record.carrier || '', channelOperator: record.channelOperator ?? record.carrier ?? '',
numberStatus: record.numberStatus ?? true, numberStatus: record.numberStatus ?? true,
linkedWecom: record.linkedWecom || '' linkedWecom: record.linkedWecom ?? ''
}; };
} }
...@@ -617,8 +648,17 @@ ...@@ -617,8 +648,17 @@
props.openAddDialog('add'); props.openAddDialog('add');
} }
window.Vue.onMounted(loadAllRecords); window.Vue.onMounted(() => {
loadAllRecords();
syncMountState(); syncMountState();
// 添加 document 级 mousedown 监听,点击非编辑区域时自动保存
document.addEventListener('mousedown', handleDocumentMousedown, true);
});
window.Vue.onBeforeUnmount(() => {
// 清理事件监听
document.removeEventListener('mousedown', handleDocumentMousedown, true);
});
return { return {
formatCellText, formatCellText,
......
(function attachRecordAdapters() { (function attachRecordAdapters() {
/** /**
* 代码作用(白话):把企微原始记录整理成列表、详情、编辑弹窗都能直接消费的统一字段;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/wechat/wechat-list-runtime.js、F:/Project/xyw_console/src/modules/wechat/wechat-add-dialog.js;关联逻辑(调用链/消息链/数据流):后端/本地记录 -> normalizeWechatRecord() -> app-v2.js 注入 -> 企微列表/详情/编辑弹窗。 * 代码作用(白话):把企微原始记录整理成列表、详情、编辑弹窗都能直接消费的统一字段;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/wechat/wechat-list-runtime.js、F:/Project/xyw_console/src/modules/wechat/wechat-add-dialog.js;关联逻辑(调用链/消息链/数据流):后端/本地记录 -> normalizeWechatRecord() -> app-v2.js 注入 -> 企微列表/详情/编辑弹窗。
*/ */
...@@ -81,7 +81,7 @@ ...@@ -81,7 +81,7 @@
} }
/** /**
* 代码作用(白话):把手机号原始记录整理成列表、详情抽屉、编辑弹窗共用的统一字段,避免每个组件自己猜字段名;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):PhoneApiClient/DATA_SOURCES.phone -> normalizePhoneRecord() -> app-v2.js 注入 -> 手机号列表/详情/编辑弹窗。 * 代码作用(白话):把手机号原始记录整理成列表、详情抽屉、编辑弹窗共用的统一字段,避免每个组件自己猜字段名;支持空值提交,使用 ?? 运算符区分 undefined/null 与空字符串;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):PhoneApiClient/DATA_SOURCES.phone -> normalizePhoneRecord() -> app-v2.js 注入 -> 手机号列表/详情/编辑弹窗。
*/ */
/** /**
* ????????????????????????????????????????????? status ??????????????F:/Project/xyw_console/src/modules/shared/record-adapters.js?F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js?F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js?????????/???/????????? -> normalizePhoneRecord() -> ??/??/????? * ????????????????????????????????????????????? status ??????????????F:/Project/xyw_console/src/modules/shared/record-adapters.js?F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js?F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js?????????/???/????????? -> normalizePhoneRecord() -> ??/??/?????
...@@ -89,41 +89,47 @@ ...@@ -89,41 +89,47 @@
function normalizePhoneRecord(row) { function normalizePhoneRecord(row) {
const source = row || {}; const source = row || {};
const images = buildPhoneImages(source); const images = buildPhoneImages(source);
// cardStatus 支持空值:空字符串表示用户主动清空,应保持为空
const cardStatusRaw = source.cardStatus ?? source.card_status ?? source.status; const cardStatusRaw = source.cardStatus ?? source.card_status ?? source.status;
const cardStatus = cardStatusRaw === 0 || cardStatusRaw === '0' || cardStatusRaw === '异常' const cardStatus = cardStatusRaw === 0 || cardStatusRaw === '0' || cardStatusRaw === '异常'
? '异常停机' ? '异常停机'
: (cardStatusRaw || '正常'); : (cardStatusRaw ?? '正常');
const phoneNumber = source.phoneNumber || source.phone_number || source.phone || ''; const phoneNumber = source.phoneNumber ?? source.phone_number ?? source.phone ?? '';
const realPerson = source.realPerson || source.real_person || ''; // realPerson 支持空值:空字符串表示用户主动清空
const channelOperator = source.channelOperator || source.channel_operator || source.carrier || ''; const realPerson = source.realPerson ?? source.real_person ?? '';
const updatedAt = source.updatedAt || source.updated_at || source.updateTime || ''; // channelOperator 支持空值:空字符串表示用户主动清空
const usageLocation = source.cardUsageLocation || source.card_usage_location || source.usageLocation || ''; const channelOperator = source.channelOperator ?? source.channel_operator ?? source.carrier ?? '';
const updatedAt = source.updatedAt ?? source.updated_at ?? source.updateTime ?? '';
const usageLocation = source.cardUsageLocation ?? source.card_usage_location ?? source.usageLocation ?? '';
const wecomEnabled = toBooleanFlag(source.wecom, false); const wecomEnabled = toBooleanFlag(source.wecom, false);
const wechatEnabled = toBooleanFlag(source.wechat, false); const wechatEnabled = toBooleanFlag(source.wechat, false);
const outboundEnabled = toBooleanFlag(source.outboundCall ?? source.outbound_call ?? source.outbound, false); const outboundEnabled = toBooleanFlag(source.outboundCall ?? source.outbound_call ?? source.outbound, false);
const miniProgramFiled = toBooleanFlag(source.miniProgramFiling ?? source.mini_program_filing, false); const miniProgramFiled = toBooleanFlag(source.miniProgramFiling ?? source.mini_program_filing, false);
const packageChangeEnabled = toBooleanFlag(source.packageChange5yuan ?? source.package_change_5yuan, false); const packageChangeEnabled = toBooleanFlag(source.packageChange5yuan ?? source.package_change_5yuan, false);
const numberStatus = toBooleanFlag(source.numberStatus ?? source.mobile_status ?? source.number_status, true); const numberStatus = toBooleanFlag(source.numberStatus ?? source.mobile_status ?? source.number_status, true);
const wechatStatus = source.wechatStatus || (numberStatus ? '正常' : '异常停机'); const wechatStatus = source.wechatStatus ?? (numberStatus ? '正常' : '异常停机');
const outbound = source.outbound || (outboundEnabled ? '可外呼' : '不可外呼'); const outbound = source.outbound ?? (outboundEnabled ? '可外呼' : '不可外呼');
const linkedWecom = source.linkedWecom || source.linked_wecom || ''; // linkedWecom 支持空值:空字符串表示用户主动清空
const douyinAccount = source.douyinAccount || source.douyin_account || ''; const linkedWecom = source.linkedWecom ?? source.linked_wecom ?? '';
// douyinAccount 支持空值:空字符串表示用户主动清空
const douyinAccount = source.douyinAccount ?? source.douyin_account ?? '';
return { return {
...source, ...source,
id: source.id, id: source.id,
images, images,
primaryImage: images[0] || '', primaryImage: images[0] ?? '',
imageAttachment1: images[0] || source.imageAttachment1 || source.image_attachment_1 || '', imageAttachment1: images[0] ?? source.imageAttachment1 ?? source.image_attachment_1 ?? '',
image_attachment_1: images[0] || source.imageAttachment1 || source.image_attachment_1 || '', image_attachment_1: images[0] ?? source.imageAttachment1 ?? source.image_attachment_1 ?? '',
imageAttachment2: images[1] || source.imageAttachment2 || source.image_attachment_2 || '', imageAttachment2: images[1] ?? source.imageAttachment2 ?? source.image_attachment_2 ?? '',
image_attachment_2: images[1] || source.imageAttachment2 || source.image_attachment_2 || '', image_attachment_2: images[1] ?? source.imageAttachment2 ?? source.image_attachment_2 ?? '',
phone: phoneNumber, phone: phoneNumber,
phoneNumber, phoneNumber,
phone_number: phoneNumber, phone_number: phoneNumber,
realPerson, realPerson,
real_person: realPerson, real_person: realPerson,
city: source.city || '', // city 支持空值:空字符串表示用户主动清空
city: source.city ?? '',
usageLocation, usageLocation,
cardUsageLocation: usageLocation, cardUsageLocation: usageLocation,
card_usage_location: usageLocation, card_usage_location: usageLocation,
...@@ -140,12 +146,13 @@ ...@@ -140,12 +146,13 @@
wechat: wechatEnabled, wechat: wechatEnabled,
wecom: wecomEnabled, wecom: wecomEnabled,
updatedAt, updatedAt,
updated_at: source.updated_at || updatedAt, updated_at: source.updated_at ?? updatedAt,
updateTime: source.updateTime || updatedAt, updateTime: source.updateTime ?? updatedAt,
owner: source.owner || realPerson, owner: source.owner ?? realPerson,
project: source.project || source.note || '暂无备注', project: source.project ?? source.note ?? '暂无备注',
note: source.note || source.project || '', note: source.note ?? source.project ?? '',
iccid: source.iccid || '', // iccid 支持空值:空字符串表示用户主动清空
iccid: source.iccid ?? '',
douyinAccount, douyinAccount,
douyin_account: douyinAccount, douyin_account: douyinAccount,
miniProgramFiling: miniProgramFiled, miniProgramFiling: miniProgramFiled,
......
...@@ -1325,7 +1325,7 @@ ...@@ -1325,7 +1325,7 @@
} }
.phone-image-action--preview:hover { .phone-image-action--preview:hover {
color: #2563eb; color: #fff;
background: rgba(255, 255, 255, 0.18); background: rgba(255, 255, 255, 0.18);
} }
......
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