Commit 1b367d0d by DaiJiezhang

Refine phone list workspace

parent 00d54727
......@@ -539,6 +539,7 @@ function renderExternalRuntime(moduleKey) {
globalState: state,
loadPhoneRows,
deletePhoneRecord,
savePhoneRecord,
openAddDialog: (mode, record) => {
window.PhoneAddDialogAPI?.open?.(mode, record);
},
......
......@@ -72,4 +72,17 @@ public class PhoneMybatisConfig {
public SqlSessionTemplate phoneSqlSessionTemplate(@Qualifier("phoneSqlSessionFactory") SqlSessionFactory phoneSqlSessionFactory) {
return new SqlSessionTemplate(phoneSqlSessionFactory);
}
@Bean
public org.springframework.boot.ApplicationRunner phoneDatabaseInitializer(@Qualifier("phoneDataSource") DataSource phoneDataSource) {
return args -> {
try (java.sql.Connection conn = phoneDataSource.getConnection();
java.sql.Statement stmt = conn.createStatement()) {
stmt.execute("ALTER TABLE wx_phone MODIFY COLUMN image_attachment_1 LONGTEXT");
stmt.execute("ALTER TABLE wx_phone MODIFY COLUMN image_attachment_2 LONGTEXT");
} catch (Exception e) {
e.printStackTrace();
}
};
}
}
......@@ -91,6 +91,7 @@
<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/shared/record-adapters.js"></script>
<script src="src/modules/shared/wechat-api-client.js"></script>
......
......@@ -289,7 +289,7 @@
<el-option label="中国联通" value="中国联通"></el-option>
<el-option label="中国电信" value="中国电信"></el-option>
<el-option label="中国广电" value="中国广电"></el-option>
<el-option label="虚商" value="虚商"></el-option>
<el-option label="亮哥渠道(虚拟)" value="亮哥渠道(虚拟)"></el-option>
</el-select>
</el-form-item>
</el-col>
......
(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;
}
image.style.display = 'none';
const placeholder = image.nextElementSibling;
if (placeholder) {
placeholder.style.display = 'flex';
}
}
const PhoneCardImageCell = {
name: 'PhoneCardImageCell',
props: {
images: { type: Array, default: () => [] },
recordId: { type: [String, Number], default: null },
clearImage: { type: Function, default: null }
},
setup(props) {
const previewVisible = ref(false);
const previewIndex = ref(0);
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);
}
}
return {
previewVisible,
previewIndex,
openPreview,
closePreview,
handleDelete,
handleImageError
};
},
template: `
<div class="phone-image-cell">
<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 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>
</div>
</div>
</template>
<div v-else class="phone-image-thumb phone-image-thumb--empty">
<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"
/>
</div>
`
};
window.PhoneCardImageCell = PhoneCardImageCell;
})();
......@@ -55,6 +55,7 @@
globalState: { type: Object, required: true },
loadPhoneRows: { type: Function, required: true },
deletePhoneRecord: { type: Function, required: true },
savePhoneRecord: { type: Function, required: true },
openAddDialog: { type: Function, required: true },
openDrawer: { type: Function, required: true },
getPhoneStatusDotClass: { type: Function, required: true },
......@@ -316,6 +317,52 @@
}
/**
* 代码作用(白话):按后端 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() -> 后端全量更新。
*/
function buildSavePayload(record, images) {
return {
id: record.id,
phoneNumber: record.phoneNumber || record.phone || '',
realPerson: record.realPerson || '',
iccid: record.iccid || '',
city: record.city || '',
imageAttachment1: images[0] || '',
imageAttachment2: images[1] || '',
cardStatus: record.cardStatus || record.status || '正常',
cardUsageLocation: record.cardUsageLocation || record.usageLocation || '',
wecom: record.wecom === true,
wechat: record.wechat === true,
outboundCall: record.outboundCall === true,
douyinAccount: record.douyinAccount || '',
miniProgramFiling: record.miniProgramFiling === true,
packageChange5yuan: record.packageChange5yuan === true,
channelOperator: record.channelOperator || record.carrier || '',
numberStatus: record.numberStatus ?? true,
linkedWecom: record.linkedWecom || ''
};
}
/**
* 代码作用(白话):删除列表图片列中指定位置的单张图片,复用现有保存逻辑把对应 imageAttachment1/2 置空并刷新列表;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-image-cell.js;关联逻辑(调用链/消息链/数据流):图片单元删除 icon -> handleClearImage() -> buildSavePayload() -> props.savePhoneRecord() -> loadAllRecords()。
*/
async function handleClearImage(record, imageIndex) {
if (!window.confirm('确认删除这张图片吗?')) {
return;
}
const remaining = getDisplayImages(record).filter((_, idx) => idx !== imageIndex);
const payload = buildSavePayload(record, remaining);
try {
await props.savePhoneRecord(payload);
if (window.ElementPlus?.ElMessage) {
window.ElementPlus.ElMessage.success('图片已删除');
}
await loadAllRecords();
} catch (error) {
props.notifyError(error.message || '删除图片失败,请稍后重试');
}
}
/**
* 代码作用(白话):把某一行设为当前选中手机号,并同步到壳层全局状态,方便详情抽屉和编辑弹窗拿到当前对象;关联文件: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) {
......@@ -425,6 +472,7 @@
formatRetentionText,
getDisplayImages,
handleImageError,
handleClearImage,
focusRow,
resetTableState,
handleRowAction,
......@@ -523,23 +571,19 @@
:data="visibleRows"
row-key="id"
style="width: 100%"
class="domain-table domain-table-refined"
class="domain-table domain-table-refined phone-table-header-nowrap"
@selection-change="handleSelectionChange"
:header-cell-style="{ background: 'linear-gradient(180deg, #f9fafb 0%, #f3f4f6 100%)', color: '#374151', fontWeight: 600, padding: '14px 20px', borderBottom: '1px solid #e5e7eb' }"
:cell-style="{ padding: '14px 20px', borderBottom: '1px solid #f3f4f6', color: '#111827', whiteSpace: 'nowrap' }"
:header-cell-style="{ background: 'linear-gradient(180deg, #f9fafb 0%, #f3f4f6 100%)', color: '#374151', fontWeight: 600, padding: '10px 12px', borderBottom: '1px solid #e5e7eb', whiteSpace: 'nowrap' }"
:cell-style="{ padding: '10px 12px', borderBottom: '1px solid #f3f4f6', color: '#111827', whiteSpace: 'nowrap' }"
>
<el-table-column type="selection" width="50" fixed="left" />
<el-table-column label="图片" min-width="160">
<template #default="scope">
<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: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); display: flex; align-items: center; justify-content: center; font-size: 11px; color: #94a3b8; flex: 0 0 auto;">占位图</div>
</div>
<phone-card-image-cell
:images="getDisplayImages(scope.row)"
:record-id="scope.row.id"
:clear-image="(imageIndex) => handleClearImage(scope.row, imageIndex)"
/>
</template>
</el-table-column>
<el-table-column label="手机号" min-width="150">
......@@ -549,18 +593,18 @@
</el-table-column>
<el-table-column label="实名人" min-width="150">
<template #default="scope">
<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>
<span :title="scope.row.realPerson || '-'" style="white-space: nowrap;">{{ 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>
<span :title="scope.row.iccid || '-'" style="white-space: nowrap;">{{ 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>
<span :title="scope.row.city || '-'" style="white-space: nowrap;">{{ scope.row.city || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="号卡状态" min-width="120">
......@@ -573,7 +617,7 @@
</el-table-column> <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">
<el-table-column label="微信" min-width="90">
<template #default="scope">{{ formatBooleanText(scope.row.wechat, '已开通', '未开通') }}</template>
</el-table-column>
<el-table-column label="可外呼" min-width="100">
......@@ -581,24 +625,24 @@
</el-table-column>
<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>
<span :title="scope.row.douyinAccount || '-'" style="white-space: nowrap;">{{ scope.row.douyinAccount || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="小程序备案" min-width="120">
<template #default="scope">{{ formatBooleanText(scope.row.miniProgramFiling, '已备案', '未备案') }}</template>
</el-table-column>
<el-table-column label="渠道运营商" min-width="150">
<el-table-column label="渠道运营商" min-width="170">
<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>
<span :title="scope.row.channelOperator || '-'" style="white-space: nowrap;">{{ scope.row.channelOperator || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="号码状态" min-width="120">
<template #default="scope">{{ formatStatusText(scope.row.numberStatus ?? scope.row.mobile_status ?? scope.row.number_status) }}</template>
</el-table-column>
<el-table-column label="绑定企业微信" min-width="180">
<el-table-column label="绑定企业微信" min-width="260">
<template #default="scope">
<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>
<span :title="scope.row.linkedWecom || '-'" style="white-space: nowrap;">{{ scope.row.linkedWecom || '-' }}</span>
</template>
</el-table-column> <el-table-column label="操作" width="100" fixed="right">
<template #default="scope">
......@@ -644,6 +688,9 @@
if (window.ElementPlus) {
app.use(window.ElementPlus);
}
if (window.PhoneCardImageCell) {
app.component('PhoneCardImageCell', window.PhoneCardImageCell);
}
app.mount(options.mountTarget);
return app;
}
......
/* Import original styles first */
/* Import original styles first */
@import url('styles.css');
/* Overrides for right side replication */
......@@ -1229,3 +1229,103 @@
.phone-date-range .el-range-separator {
padding: 0 2px !important;
}
.phone-table-header-nowrap .cell {
white-space: nowrap !important;
text-overflow: clip !important;
padding: 0 8px !important;
}
/* Phone card image column cell: thumbnail + hover preview/delete actions */
.phone-image-cell {
display: flex;
gap: 8px;
align-items: center;
min-height: 52px;
}
.phone-image-thumb {
position: relative;
width: 52px;
height: 52px;
border-radius: 10px;
overflow: hidden;
border: 1px solid #e5e7eb;
background: #f8fafc;
flex: 0 0 auto;
}
.phone-image-thumb--empty {
border: 1px dashed #cbd5e1;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
}
.phone-image-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.phone-image-fallback {
display: none;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
font-size: 11px;
color: #94a3b8;
background: #f8fafc;
}
.phone-image-thumb--empty .phone-image-fallback {
display: flex;
}
.phone-image-actions {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
background: rgba(17, 24, 39, 0.5);
opacity: 0;
transition: opacity 0.18s ease;
}
.phone-image-thumb:hover .phone-image-actions {
opacity: 1;
}
.phone-image-action {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
padding: 0;
border: none;
border-radius: 6px;
background: transparent;
color: #bfbfbf;
cursor: pointer;
line-height: 1;
transition: color 0.15s ease, background 0.15s ease;
}
.phone-image-action svg {
width: 16px;
height: 16px;
display: block;
}
.phone-image-action--preview:hover {
color: #2563eb;
background: rgba(255, 255, 255, 0.18);
}
.phone-image-action--delete:hover {
color: #ef4444;
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