Commit f2fe141d by DaiJiezhang

Refine phone workflow and ignore local backend files

parent 85fee5d2
......@@ -11,3 +11,5 @@ backend/target/
F_TMP_WRITE_TEST.txt
localhost
wechat-page.png
backend/src/main/resources/application.yml
start-backend.bat
server:
port: 8080
spring:
application:
name: xyw-console-backend
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: ${XYW_DB_URL:jdbc:mysql://localhost:3306/xyw_private_data?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true}
username: ${XYW_DB_USERNAME:root}
password: ${XYW_DB_PASSWORD:}
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
......@@ -2,31 +2,85 @@
const { createApp, ref, reactive } = window.Vue;
const ElementPlus = window.ElementPlus;
/**
* 代码作用(白话):给手机号卡新增/编辑弹窗准备一份干净的默认表单对象,避免多次打开时残留上一次输入;关联文件:F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js、F:/Project/xyw_console/src/modules/shared/record-adapters.js;关联逻辑(调用链/消息链/数据流):open('add') -> buildEmptyFormData() -> reactive formData -> 弹窗输入框和图片区域渲染。
*/
function buildEmptyFormData() {
return {
id: null,
phoneNumber: '',
realPerson: '',
iccid: '',
city: '',
cardStatus: '正常',
cardUsageLocation: '',
wecom: false,
wechat: false,
outboundCall: false,
douyinAccount: '',
miniProgramFiling: false,
packageChange5yuan: false,
channelOperator: '',
numberStatus: true,
linkedWecom: '',
numberRetentionStatus: true,
imageAttachment1: '',
imageAttachment2: ''
};
}
/**
* 代码作用(白话):把本地图片文件转成当前接口可直接保存的文本内容,避免新增文件上传接口;关联文件:F:/Project/xyw_console/src/modules/shared/phone-api-client.js、F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/dto/WxPhoneSaveRequest.java;关联逻辑(调用链/消息链/数据流):本地文件 -> readFileAsDataUrl() -> imageItems -> submitAfterValidate() -> savePhone()。
*/
function readFileAsDataUrl(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '');
reader.onerror = () => reject(new Error('读取图片失败,请重新选择'));
reader.readAsDataURL(file);
});
}
/**
* 代码作用(白话):把已有记录里的两个 SQL 图片字段整理成弹窗统一图片预览集合,保证编辑时能按一个图片区域回显;关联文件:F:/Project/xyw_console/src/modules/shared/record-adapters.js、F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):record.images/imageAttachment1/2 -> buildImageItemsFromRecord() -> populateFormData() -> 图片上传区域预览。
*/
function buildImageItemsFromRecord(record) {
const rawImages = Array.isArray(record?.images)
? record.images
: [record?.imageAttachment1, record?.image_attachment_1, record?.imageAttachment2, record?.image_attachment_2].filter(Boolean);
return rawImages
.filter(Boolean)
.filter((value, index, array) => array.indexOf(value) === index)
.slice(0, 2)
.map((url, index) => ({
id: `existing-${index}`,
url,
name: `已保存图片${index + 1}`
}));
}
/**
* 代码作用(白话):读取外层注入的手机号保存动作,没有配置时直接给出明确错误,避免弹窗假保存;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/shared/phone-api-client.js;关联逻辑(调用链/消息链/数据流):保存按钮 -> getPhoneAddDialogActions() -> save()/afterSave() -> 刷新列表。
*/
function getPhoneAddDialogActions() {
if (!window.PhoneAddDialogActions?.save) {
throw new Error('手机号保存动作尚未初始化');
}
return window.PhoneAddDialogActions;
}
const PhoneAddDialog = {
/**
* 代码作用(白话):初始化手机号卡弹窗的表单状态、图片多选上传和统一提交流程,让列表与详情都能共用一个弹窗;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js;关联逻辑(调用链/消息链/数据流):新增/编辑入口 -> PhoneAddDialogAPI.open() -> setup() 状态变化 -> 表单与图片区域渲染。
*/
setup() {
const visible = ref(false);
const formRef = ref(null);
const imageInputRef = ref(null);
const isEdit = ref(false);
const formData = reactive({
id: null,
phoneNumber: '',
realPerson: '',
iccid: '',
city: '',
cardStatus: '正常',
cardUsageLocation: '',
wecom: false,
wechat: false,
outboundCall: false,
douyinAccount: '',
miniProgramFiling: false,
packageChange5yuan: false,
channelOperator: '',
numberStatus: true,
linkedWecom: '',
numberRetentionStatus: true
});
const imageItems = ref([]);
const formData = reactive(buildEmptyFormData());
const rules = {
phoneNumber: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
......@@ -34,30 +88,24 @@
};
/**
* 代码作用(白话):把手机号表单恢复成新增状态,避免上一次编辑数据残留到下一次打开弹窗;关联文件:F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):open('add') -> resetFormData() -> formData 默认值 -> 表单渲染。
* 代码作用(白话):把图片预览集合回写到两个 SQL 对应的表单字段,保证一个图片区域里的两张图最终能落回后端两个字段;关联文件:F:/Project/xyw_console/src/modules/shared/phone-api-client.js、F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/dto/WxPhoneSaveRequest.java;关联逻辑(调用链/消息链/数据流):imageItems -> syncImagesToFormData() -> formData.imageAttachment1/2 -> submitAfterValidate()。
*/
function syncImagesToFormData() {
formData.imageAttachment1 = imageItems.value[0]?.url || '';
formData.imageAttachment2 = imageItems.value[1]?.url || '';
}
/**
* 代码作用(白话):把手机号表单恢复成新增状态,避免上一次编辑数据残留到下一次打开弹窗;关联文件:F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):open('add') -> resetFormData() -> formData 默认值和图片区域清空 -> 表单渲染。
*/
function resetFormData() {
formData.id = null;
formData.phoneNumber = '';
formData.realPerson = '';
formData.iccid = '';
formData.city = '';
formData.cardStatus = '正常';
formData.cardUsageLocation = '';
formData.wecom = false;
formData.wechat = false;
formData.outboundCall = false;
formData.douyinAccount = '';
formData.miniProgramFiling = false;
formData.packageChange5yuan = false;
formData.channelOperator = '';
formData.numberStatus = true;
formData.linkedWecom = '';
formData.numberRetentionStatus = true;
Object.assign(formData, buildEmptyFormData());
imageItems.value = [];
syncImagesToFormData();
}
/**
* 代码作用(白话):把外层传入的手机号记录回填到表单里,兼容列表记录和详情记录两种字段名;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js;关联逻辑(调用链/消息链/数据流):编辑按钮/详情抽屉编辑 -> PhoneAddDialogAPI.open('edit', record) -> populateFormData() -> 表单回填。
* 代码作用(白话):把外层传入的手机号记录回填到表单里,兼容列表记录和详情记录两种字段名,并把两张旧图按一个图片区域回显;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js、F:/Project/xyw_console/src/modules/shared/record-adapters.js;关联逻辑(调用链/消息链/数据流):编辑按钮/详情抽屉编辑 -> PhoneAddDialogAPI.open('edit', record) -> populateFormData() -> 表单与图片预览回填。
*/
function populateFormData(record) {
formData.id = record.id || null;
......@@ -77,10 +125,12 @@
formData.numberStatus = record.numberStatus !== false;
formData.linkedWecom = record.linkedWecom || '';
formData.numberRetentionStatus = record.numberRetentionStatus !== false;
imageItems.value = buildImageItemsFromRecord(record);
syncImagesToFormData();
}
/**
* 代码作用(白话):按新增或编辑模式打开手机号弹窗,并决定是重置表单还是回填旧记录;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):新增/编辑按钮 -> PhoneAddDialogAPI.open() -> open() -> visible/formData 更新。
* 代码作用(白话):按新增或编辑模式打开手机号弹窗,并决定是重置表单还是回填旧记录;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):新增/编辑按钮 -> PhoneAddDialogAPI.open() -> open() -> visible/formData/imageItems 更新。
*/
function open(mode = 'add', record = null) {
visible.value = true;
......@@ -102,7 +152,57 @@
}
/**
* 代码作用(白话):在表单校验通过后调用外层统一保存动作,并在成功后刷新列表;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/shared/phone-api-client.js;关联逻辑(调用链/消息链/数据流):保存按钮 -> handleSubmit() -> PhoneAddDialogActions.save() -> afterSave() -> phone 列表刷新。
* 代码作用(白话):触发系统文件选择框,让图片上传区域保持一个按钮入口而不是暴露两个底层字段;关联文件:F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):点击“选择图片” -> triggerImageSelect() -> hidden file input -> handleImageFileChange()。
*/
function triggerImageSelect() {
imageInputRef.value?.click();
}
/**
* 代码作用(白话):读取用户一次多选的图片文件,限制最多 2 张,并生成可预览可保存的图片集合;关联文件:F:/Project/xyw_console/src/modules/shared/phone-api-client.js、F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/dto/WxPhoneSaveRequest.java;关联逻辑(调用链/消息链/数据流):file input change -> handleImageFileChange() -> readFileAsDataUrl() -> imageItems/syncImagesToFormData() -> submitAfterValidate()。
*/
async function handleImageFileChange(event) {
const files = Array.from(event?.target?.files || []);
if (!files.length) {
return;
}
if (imageItems.value.length + files.length > 2) {
ElementPlus.ElMessage.warning('最多上传 2 张图片');
event.target.value = '';
return;
}
try {
const nextItems = [];
for (let index = 0; index < files.length; index += 1) {
const file = files[index];
const url = await readFileAsDataUrl(file);
nextItems.push({
id: `${Date.now()}-${index}`,
url,
name: file.name || `图片${imageItems.value.length + index + 1}`
});
}
imageItems.value = imageItems.value.concat(nextItems).slice(0, 2);
syncImagesToFormData();
} catch (error) {
ElementPlus.ElMessage.error(error.message || '读取图片失败,请稍后重试');
} finally {
event.target.value = '';
}
}
/**
* 代码作用(白话):删除当前图片区域中的单张图片,并把剩余图片重新同步回两个 SQL 字段;关联文件:F:/Project/xyw_console/src/modules/shared/phone-api-client.js、F:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/dto/WxPhoneSaveRequest.java;关联逻辑(调用链/消息链/数据流):点击删除图片 -> removeImage() -> imageItems -> syncImagesToFormData() -> 保存请求。
*/
function removeImage(index) {
imageItems.value = imageItems.value.filter((_, itemIndex) => itemIndex !== index);
syncImagesToFormData();
}
/**
* 代码作用(白话):在表单校验通过后调用外层统一保存动作,并在成功后刷新列表;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/shared/phone-api-client.js;关联逻辑(调用链/消息链/数据流):保存按钮 -> handleSubmit() -> submitAfterValidate() -> PhoneAddDialogActions.save() -> afterSave() -> phone 列表刷新。
*/
async function submitAfterValidate(valid) {
if (!valid) {
......@@ -110,10 +210,8 @@
}
try {
const actions = window.PhoneAddDialogActions || {};
if (typeof actions.save !== 'function') {
throw new Error('手机号保存动作尚未初始化');
}
const actions = getPhoneAddDialogActions();
syncImagesToFormData();
await actions.save({ ...formData });
ElementPlus.ElMessage.success(formData.id ? '修改成功' : '新增成功');
close();
......@@ -146,9 +244,14 @@
visible,
isEdit,
formRef,
imageInputRef,
imageItems,
formData,
rules,
close,
triggerImageSelect,
handleImageFileChange,
removeImage,
handleSubmit
};
},
......@@ -204,6 +307,27 @@
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="图片字段">
<div style="width: 100%; display: flex; flex-direction: column; gap: 12px;">
<input ref="imageInputRef" type="file" accept="image/*" multiple style="display: none;" @change="handleImageFileChange" />
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
<el-button type="primary" plain @click="triggerImageSelect">选择图片</el-button>
<span style="font-size: 12px; color: #6b7280;">一个图片字段区域内最多展示 2 张图,保存时会拆回两个 SQL 字段。</span>
</div>
<div style="display: flex; gap: 12px; flex-wrap: wrap; min-height: 104px;">
<div v-for="(image, index) in imageItems" :key="image.id" style="position: relative; width: 104px; height: 104px; border-radius: 12px; overflow: hidden; border: 1px solid #e5e7eb; background: #f8fafc;">
<img :src="image.url" :alt="image.name" style="width: 100%; height: 100%; object-fit: cover; display: block;" />
<button type="button" @click="removeImage(index)" style="position: absolute; top: 6px; right: 6px; width: 24px; height: 24px; border: none; border-radius: 999px; background: rgba(17, 24, 39, 0.72); color: #ffffff; cursor: pointer; line-height: 1;">x</button>
</div>
<div v-if="!imageItems.length" style="width: 104px; height: 104px; border-radius: 12px; border: 1px dashed #cbd5e1; background: #f8fafc; display: flex; align-items: center; justify-content: center; color: #94a3b8; font-size: 12px; text-align: center; padding: 8px;">暂无图片<br/>占位图</div>
</div>
</div>
</el-form-item>
</el-col>
</el-row>
<el-divider border-style="dashed" style="margin: 16px 0;"></el-divider>
<div style="font-size: 15px; font-weight: 600; color: #303133; margin: 0 0 16px 0; line-height: 1;">状态与保留</div>
<el-row :gutter="24">
......
(function attachPhoneListRuntime() {
const PHONE_TABLE_COLUMN_WIDTHS = [
'48px',
'calc((100% - 48px) * 0.12)',
'calc((100% - 48px) * 0.10)',
'calc((100% - 48px) * 0.10)',
'calc((100% - 48px) * 0.10)',
'calc((100% - 48px) * 0.10)',
'calc((100% - 48px) * 0.10)',
'calc((100% - 48px) * 0.10)',
'calc((100% - 48px) * 0.16)',
'calc((100% - 48px) * 0.12)'
];
/**
* 代码作用(白话):把各种日期输入统一转成可比较的时间戳,方便手机号卡页面按更新时间筛选;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):表格记录.updatedAt -> toTimestamp() -> matchesDateRange() -> filteredRecords()
* 代码作用(白话):把各种日期输入统一转成可比较的时间戳,方便手机号卡页面按更新时间筛选;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):表格记录.updatedAt -> toTimestamp() -> matchesDateRange() -> filteredRecords。
*/
function toTimestamp(value) {
if (!value) {
......@@ -29,7 +16,7 @@
}
/**
* 代码作用(白话):判断一条手机号记录是否落在当前日期筛选范围内,没有选择日期时直接放行;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):filterDate -> matchesDateRange() -> filteredRecords() -> visibleRows()
* 代码作用(白话):判断一条手机号记录是否落在当前日期筛选范围内,没有选择日期时直接放行;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):filterDate -> matchesDateRange() -> filteredRecords() -> visibleRows。
*/
function matchesDateRange(record, rangeValue) {
if (!Array.isArray(rangeValue) || rangeValue.length !== 2) {
......@@ -63,7 +50,7 @@
notifyError: { type: Function, required: true }
},
/**
* 代码作用(白话):初始化手机号卡页面的分页、筛选、选中和操作入口,让壳层传入的数据与动作变成真正可交互的表格;关联文件: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) {
const { computed, ref } = window.Vue;
......@@ -153,10 +140,10 @@
record.realPerson,
record.city,
record.carrier,
record.project,
record.owner,
record.usageLocation,
record.iccid
record.iccid,
record.douyinAccount,
record.linkedWecom
]
.filter(Boolean)
.some((value) => String(value).toLowerCase().includes(query));
......@@ -225,6 +212,49 @@
}
/**
* 代码作用(白话):把真假值字段翻译成人能看懂的文案,避免列表里直接出现 true/false;关联文件:F:/Project/xyw_console/src/modules/shared/record-adapters.js;关联逻辑(调用链/消息链/数据流):normalizePhoneRecord() -> formatBooleanText() -> 手机号卡全字段列表列渲染。
*/
function formatBooleanText(value, trueText = '是', falseText = '否') {
return value ? trueText : falseText;
}
/**
* 代码作用(白话):把手机号状态和号码保留状态统一翻译成业务文案,方便列表按库表字段直观看;关联文件:F:/Project/xyw_console/src/modules/shared/record-adapters.js;关联逻辑(调用链/消息链/数据流):normalizePhoneRecord() -> formatStatusText()/formatRetentionText() -> 状态列渲染。
*/
function formatStatusText(value) {
return value ? '正常' : '异常';
}
/**
* 代码作用(白话):把号码保留布尔值翻译成“保留/注销”,避免业务同学自己推断 0 和 1;关联文件:F:/Project/xyw_console/src/modules/shared/record-adapters.js;关联逻辑(调用链/消息链/数据流):normalizePhoneRecord() -> formatRetentionText() -> 号码保留状态列。
*/
function formatRetentionText(value) {
return value ? '保留' : '注销';
}
/**
* 代码作用(白话):从统一图片集合里挑出当前记录应展示的最多两张缩略图,让两个 SQL 图片字段以前端一个图片列表现;关联文件:F:/Project/xyw_console/src/modules/shared/record-adapters.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):normalizePhoneRecord() -> getDisplayImages() -> 列表图片列双缩略图渲染。
*/
function getDisplayImages(record) {
return Array.isArray(record.images) ? record.images.slice(0, 2) : [];
}
/**
* 代码作用(白话):给没有图片或图片打不开的情况显示统一占位块,避免列表出现空洞或破图;关联文件:F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):getDisplayImages() -> handleImageError() -> 图片列占位图回退。
*/
function handleImageError(event) {
const image = event?.target;
if (!image) {
return;
}
image.style.display = 'none';
const placeholder = image.nextElementSibling;
if (placeholder) {
placeholder.style.display = 'flex';
}
}
/**
* 代码作用(白话):把某一行设为当前选中手机号,并同步到壳层全局状态,方便详情抽屉和编辑弹窗拿到当前对象;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js;关联逻辑(调用链/消息链/数据流):点击表格手机号/操作按钮 -> focusRow() -> globalState.selectedBySource.phone -> openDrawer()/详情联动。
*/
function focusRow(recordId) {
......@@ -315,7 +345,6 @@
syncMountState();
return {
PHONE_TABLE_COLUMN_WIDTHS,
carrierOptions,
outboundOptions,
currentPage,
......@@ -324,6 +353,11 @@
checkedIds,
isRowSelected,
getStatusClass,
formatBooleanText,
formatStatusText,
formatRetentionText,
getDisplayImages,
handleImageError,
focusRow,
resetTableState,
handleRowAction,
......@@ -399,33 +433,31 @@
class="domain-table domain-table-refined"
@selection-change="handleSelectionChange"
:header-cell-style="{ background: '#fafafa', color: '#4b5563', fontWeight: 500, padding: '12px 20px', borderBottom: '1px solid #f3f4f6' }"
:cell-style="{ padding: '12px 20px', borderBottom: '1px solid #f3f4f6', color: '#111827' }"
:cell-style="{ padding: '12px 20px', borderBottom: '1px solid #f3f4f6', color: '#111827', whiteSpace: 'nowrap' }"
>
<el-table-column type="selection" width="50" />
<el-table-column label="手机号" min-width="140">
<template #default="scope">
<a href="#" class="domain-link" @click.prevent="focusRow(scope.row.id)">{{ scope.row.phone }}</a>
<div class="cell-sub">{{ scope.row.project || '-' }}</div>
</template>
</el-table-column>
<el-table-column label="实名人" min-width="120">
<el-table-column type="selection" width="50" fixed="left" />
<el-table-column label="图片" min-width="160">
<template #default="scope">
<div class="date-stack">
<span>{{ scope.row.realPerson }}</span>
<div class="cell-sub">{{ scope.row.owner || '-' }}</div>
<div style="display: flex; gap: 8px; align-items: center; min-height: 52px;">
<template v-if="getDisplayImages(scope.row).length">
<div v-for="(image, imageIndex) in getDisplayImages(scope.row)" :key="scope.row.id + '-image-' + imageIndex" style="width: 52px; height: 52px; border-radius: 10px; overflow: hidden; border: 1px solid #e5e7eb; background: #f8fafc; position: relative; flex: 0 0 auto;">
<img :src="image" alt="号卡图片缩略图" style="width: 100%; height: 100%; object-fit: cover; display: block;" @error="handleImageError" />
<div style="display: none; align-items: center; justify-content: center; width: 100%; height: 100%; font-size: 11px; color: #94a3b8; background: #f8fafc;">占位图</div>
</div>
</template>
<div v-else style="width: 52px; height: 52px; border-radius: 10px; border: 1px dashed #cbd5e1; background: #f8fafc; display: flex; align-items: center; justify-content: center; font-size: 11px; color: #94a3b8; flex: 0 0 auto;">占位图</div>
</div>
</template>
</el-table-column>
<el-table-column label="归属地" min-width="120">
<el-table-column label="手机号" min-width="150">
<template #default="scope">
<div class="date-stack">
<span>{{ scope.row.city }}</span>
<div class="cell-sub">{{ scope.row.usageLocation || '-' }}</div>
</div>
<a href="#" class="domain-link" @click.prevent="focusRow(scope.row.id)">{{ scope.row.phone || '-' }}</a>
</template>
</el-table-column>
<el-table-column property="carrier" label="运营商" min-width="100" />
<el-table-column label="号卡状态" min-width="100">
<el-table-column property="realPerson" label="真实联系人" min-width="130" />
<el-table-column property="iccid" label="ICCID" min-width="190" />
<el-table-column property="city" label="所在城市" min-width="120" />
<el-table-column label="号卡状态" min-width="120">
<template #default="scope">
<span class="status-chip-wrap">
<span class="status-dot" :class="getStatusClass(scope.row.status)"></span>
......@@ -433,29 +465,35 @@
</span>
</template>
</el-table-column>
<el-table-column label="微信开通" min-width="100">
<template #default="scope">
<span class="status-chip-wrap">
<span class="status-dot" :class="getStatusClass(scope.row.wechatStatus)"></span>
<span>{{ scope.row.wechatStatus }}</span>
</span>
</template>
<el-table-column property="cardUsageLocation" label="卡使用地点" min-width="160" />
<el-table-column label="企业微信" min-width="110">
<template #default="scope">{{ formatBooleanText(scope.row.wecom, '已开通', '未开通') }}</template>
</el-table-column>
<el-table-column label="外呼能力" min-width="100">
<template #default="scope">
<span class="status-chip-wrap">
<span class="status-dot" :class="getStatusClass(scope.row.outbound)"></span>
<span>{{ scope.row.outbound }}</span>
</span>
</template>
<el-table-column label="微信" min-width="100">
<template #default="scope">{{ formatBooleanText(scope.row.wechat, '已开通', '未开通') }}</template>
</el-table-column>
<el-table-column label="可外呼" min-width="100">
<template #default="scope">{{ formatBooleanText(scope.row.outboundCall, '可外呼', '不可外呼') }}</template>
</el-table-column>
<el-table-column property="douyinAccount" label="抖音账号" min-width="160" />
<el-table-column label="小程序备案" min-width="120">
<template #default="scope">{{ formatBooleanText(scope.row.miniProgramFiling, '已备案', '未备案') }}</template>
</el-table-column>
<el-table-column property="channelOperator" label="渠道运营商" min-width="130" />
<el-table-column label="手机号状态" min-width="120">
<template #default="scope">{{ formatStatusText(scope.row.numberStatus) }}</template>
</el-table-column>
<el-table-column property="updatedAt" label="更新时间" min-width="160" />
<el-table-column label="操作" width="160" fixed="right">
<el-table-column property="linkedWecom" label="绑定企业微信" min-width="160" />
<el-table-column label="号码保留状态" min-width="130">
<template #default="scope">{{ formatRetentionText(scope.row.numberRetentionStatus) }}</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="scope">
<div class="action-cluster" style="display: flex; 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: 4px; font-weight: 500;">查看</button>
<button class="action-pill" type="button" @click.stop="handleRowAction('edit', scope.row.id)" style="border: none; background: transparent; color: #4b5563; cursor: pointer; padding: 4px;">编辑</button>
<button class="action-pill" type="button" @click.stop="handleRowAction('delete', scope.row.id)" style="border: none; background: transparent; color: #ef4444; cursor: pointer; padding: 4px;">删除</button>
<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" type="button" @click.stop="handleRowAction('edit', scope.row.id)" style="border: none; background: transparent; color: #4b5563; cursor: pointer; padding: 0; font-weight: 500; line-height: 1;">编辑</button>
<button class="action-pill" type="button" @click.stop="handleRowAction('delete', scope.row.id)" style="border: none; background: transparent; color: #ef4444; cursor: pointer; padding: 0; font-weight: 500; line-height: 1;">删除</button>
</div>
</template>
</el-table-column>
......@@ -502,3 +540,7 @@
mountPhoneListComponent
};
})();
......@@ -55,48 +55,105 @@
}
/**
* 代码作用(白话):把手机号真假值统一转成布尔值,避免不同接口口径导致列表和弹窗判断不一致;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):后端字段原值 -> toBooleanFlag() -> normalizePhoneRecord() -> 列表显示/弹窗回填。
*/
function toBooleanFlag(value, defaultValue) {
if (value === null || value === undefined || value === '') {
return defaultValue;
}
if (value === true || value === 1 || value === '1' || value === 'true' || value === 'enabled') {
return true;
}
if (value === false || value === 0 || value === '0' || value === 'false' || value === 'disabled') {
return false;
}
return Boolean(value);
}
/**
* 代码作用(白话):把两个 SQL 图片字段整理成前端统一图片集合,让列表和弹窗都按一个图片字段来理解;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):image_attachment_1/image_attachment_2 -> buildPhoneImages() -> normalizePhoneRecord() -> 图片列/图片上传区域。
*/
function buildPhoneImages(source) {
return [source.imageAttachment1, source.image_attachment_1, source.imageAttachment2, source.image_attachment_2]
.filter(Boolean)
.filter((value, index, array) => array.indexOf(value) === index)
.slice(0, 2);
}
/**
* 代码作用(白话):把手机号原始记录整理成列表、详情抽屉、编辑弹窗共用的统一字段,避免每个组件自己猜字段名;关联文件: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 注入 -> 手机号列表/详情/编辑弹窗。
*/
function normalizePhoneRecord(row) {
const source = row || {};
const cardStatus = source.cardStatus || source.status || '正常';
const phoneNumber = source.phoneNumber || source.phone || '';
const channelOperator = source.channelOperator || source.carrier || '';
const updatedAt = source.updatedAt || source.updateTime || '';
const usageLocation = source.cardUsageLocation || source.usageLocation || '';
const wechatStatus = source.wechatStatus || (source.numberStatus === false ? '异常停机' : '正常');
const outbound = source.outbound || (source.outboundCall ? '可外呼' : '不可外呼');
const images = buildPhoneImages(source);
const cardStatusRaw = source.cardStatus ?? source.card_status ?? source.status;
const cardStatus = cardStatusRaw === 0 || cardStatusRaw === '0' || cardStatusRaw === '异常' ? '异常停机' : (cardStatusRaw || '正常');
const phoneNumber = source.phoneNumber || source.phone_number || source.phone || '';
const realPerson = source.realPerson || source.real_person || '';
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 wechatEnabled = toBooleanFlag(source.wechat, false);
const outboundEnabled = toBooleanFlag(source.outboundCall ?? source.outbound_call ?? source.outbound, false);
const miniProgramFiled = toBooleanFlag(source.miniProgramFiling ?? source.mini_program_filing, false);
const packageChangeEnabled = toBooleanFlag(source.packageChange5yuan ?? source.package_change_5yuan, false);
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 outbound = source.outbound || (outboundEnabled ? '可外呼' : '不可外呼');
const linkedWecom = source.linkedWecom || source.linked_wecom || '';
const douyinAccount = source.douyinAccount || source.douyin_account || '';
return {
...source,
id: source.id,
images,
primaryImage: images[0] || '',
imageAttachment1: 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 || '',
image_attachment_2: images[1] || source.imageAttachment2 || source.image_attachment_2 || '',
phone: phoneNumber,
phoneNumber,
realPerson: source.realPerson || '',
phone_number: phoneNumber,
realPerson,
real_person: realPerson,
city: source.city || '',
usageLocation,
cardUsageLocation: usageLocation,
card_usage_location: usageLocation,
carrier: channelOperator,
channelOperator,
channel_operator: channelOperator,
status: cardStatus,
cardStatus,
card_status: cardStatus,
wechatStatus,
outbound,
outboundCall: source.outboundCall === true || source.outbound === 'available' || source.outbound === '可外呼',
wechat: source.wechat === true || source.wechat === 'enabled',
wecom: source.wecom === true || source.wecom === 'enabled',
outboundCall: outboundEnabled,
outbound_call: outboundEnabled,
wechat: wechatEnabled,
wecom: wecomEnabled,
updatedAt,
updated_at: source.updated_at || updatedAt,
updateTime: source.updateTime || updatedAt,
owner: source.owner || source.realPerson || '',
owner: source.owner || realPerson,
project: source.project || source.note || '暂无备注',
note: source.note || source.project || '',
iccid: source.iccid || '',
douyinAccount: source.douyinAccount || '',
miniProgramFiling: source.miniProgramFiling === true,
packageChange5yuan: source.packageChange5yuan === true,
numberStatus: source.numberStatus !== false,
linkedWecom: source.linkedWecom || '',
numberRetentionStatus: source.numberRetentionStatus !== false
douyinAccount,
douyin_account: douyinAccount,
miniProgramFiling: miniProgramFiled,
mini_program_filing: miniProgramFiled,
packageChange5yuan: packageChangeEnabled,
package_change_5yuan: packageChangeEnabled,
numberStatus,
mobile_status: numberStatus,
linkedWecom,
linked_wecom: linkedWecom,
numberRetentionStatus,
number_retention_status: numberRetentionStatus
};
}
......
@echo off
chcp 65001 >nul
set "BACKEND_DIR=%~dp0backend"
set "PROJECT_JAVA_HOME=F:\jdk-17.0.12_windows-x64_bin\jdk-17.0.12"
set "XYW_DB_USERNAME=root"
set "XYW_DB_PASSWORD=root"
echo ===================================================
echo [Starting Backend]
echo Backend Dir: %BACKEND_DIR%
echo Backend URL: http://localhost:8080
echo JAVA_HOME: %PROJECT_JAVA_HOME%
echo DB User: %XYW_DB_USERNAME%
echo ===================================================
echo.
if not exist "%BACKEND_DIR%\pom.xml" (
echo [Error] Backend pom.xml not found: %BACKEND_DIR%\pom.xml
pause
exit /b 1
)
if not exist "%PROJECT_JAVA_HOME%\bin\java.exe" (
echo [Error] java.exe not found: %PROJECT_JAVA_HOME%\bin\java.exe
pause
exit /b 1
)
if not exist "%PROJECT_JAVA_HOME%\bin\javac.exe" (
echo [Error] javac.exe not found: %PROJECT_JAVA_HOME%\bin\javac.exe
pause
exit /b 1
)
set "JAVA_HOME=%PROJECT_JAVA_HOME%"
set "PATH=%JAVA_HOME%\bin;%PATH%"
java -version
cd /d "%BACKEND_DIR%"
echo (Press Ctrl+C or close this window to stop backend)
mvn spring-boot:run
\ No newline at end of file
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