Commit da65cb92 by DaiJiezhang

Refine fetch handling and phone workflow

parent f2fe141d
...@@ -40,6 +40,5 @@ public class WxPhoneSaveRequest { ...@@ -40,6 +40,5 @@ public class WxPhoneSaveRequest {
private Boolean numberStatus; private Boolean numberStatus;
private String linkedWecom; private String linkedWecom;
private Boolean numberRetentionStatus;
} }
...@@ -64,8 +64,7 @@ public class WxPhoneEntity { ...@@ -64,8 +64,7 @@ public class WxPhoneEntity {
@TableField("linked_wecom") @TableField("linked_wecom")
private String linkedWecom; private String linkedWecom;
@TableField("number_retention_status")
private Boolean numberRetentionStatus;
@TableField(exist = false) @TableField(exist = false)
private String wechatStatus; private String wechatStatus;
...@@ -73,3 +72,4 @@ public class WxPhoneEntity { ...@@ -73,3 +72,4 @@ public class WxPhoneEntity {
@TableField(exist = false) @TableField(exist = false)
private String outbound; private String outbound;
} }
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
channelOperator: '', channelOperator: '',
numberStatus: true, numberStatus: true,
linkedWecom: '', linkedWecom: '',
numberRetentionStatus: true,
imageAttachment1: '', imageAttachment1: '',
imageAttachment2: '' imageAttachment2: ''
}; };
...@@ -124,7 +124,7 @@ ...@@ -124,7 +124,7 @@
formData.channelOperator = record.channelOperator || record.carrier || ''; formData.channelOperator = record.channelOperator || record.carrier || '';
formData.numberStatus = record.numberStatus !== false; formData.numberStatus = record.numberStatus !== false;
formData.linkedWecom = record.linkedWecom || ''; formData.linkedWecom = record.linkedWecom || '';
formData.numberRetentionStatus = record.numberRetentionStatus !== false;
imageItems.value = buildImageItemsFromRecord(record); imageItems.value = buildImageItemsFromRecord(record);
syncImagesToFormData(); syncImagesToFormData();
} }
...@@ -347,12 +347,7 @@ ...@@ -347,12 +347,7 @@
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="24"> <el-row :gutter="24">
<el-col :span="12"> <el-col :span="24">
<el-form-item label="是否保留号码" prop="numberRetentionStatus">
<el-switch v-model="formData.numberRetentionStatus" active-text="保留" inactive-text="注销"></el-switch>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="可变更 5 元套餐" prop="packageChange5yuan"> <el-form-item label="可变更 5 元套餐" prop="packageChange5yuan">
<el-switch v-model="formData.packageChange5yuan"></el-switch> <el-switch v-model="formData.packageChange5yuan"></el-switch>
</el-form-item> </el-form-item>
...@@ -412,3 +407,6 @@ ...@@ -412,3 +407,6 @@
app.use(ElementPlus); app.use(ElementPlus);
app.mount('#phoneDialogRoot'); app.mount('#phoneDialogRoot');
})(); })();
...@@ -34,6 +34,17 @@ ...@@ -34,6 +34,17 @@
return recordTime >= startTime && recordTime <= endTime + 24 * 60 * 60 * 1000 - 1; return recordTime >= startTime && recordTime <= endTime + 24 * 60 * 60 * 1000 - 1;
} }
/**
* 代码作用(白话):把列表里的长文本压成 20 个字符内的单行展示,鼠标悬浮时还能看到完整内容;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):表格单元格原始值 -> formatCellText() -> 列表展示/tooltip 提示。
*/
function formatCellText(value) {
const text = String(value ?? '-');
if (text.length <= 20) {
return text;
}
return `${text.slice(0, 20)}...`;
}
const PhoneListTable = { const PhoneListTable = {
name: 'PhoneListTable', name: 'PhoneListTable',
props: { props: {
...@@ -454,9 +465,21 @@ ...@@ -454,9 +465,21 @@
<a href="#" class="domain-link" @click.prevent="focusRow(scope.row.id)">{{ scope.row.phone || '-' }}</a> <a href="#" class="domain-link" @click.prevent="focusRow(scope.row.id)">{{ scope.row.phone || '-' }}</a>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column property="realPerson" label="真实联系人" min-width="130" /> <el-table-column label="真实联系人" min-width="150">
<el-table-column property="iccid" label="ICCID" min-width="190" /> <template #default="scope">
<el-table-column property="city" label="所在城市" min-width="120" /> <span :title="scope.row.realPerson || '-'" style="display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ formatCellText(scope.row.realPerson) }}</span>
</template>
</el-table-column>
<el-table-column label="ICCID" min-width="220">
<template #default="scope">
<span :title="scope.row.iccid || '-'" style="display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ formatCellText(scope.row.iccid) }}</span>
</template>
</el-table-column>
<el-table-column label="所在城市" min-width="140">
<template #default="scope">
<span :title="scope.row.city || '-'" style="display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ formatCellText(scope.row.city) }}</span>
</template>
</el-table-column>
<el-table-column label="号卡状态" min-width="120"> <el-table-column label="号卡状态" min-width="120">
<template #default="scope"> <template #default="scope">
<span class="status-chip-wrap"> <span class="status-chip-wrap">
...@@ -464,9 +487,7 @@ ...@@ -464,9 +487,7 @@
<span>{{ scope.row.status }}</span> <span>{{ scope.row.status }}</span>
</span> </span>
</template> </template>
</el-table-column> </el-table-column> <el-table-column label="企业微信" min-width="110">
<el-table-column property="cardUsageLocation" label="卡使用地点" min-width="160" />
<el-table-column label="企业微信" min-width="110">
<template #default="scope">{{ formatBooleanText(scope.row.wecom, '已开通', '未开通') }}</template> <template #default="scope">{{ formatBooleanText(scope.row.wecom, '已开通', '未开通') }}</template>
</el-table-column> </el-table-column>
<el-table-column label="微信" min-width="100"> <el-table-column label="微信" min-width="100">
...@@ -475,20 +496,28 @@ ...@@ -475,20 +496,28 @@
<el-table-column label="可外呼" min-width="100"> <el-table-column label="可外呼" min-width="100">
<template #default="scope">{{ formatBooleanText(scope.row.outboundCall, '可外呼', '不可外呼') }}</template> <template #default="scope">{{ formatBooleanText(scope.row.outboundCall, '可外呼', '不可外呼') }}</template>
</el-table-column> </el-table-column>
<el-table-column property="douyinAccount" label="抖音账号" min-width="160" /> <el-table-column label="抖音账号" min-width="180">
<template #default="scope">
<span :title="scope.row.douyinAccount || '-'" style="display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ formatCellText(scope.row.douyinAccount) }}</span>
</template>
</el-table-column>
<el-table-column label="小程序备案" min-width="120"> <el-table-column label="小程序备案" min-width="120">
<template #default="scope">{{ formatBooleanText(scope.row.miniProgramFiling, '已备案', '未备案') }}</template> <template #default="scope">{{ formatBooleanText(scope.row.miniProgramFiling, '已备案', '未备案') }}</template>
</el-table-column> </el-table-column>
<el-table-column property="channelOperator" label="渠道运营商" min-width="130" /> <el-table-column label="渠道运营商" min-width="150">
<template #default="scope">
<span :title="scope.row.channelOperator || '-'" style="display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ formatCellText(scope.row.channelOperator) }}</span>
</template>
</el-table-column>
<el-table-column label="手机号状态" min-width="120"> <el-table-column label="手机号状态" min-width="120">
<template #default="scope">{{ formatStatusText(scope.row.numberStatus) }}</template> <template #default="scope">{{ formatStatusText(scope.row.numberStatus) }}</template>
</el-table-column> </el-table-column>
<el-table-column property="linkedWecom" label="绑定企业微信" min-width="160" /> <el-table-column label="绑定企业微信" min-width="180">
<el-table-column label="号码保留状态" min-width="130"> <template #default="scope">
<template #default="scope">{{ formatRetentionText(scope.row.numberRetentionStatus) }}</template> <span :title="scope.row.linkedWecom || '-'" style="display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ formatCellText(scope.row.linkedWecom) }}</span>
</el-table-column> </template>
<el-table-column label="操作" width="100" fixed="right"> </el-table-column> <el-table-column label="操作" width="100" fixed="right">
<template #default="scope"> <template #default="scope">
<div class="action-cluster" style="display: flex; flex-direction: column; align-items: flex-start; gap: 8px;"> <div class="action-cluster" style="display: flex; flex-direction: column; align-items: flex-start; gap: 8px;">
<button class="action-pill action-pill-primary" type="button" @click.stop="handleRowAction('view', scope.row.id)" style="border: none; background: transparent; color: #2563eb; cursor: pointer; padding: 0; font-weight: 600; line-height: 1;">查看</button> <button class="action-pill action-pill-primary" type="button" @click.stop="handleRowAction('view', scope.row.id)" style="border: none; background: transparent; color: #2563eb; cursor: pointer; padding: 0; font-weight: 600; line-height: 1;">查看</button>
...@@ -544,3 +573,5 @@ ...@@ -544,3 +573,5 @@
...@@ -13,11 +13,33 @@ ...@@ -13,11 +13,33 @@
} }
/** /**
* 代码作用(白话):把浏览器在本地后端没启动时抛出的网络错误翻成稳定中文,避免页面直接展示原始 Failed to fetch;关联文件:F:/Project/xyw_console/src/modules/shared/phone-api-client.js、F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):fetch() 失败 -> requestJson() -> 抛出中文错误 -> 外层 catch -> 页面提示。
*/
function isFetchUnavailableError(error) {
const message = String(error?.message || '').toLowerCase();
return error instanceof TypeError && message.includes('failed to fetch');
}
/**
* 代码作用(白话):先请求接口再统一解析 JSON,并在网络断开或本地后端未启动时返回稳定错误文案;关联文件:F:/Project/xyw_console/src/modules/shared/phone-api-client.js;关联逻辑(调用链/消息链/数据流):listPhones()/savePhone()/deletePhone() -> requestJson() -> parseJsonResponse() -> 外层页面提示。
*/
async function requestJson(url, options, networkMessage) {
try {
const response = await fetch(url, options);
return await parseJsonResponse(response);
} catch (error) {
if (isFetchUnavailableError(error)) {
throw new Error(networkMessage);
}
throw error;
}
}
/**
* 代码作用(白话):从本地手机号接口拉取完整列表,返回后端 data 数组给外层适配;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):phone 页面挂载/刷新 -> listPhones() -> app-v2.js -> normalizePhoneRecord() -> 列表渲染。 * 代码作用(白话):从本地手机号接口拉取完整列表,返回后端 data 数组给外层适配;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):phone 页面挂载/刷新 -> listPhones() -> app-v2.js -> normalizePhoneRecord() -> 列表渲染。
*/ */
async function listPhones() { async function listPhones() {
const response = await fetch(BASE_URL); const json = await requestJson(BASE_URL, undefined, '手机号服务暂时不可用,请确认本地后端已启动');
const json = await parseJsonResponse(response);
return Array.isArray(json.data) ? json.data : []; return Array.isArray(json.data) ? json.data : [];
} }
...@@ -28,12 +50,11 @@ ...@@ -28,12 +50,11 @@
const payload = { ...record }; const payload = { ...record };
const isEdit = Boolean(payload.id); const isEdit = Boolean(payload.id);
const targetUrl = isEdit ? `${BASE_URL}/${payload.id}` : BASE_URL; const targetUrl = isEdit ? `${BASE_URL}/${payload.id}` : BASE_URL;
const response = await fetch(targetUrl, { const json = await requestJson(targetUrl, {
method: isEdit ? 'PUT' : 'POST', method: isEdit ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); }, '手机号保存失败,请确认本地后端已启动');
const json = await parseJsonResponse(response);
return json.data || json; return json.data || json;
} }
...@@ -41,10 +62,9 @@ ...@@ -41,10 +62,9 @@
* 代码作用(白话):删除指定手机号记录,并把失败信息统一抛给外层处理;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):手机号列表删除按钮 -> deletePhoneRecord() -> deletePhone() -> 刷新列表或提示失败。 * 代码作用(白话):删除指定手机号记录,并把失败信息统一抛给外层处理;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):手机号列表删除按钮 -> deletePhoneRecord() -> deletePhone() -> 刷新列表或提示失败。
*/ */
async function deletePhone(id) { async function deletePhone(id) {
const response = await fetch(`${BASE_URL}/${id}`, { const json = await requestJson(`${BASE_URL}/${id}`, {
method: 'DELETE' method: 'DELETE'
}); }, '手机号删除失败,请确认本地后端已启动');
const json = await parseJsonResponse(response);
return json.data || json; return json.data || json;
} }
......
...@@ -99,7 +99,6 @@ ...@@ -99,7 +99,6 @@
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 numberRetentionStatus = toBooleanFlag(source.numberRetentionStatus ?? source.number_retention_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 || ''; const linkedWecom = source.linkedWecom || source.linked_wecom || '';
...@@ -152,8 +151,6 @@ ...@@ -152,8 +151,6 @@
mobile_status: numberStatus, mobile_status: numberStatus,
linkedWecom, linkedWecom,
linked_wecom: linkedWecom, linked_wecom: linkedWecom,
numberRetentionStatus,
number_retention_status: numberRetentionStatus
}; };
} }
...@@ -162,3 +159,4 @@ ...@@ -162,3 +159,4 @@
normalizePhoneRecord normalizePhoneRecord
}; };
})(); })();
...@@ -14,11 +14,33 @@ ...@@ -14,11 +14,33 @@
} }
/** /**
* 代码作用(白话):把浏览器在本地后端没启动时抛出的网络错误翻成稳定中文,避免页面直接展示原始 Failed to fetch;关联文件:F:/Project/xyw_console/src/modules/shared/wechat-api-client.js、F:/Project/xyw_console/src/modules/wechat/wechat-list-runtime.js、F:/Project/xyw_console/src/modules/wechat/wechat-add-dialog.js;关联逻辑(调用链/消息链/数据流):fetch() 失败 -> requestJson() -> 抛出中文错误 -> 外层 catch -> 页面提示。
*/
function isFetchUnavailableError(error) {
const message = String(error?.message || '').toLowerCase();
return error instanceof TypeError && message.includes('failed to fetch');
}
/**
* 代码作用(白话):先请求接口再统一解析 JSON,并在网络断开或本地后端未启动时返回稳定错误文案;关联文件:F:/Project/xyw_console/src/modules/shared/wechat-api-client.js;关联逻辑(调用链/消息链/数据流):listWechatRecords()/getWechatRecord()/saveWechatRecord() -> requestJson() -> parseJsonResponse() -> 外层页面提示。
*/
async function requestJson(url, options, networkMessage) {
try {
const response = await fetch(url, options);
return await parseJsonResponse(response);
} catch (error) {
if (isFetchUnavailableError(error)) {
throw new Error(networkMessage);
}
throw error;
}
}
/**
* 代码作用(白话):从后端拉取企微资料全量列表,交给页面继续做本地筛选和分页;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/wechat/wechat-list-runtime.js;关联逻辑(调用链/消息链/数据流):wechat 页面挂载/刷新 -> listWechatRecords() -> app-v2.js -> normalizeWechatRecord() -> 列表渲染。 * 代码作用(白话):从后端拉取企微资料全量列表,交给页面继续做本地筛选和分页;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/wechat/wechat-list-runtime.js;关联逻辑(调用链/消息链/数据流):wechat 页面挂载/刷新 -> listWechatRecords() -> app-v2.js -> normalizeWechatRecord() -> 列表渲染。
*/ */
async function listWechatRecords() { async function listWechatRecords() {
const response = await fetch(BASE_URL); const json = await requestJson(BASE_URL, undefined, '企微服务暂时不可用,请确认本地后端已启动');
const json = await parseJsonResponse(response);
return Array.isArray(json.data) ? json.data : []; return Array.isArray(json.data) ? json.data : [];
} }
...@@ -26,8 +48,7 @@ ...@@ -26,8 +48,7 @@
* 代码作用(白话):按主键读取单条企微资料,让详情抽屉和编辑弹窗拿到数据库最新值;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/wechat/wechat-detail-drawer.js;关联逻辑(调用链/消息链/数据流):查看/编辑按钮 -> getWechatRecord() -> app-v2.js -> 详情抽屉或编辑弹窗打开。 * 代码作用(白话):按主键读取单条企微资料,让详情抽屉和编辑弹窗拿到数据库最新值;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/wechat/wechat-detail-drawer.js;关联逻辑(调用链/消息链/数据流):查看/编辑按钮 -> getWechatRecord() -> app-v2.js -> 详情抽屉或编辑弹窗打开。
*/ */
async function getWechatRecord(id) { async function getWechatRecord(id) {
const response = await fetch(`${BASE_URL}/${id}`); const json = await requestJson(`${BASE_URL}/${id}`, undefined, '企微详情读取失败,请确认本地后端已启动');
const json = await parseJsonResponse(response);
return json.data || null; return json.data || null;
} }
...@@ -41,12 +62,11 @@ ...@@ -41,12 +62,11 @@
if (!isEdit) { if (!isEdit) {
delete payload.id; delete payload.id;
} }
const response = await fetch(targetUrl, { const json = await requestJson(targetUrl, {
method: isEdit ? 'PUT' : 'POST', method: isEdit ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); }, '企微保存失败,请确认本地后端已启动');
const json = await parseJsonResponse(response);
return json.data || json; return json.data || json;
} }
...@@ -56,4 +76,3 @@ ...@@ -56,4 +76,3 @@
saveWechatRecord saveWechatRecord
}; };
})(); })();
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