Commit 97ff58ad by DaiJiezhang

feat: 完善企微新增号码信息

parent ea21e468
package com.xyw.console.asset.controller;
import com.xyw.console.asset.exception.PhoneAssetNotFoundException;
import com.xyw.console.asset.exception.PhoneAssetValidationException;
import com.xyw.console.common.ApiResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/** 文件用途(白话):把手机号码管理的业务异常转换成前端可读的统一 JSON 错误。 */
@RestControllerAdvice(assignableTypes = PhoneAssetController.class)
public class PhoneAssetExceptionHandler {
/** 代码作用(白话):把手机号格式、ICCID 和实名人必填等校验失败转为 400。关联文件:PhoneAssetService.java。关联逻辑(调用链/数据流):Service 校验异常 -> Advice -> ApiResponse -> 弹窗提示。 */
@ExceptionHandler(PhoneAssetValidationException.class) @ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Void> validation(PhoneAssetValidationException exception) { return ApiResponse.error(400, exception.getMessage()); }
/** 代码作用(白话):把不存在的手机号码管理转为 404。关联文件:PhoneAssetService.java。关联逻辑(调用链/数据流):Service 查无记录 -> Advice -> ApiResponse -> 前端提示。 */
@ExceptionHandler(PhoneAssetNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND)
public ApiResponse<Void> notFound(PhoneAssetNotFoundException exception) { return ApiResponse.error(404, exception.getMessage()); }
}
......@@ -57,6 +57,10 @@ public class WecomAccountController {
@GetMapping("/lookups/phone-assets")
public ApiResponse<List<PhoneAssetLookupResponse>> phoneAssets(@RequestParam(defaultValue = "") String keyword) { permissions.require(PagePermissionService.WECOM, "READ"); return ApiResponse.success(service.searchPhoneAssets(keyword)); }
/** 代码作用(白话):查询注册手机号在手机号码管理中是否已存在,供企微表单决定是否展开新增手机号区块。关联文件:WecomAccountService.java、WecomAccountView.js。关联逻辑(调用链/数据流):手机号输入 -> GET phone-exists -> 精确查询 -> 布尔结果。 */
@GetMapping("/lookups/phone-exists")
public ApiResponse<Boolean> phoneExists(@RequestParam(defaultValue = "") String phoneNumber) { permissions.require(PagePermissionService.WECOM, "READ"); return ApiResponse.success(service.phoneExists(phoneNumber)); }
/** 代码作用(白话):按名称搜索可关联的有效设备,供表单下拉远程查询。关联文件:DeviceAssetLookupResponse.java、WecomAccountService.java。关联逻辑(调用链/数据流):下拉输入 -> GET lookup -> 设备选项。 */
@GetMapping("/lookups/devices")
public ApiResponse<List<DeviceAssetLookupResponse>> devices(@RequestParam(defaultValue = "") String keyword) { permissions.require(PagePermissionService.WECOM, "READ"); return ApiResponse.success(service.searchDevices(keyword)); }
......
......@@ -2,7 +2,7 @@ package com.xyw.console.asset.dto;
import jakarta.validation.constraints.NotBlank;
/** ???????????????????????? Controller ??????????? */
/** 代码作用(白话):限定新增和编辑时浏览器可以提交的企业微信资产字段,并携带注册手机号为新增号码时所需的手机号信息。关联文件:WecomAccountController.java、WecomAccountService.java。关联逻辑(调用链/数据流):弹窗表单 -> DTO -> Service 写入允许修改的实体字段。 */
public record WecomAccountSaveRequest(
@NotBlank String wecomName,
String wecomAlias,
......@@ -13,5 +13,8 @@ public record WecomAccountSaveRequest(
String realNameOwnerStatus,
String gender,
Long deviceId,
Long operatorPersonId) {
Long operatorPersonId,
String cardType,
String iccid,
String phoneRealNameOwner) {
}
......@@ -69,7 +69,9 @@ public class PhoneAssetService {
/** 代码作用(白话):读取一条仍有效的资产,供编辑和删除共用。关联文件:PhoneAssetMapper.java、PhoneAssetExceptionHandler.java。关联逻辑(调用链/数据流):Service 查询 -> 无记录 -> 404 异常。 */
private PhoneAssetEntity requireActiveEntity(Long id){ PhoneAssetEntity entity=mapper.selectOne(new LambdaQueryWrapper<PhoneAssetEntity>().eq(PhoneAssetEntity::getDeleteTime,0L).eq(PhoneAssetEntity::getId,id)); if(entity==null) throw new PhoneAssetNotFoundException("手机号码管理不存在或已删除"); return entity; }
/** 代码作用(白话):把表单允许提交的字段写入实体,并统一处理手机号和默认使用状态。关联文件:PhoneAssetSaveRequest.java、PhoneAssetEntity.java。关联逻辑(调用链/数据流):弹窗表单 -> DTO -> Entity -> Mapper 保存。 */
private void applyEditableFields(PhoneAssetEntity entity, PhoneAssetSaveRequest request){ entity.setPhoneNumber(normalizePhoneNumber(request.phoneNumber())); entity.setCardType(request.cardType()); entity.setIccid(request.iccid()); entity.setRealNameOwner(request.realNameOwner()); entity.setManagementType(request.managementType()); entity.setDisposalStatus(request.disposalStatus()==null||request.disposalStatus().isBlank()?"正常使用":request.disposalStatus()); entity.setDeviceId(request.deviceId()); }
private void applyEditableFields(PhoneAssetEntity entity, PhoneAssetSaveRequest request){ validateRequiredIdentityFields(request); entity.setPhoneNumber(normalizePhoneNumber(request.phoneNumber())); entity.setCardType(request.cardType()); entity.setIccid(request.iccid()); entity.setRealNameOwner(request.realNameOwner()); entity.setManagementType(request.managementType()); entity.setDisposalStatus(request.disposalStatus()==null||request.disposalStatus().isBlank()?"正常使用":request.disposalStatus()); entity.setDeviceId(request.deviceId()); }
/** 代码作用(白话):运营商为虚拟号码时实名人和 ICCID 可不填,其余运营商必须填写。关联文件:PhoneAssetSaveRequest.java、PhoneAssetValidationException.java。关联逻辑(调用链/数据流):DTO -> 校验 -> 失败抛 400 -> 前端保留弹窗并提示。 */
private void validateRequiredIdentityFields(PhoneAssetSaveRequest request){ if("虚拟号码".equals(request.cardType())) return; if(!hasText(request.iccid())) throw new PhoneAssetValidationException("ICCID 不能为空"); if(!hasText(request.realNameOwner())) throw new PhoneAssetValidationException("实名人不能为空"); }
/** 代码作用(白话):按已确认规则清理手机号并验证其最终格式。关联文件:PhoneAssetSaveRequest.java、PhoneAssetExceptionHandler.java。关联逻辑(调用链/数据流):前端输入 -> 去空格和 +86 -> 格式失败返回 400。 */
private String normalizePhoneNumber(String value){ String normalized=value==null?"":value.trim(); if(normalized.startsWith("+86")) normalized=normalized.substring(3); if(!normalized.matches("\\d{11}")) throw new PhoneAssetValidationException("手机号必须是 11 位数字"); return normalized; }
/** 代码作用(白话):只挑选列表需要展示的字段,并按传入的设备名映射补上关联设备名称。关联文件:PhoneAssetResponse.java、PhoneAssetEntity.java。关联逻辑(调用链/数据流):实体 + 设备名映射 -> DTO -> ApiResponse -> 表格。 */
......
......@@ -84,7 +84,7 @@ public class WecomAccountService {
PhoneAssetEntity phone = phoneAssetMapper.selectOne(new LambdaQueryWrapper<PhoneAssetEntity>().eq(PhoneAssetEntity::getDeleteTime, 0L).eq(PhoneAssetEntity::getPhoneNumber, phoneNumber));
boolean created = phone == null;
LocalDateTime now = LocalDateTime.now();
if (created) { phone = new PhoneAssetEntity(); phone.setPhoneNumber(phoneNumber); phone.setNumberType("EXTERNAL"); phone.setSourceAssetType("WECOM"); phone.setDisposalStatus("正常使用"); phone.setCreateTime(now); phone.setUpdateTime(now); phone.setDeleteTime(0L); phone.setLinkedWecomAccounts("[]"); phone.setLinkedWechatAccounts("[]"); phone.setLinkedDouyinAccounts("[]"); phone.setLinkedDomainAccounts("[]"); phone.setLinkedMerchants("[]"); if (phoneAssetMapper.insert(phone) != 1) throw new IllegalStateException("手机号码管理新增失败"); }
if (created) { phone = new PhoneAssetEntity(); phone.setPhoneNumber(phoneNumber); phone.setNumberType("EXTERNAL"); phone.setSourceAssetType("WECOM"); applyPhoneIdentity(phone, request); phone.setDisposalStatus("正常使用"); phone.setCreateTime(now); phone.setUpdateTime(now); phone.setDeleteTime(0L); phone.setLinkedWecomAccounts("[]"); phone.setLinkedWechatAccounts("[]"); phone.setLinkedDouyinAccounts("[]"); phone.setLinkedDomainAccounts("[]"); phone.setLinkedMerchants("[]"); if (phoneAssetMapper.insert(phone) != 1) throw new IllegalStateException("手机号码管理新增失败"); }
WecomAccountEntity entity = new WecomAccountEntity(); applyEditableFields(entity, request); entity.setPhoneAssetId(phone.getId()); entity.setPhoneLinkMode(created ? "CREATED" : "EXISTING"); entity.setCreateTime(now); entity.setUpdateTime(now); entity.setDeleteTime(0L); if (wecomAccountMapper.insert(entity) != 1) throw new IllegalStateException("企业微信资产新增失败"); if (created) { phone.setSourceAssetId(entity.getId()); phoneAssetMapper.updateById(phone); } return toResponse(entity, Map.of(), Map.of(phone.getId(), phone.getPhoneNumber()), Map.of(), Map.of());
}
......@@ -104,7 +104,7 @@ public class WecomAccountService {
LocalDateTime now = LocalDateTime.now();
// 号码没变就不动关联,避免把 EXISTING 误改成 CREATED、或把还在用的号码当成旧号清理掉。
if (!phoneNumber.equals(currentPhoneNumber(previousPhoneAssetId))) {
PhoneAssetEntity phone = attachPhone(phoneNumber, entity.getId(), now);
PhoneAssetEntity phone = attachPhone(phoneNumber, entity.getId(), now, request);
entity.setPhoneAssetId(phone.getId());
entity.setPhoneLinkMode(isCreatedFor(phone, entity.getId()) ? "CREATED" : "EXISTING");
releaseAutoCreatedPhone(previousPhoneAssetId, previousLinkMode, entity.getId(), now);
......@@ -149,11 +149,11 @@ public class WecomAccountService {
* 关联文件:PhoneAssetEntity.java、PhoneAssetMapper.java。
* 关联逻辑(调用链/数据流):号码 -> 查已有 -> 命中则复用,未命中则新建 EXTERNAL/WECOM 记录。
*/
private PhoneAssetEntity attachPhone(String phoneNumber, Long wecomAccountId, LocalDateTime now) {
private PhoneAssetEntity attachPhone(String phoneNumber, Long wecomAccountId, LocalDateTime now, WecomAccountSaveRequest request) {
PhoneAssetEntity phone = phoneAssetMapper.selectOne(new LambdaQueryWrapper<PhoneAssetEntity>().eq(PhoneAssetEntity::getDeleteTime, 0L).eq(PhoneAssetEntity::getPhoneNumber, phoneNumber));
if (phone != null) return phone;
phone = new PhoneAssetEntity();
phone.setPhoneNumber(phoneNumber); phone.setNumberType("EXTERNAL"); phone.setSourceAssetType("WECOM"); phone.setSourceAssetId(wecomAccountId); phone.setDisposalStatus("正常使用");
phone.setPhoneNumber(phoneNumber); phone.setNumberType("EXTERNAL"); phone.setSourceAssetType("WECOM"); phone.setSourceAssetId(wecomAccountId); applyPhoneIdentity(phone, request); phone.setDisposalStatus("正常使用");
phone.setCreateTime(now); phone.setUpdateTime(now); phone.setDeleteTime(0L);
phone.setLinkedWecomAccounts("[]"); phone.setLinkedWechatAccounts("[]"); phone.setLinkedDouyinAccounts("[]"); phone.setLinkedDomainAccounts("[]"); phone.setLinkedMerchants("[]");
if (phoneAssetMapper.insert(phone) != 1) throw new IllegalStateException("手机号码管理新增失败");
......@@ -201,6 +201,21 @@ public class WecomAccountService {
entity.setGender(request.gender()); entity.setDeviceId(request.deviceId()); entity.setOperatorPersonId(request.operatorPersonId());
}
/** 代码作用(白话):企微为新增注册手机号自动建号时,把运营商、ICCID 和手机号实名人写入号码资产,并套用与手机号码管理一致的必填规则(运营商为虚拟号码时 ICCID 和手机号实名人可不填)。关联文件:PhoneAssetEntity.java、PhoneAssetValidationException.java。关联逻辑(调用链/数据流):企微建号分支 -> 字段写入 -> 校验失败抛 400 -> 前端提示。 */
private void applyPhoneIdentity(PhoneAssetEntity phone, WecomAccountSaveRequest request) {
phone.setCardType(request.cardType());
phone.setIccid(request.iccid());
phone.setRealNameOwner(request.phoneRealNameOwner());
if ("虚拟号码".equals(request.cardType())) return;
if (!hasText(request.iccid())) throw new PhoneAssetValidationException("ICCID 不能为空");
if (!hasText(request.phoneRealNameOwner())) throw new PhoneAssetValidationException("手机号实名人不能为空");
}
/** 代码作用(白话):判断指定号码在手机号码管理中是否存在(未删除),供企微表单决定是否展开新增手机号区块。关联文件:WecomAccountController.java、PhoneAssetMapper.java。关联逻辑(调用链/数据流):手机号输入 -> GET phone-exists -> 精确查询 -> 布尔结果。 */
public boolean phoneExists(String phoneNumber) {
return phoneAssetMapper.selectCount(new LambdaQueryWrapper<PhoneAssetEntity>().eq(PhoneAssetEntity::getDeleteTime, 0L).eq(PhoneAssetEntity::getPhoneNumber, phoneNumber)) > 0;
}
/** 代码作用(白话):确认选中的关联设备仍然有效,阻止伪造或已删除的设备 ID 写入。关联文件:AssetDeviceMapper.java、WecomAccountSaveRequest.java。关联逻辑(调用链/数据流):表单 deviceId -> 有效性查询 -> 参数错误或继续保存。 */
private void requireActiveDevice(Long deviceId) {
if (deviceId == null) return;
......
......@@ -222,6 +222,15 @@ export default {
* 关联逻辑(调用链/数据流):确认保存 -> POST/PUT -> 成功提示 -> dialogVisible=false -> loadPage。
*/
async function submitForm() {
// 运营商为虚拟号码时实名人/ICCID 可不填,其余运营商必须填写
if (form.cardType !== '虚拟号码' && (!form.iccid || !form.iccid.trim())) {
ElMessage.warning('请填写 ICCID');
return;
}
if (form.cardType !== '虚拟号码' && (!form.realNameOwner || !form.realNameOwner.trim())) {
ElMessage.warning('请填写实名人');
return;
}
saving.value = true;
try {
const payload = { ...form, phoneNumber: normalizePhoneNumber(form.phoneNumber) };
......@@ -331,11 +340,11 @@ export default {
<template #suffix><span class="phone-asset-modal__character-count" aria-live="polite">{{ form.phoneNumber.length }}/11</span></template>
</el-input>
</el-form-item>
<el-form-item class="phone-asset-modal__form-row" label="实名人"><el-input v-model="form.realNameOwner" maxlength="20" placeholder="请输入实名人" /></el-form-item>
<el-form-item class="phone-asset-modal__form-row" label="实名人" :required="form.cardType !== '虚拟号码'"><el-input v-model="form.realNameOwner" maxlength="20" placeholder="请输入实名人" /></el-form-item>
<el-form-item class="phone-asset-modal__form-row" label="运营商">
<el-select v-model="form.cardType" clearable placeholder="请选择" popper-class="phone-asset-modal__select-popper"><el-option label="移动" value="移动" /><el-option label="电信" value="电信" /><el-option label="联通" value="联通" /><el-option label="电销卡「联通」" value="电销卡「联通」" /><el-option label="广电" value="广电" /><el-option label="虚拟号码" value="虚拟号码" /></el-select>
</el-form-item>
<el-form-item class="phone-asset-modal__form-row" label="ICCID">
<el-form-item class="phone-asset-modal__form-row" label="ICCID" :required="form.cardType !== '虚拟号码'">
<el-input v-model="form.iccid" class="phone-asset-modal__count-input" maxlength="20" autocomplete="off" placeholder="请输入 ICCID" @input="limitIccid" @paste="handleIccidPaste">
<template #suffix><span class="phone-asset-modal__character-count" aria-live="polite">{{ form.iccid.length }}/20</span></template>
</el-input>
......
import { computed, nextTick, onMounted, reactive, ref } from 'vue/dist/vue.esm-bundler.js';
import { ElMessage, ElMessageBox } from 'element-plus';
import { authState } from '../auth/auth-store.js';
import { createWecomAccount, deleteWecomAccount, listWecomAccounts, searchCompanyPersons, searchCompanyProfiles, searchDevices, searchPhoneAssets, updateWecomAccount } from './wecom-api-client.js';
import { createWecomAccount, checkPhoneExists, deleteWecomAccount, listWecomAccounts, searchCompanyPersons, searchCompanyProfiles, searchDevices, searchPhoneAssets, updateWecomAccount } from './wecom-api-client.js';
/** File purpose (plain language): renders the enterprise WeChat asset list and its creation dialog with reusable asset searches. */
export default {
......@@ -21,7 +21,10 @@ export default {
const canEdit = computed(() => authState.user?.pagePermissions?.['reference-wecom'] === 'EDIT');
let searchTimer;
const filters = reactive({ page: 1, size: 20, keyword: '', wecomAccount: '', phoneAssetId: new URLSearchParams(window.location.hash.split('?')[1] || '').get('phoneAssetId') || '', companyProfileId: 'ALL', realNameOwnerStatus: 'ALL' });
const form = reactive({ wecomName: '', wecomAlias: '记忆力梅老师-助教老师', wecomAccount: '', companyProfileId: null, phoneNumber: '', realNameOwner: '', realNameOwnerStatus: '在职', gender: '', deviceId: null, operatorPersonId: null });
const form = reactive({ wecomName: '', wecomAlias: '记忆力梅老师-助教老师', wecomAccount: '', companyProfileId: null, phoneNumber: '', realNameOwner: '', realNameOwnerStatus: '在职', gender: '', deviceId: null, operatorPersonId: null, cardType: '', iccid: '', phoneRealNameOwner: '' });
/** 代码作用(白话):注册手机号在手机号码管理中不存在时置为 true,弹窗展开"新增手机号"信息区块。关联文件:wecom-api-client.js、WecomAccountService.java。关联逻辑(调用链/数据流):手机号输入 -> checkPhoneExists -> isNewPhone -> 模板 v-if 区块。 */
const isNewPhone = ref(false);
let phoneCheckTimer = null;
/** Code purpose (plain language): reloads the table using the active filters. Related files: wecom-api-client.js, WecomAccountService.java. Data flow: page action -> GET -> records/total -> table. */
async function loadPage() {
......@@ -31,7 +34,7 @@ export default {
}
/** Code purpose (plain language): resets the create form to the business defaults. Related files: WecomAccountSaveRequest.java. Data flow: add button -> reset -> dialog form. */
function resetForm() { Object.assign(form, { wecomName: '', wecomAlias: '记忆力梅老师-助教老师', wecomAccount: '', companyProfileId: null, phoneNumber: '', realNameOwner: '', realNameOwnerStatus: '在职', gender: '', deviceId: null, operatorPersonId: null }); }
function resetForm() { Object.assign(form, { wecomName: '', wecomAlias: '记忆力梅老师-助教老师', wecomAccount: '', companyProfileId: null, phoneNumber: '', realNameOwner: '', realNameOwnerStatus: '在职', gender: '', deviceId: null, operatorPersonId: null, cardType: '', iccid: '', phoneRealNameOwner: '' }); isNewPhone.value = false; }
/**
* 代码作用(白话):在统一资产弹窗打开后把正文滚动位置恢复到顶部,保证标题、表单起点和底部操作区的使用体验一致。
......@@ -55,8 +58,10 @@ export default {
wecomName: row.wecomName || '', wecomAlias: row.wecomAlias || '', wecomAccount: row.wecomAccount || '',
companyProfileId: row.companyProfileId ?? null, phoneNumber: row.phoneNumber || '',
realNameOwner: row.realNameOwner || '', realNameOwnerStatus: row.realNameOwnerStatus || '在职',
gender: row.gender || '', deviceId: row.deviceId ?? null, operatorPersonId: row.operatorPersonId ?? null
gender: row.gender || '', deviceId: row.deviceId ?? null, operatorPersonId: row.operatorPersonId ?? null,
cardType: '', iccid: '', phoneRealNameOwner: ''
});
isNewPhone.value = false;
// 先用行数据兜底一条选项:三个远程下拉在还没搜索前会空白,回退显示当前值而不是让人以为没关联。
// 名称取不到时退回显示 ID(关联资产被软删就属于这种情况),也比一片空白好判断。
companyOptions.value = row.companyProfileId ? [{ id: row.companyProfileId, companyName: row.companyProfileName || `主体 ${row.companyProfileId}`, shortName: row.companyProfileName }] : [];
......@@ -85,13 +90,34 @@ export default {
}
/** 代码作用(白话):把注册手机号实时限制为最多 11 位数字,输入非数字或粘贴混合文字时自动清理;关联文件:PhoneAssetView.js、WecomAccountSaveRequest.java;关联逻辑(调用链/数据流):手机号输入框 -> 本方法 -> form.phoneNumber -> 保存请求 -> 后端格式校验。 */
function limitPhoneNumber(value) { form.phoneNumber = String(value || '').replace(/\D/g, '').slice(0, 11); }
function limitPhoneNumber(value) {
form.phoneNumber = String(value || '').replace(/\D/g, '').slice(0, 11);
schedulePhoneCheck();
}
/** 代码作用(白话):手机号输入完整 11 位后防抖查询是否已存在,决定是否展开"新增手机号"区块;号码不完整或已存在时隐藏并清空新增字段。关联文件:wecom-api-client.js、WecomAccountService.java。关联逻辑(调用链/数据流):输入停顿 -> checkPhoneExists -> isNewPhone -> 模板区块与提交校验。 */
function schedulePhoneCheck() {
window.clearTimeout(phoneCheckTimer);
const phoneNumber = form.phoneNumber;
if (phoneNumber.length !== 11) { isNewPhone.value = false; return; }
phoneCheckTimer = window.setTimeout(async () => {
try {
isNewPhone.value = !(await checkPhoneExists(phoneNumber));
if (!isNewPhone.value) Object.assign(form, { cardType: '', iccid: '', phoneRealNameOwner: '' });
} catch (_) { isNewPhone.value = false; }
}, 300);
}
/** Code purpose (plain language): saves the dialog as a create or an edit and refreshes the list. Related files: wecom-api-client.js, WecomAccountService.java. Data flow: dialog submit -> POST/PUT -> transaction -> table refresh. */
async function submitCreate() {
limitPhoneNumber(form.phoneNumber);
if (!form.wecomName.trim() || !form.phoneNumber) { ElMessage.warning('请填写企微名称和注册手机号'); return; }
if (form.phoneNumber.length !== 11) { ElMessage.warning('手机号必须是 11 位数字'); return; }
// 注册手机号是新增号码时,套用与手机号码管理一致的必填规则(运营商为虚拟号码时 ICCID、手机号实名人可不填)
if (isNewPhone.value && form.cardType !== '虚拟号码') {
if (!form.iccid || !form.iccid.trim()) { ElMessage.warning('请填写 ICCID'); return; }
if (!form.phoneRealNameOwner || !form.phoneRealNameOwner.trim()) { ElMessage.warning('请填写手机号实名人'); return; }
}
saving.value = true;
const payload = { ...form, wecomName: form.wecomName.trim(), phoneNumber: form.phoneNumber };
try {
......@@ -172,14 +198,14 @@ export default {
function deviceAssetLink(row) { return row.deviceName ? `#/device-assets?deviceName=${encodeURIComponent(row.deviceName)}` : ''; }
onMounted(loadPage);
return { canEdit, changePage, changePageSize, companyOptions, confirmDelete, deviceOptions, dialogVisible, editingId, deviceAssetLink, fetchPhoneSuggestions, filters, form, formatDate, formatRelation, genderClass, limitPhoneNumber, phoneAssetLink, loadCompanies, loadDevices, loadOwners, loading, openCreate, openEdit, ownerOptions, records, resetDialogScroll, resetSearch, restoreAllCompanyProfiles, restoreAllRealNameStatuses, saving, scheduleSearch, submitCreate, submitSearch, total };
return { canEdit, changePage, changePageSize, companyOptions, confirmDelete, deviceOptions, dialogVisible, editingId, deviceAssetLink, fetchPhoneSuggestions, filters, form, formatDate, formatRelation, genderClass, isNewPhone, limitPhoneNumber, phoneAssetLink, loadCompanies, loadDevices, loadOwners, loading, openCreate, openEdit, ownerOptions, records, resetDialogScroll, resetSearch, restoreAllCompanyProfiles, restoreAllRealNameStatuses, saving, scheduleSearch, submitCreate, submitSearch, total };
},
template: `
<section class="phone-asset-list-page wecom-account-page">
<header class="phone-asset-list-page__header"><div><h2>企业微信资产</h2></div><el-button v-if="canEdit" class="phone-asset-list-page__add" type="primary" @click="openCreate">新增企业微信资产</el-button></header>
<section class="phone-asset-list-page__panel phone-asset-list-page__search"><el-form class="phone-asset-list-page__filters" @submit.prevent="submitSearch"><el-input v-model="filters.keyword" placeholder="企微名称或手机号" clearable @input="scheduleSearch" @clear="scheduleSearch" /><el-select v-model="filters.companyProfileId" filterable remote clearable :remote-method="loadCompanies" placeholder="注册主体:" @change="submitSearch" @clear="restoreAllCompanyProfiles"><template #prefix>注册主体:</template><el-option label="全部" value="ALL" /><el-option v-for="item in companyOptions" :key="item.id" :label="item.shortName || item.companyName" :value="item.id" /></el-select><el-select v-model="filters.realNameOwnerStatus" clearable placeholder="实名状态:" @change="submitSearch" @clear="restoreAllRealNameStatuses"><template #prefix>实名状态:</template><el-option label="全部" value="ALL" /><el-option label="在职" value="在职" /><el-option label="离职" value="离职" /></el-select><el-button @click="resetSearch">重置</el-button></el-form></section>
<section class="phone-asset-list-page__panel phone-asset-list-page__table"><div class="phone-asset-list-page__grid-wrap"><el-table v-loading="loading" :data="records" empty-text="暂无匹配数据" class="phone-asset-list-page__grid wecom-account-page__grid"><el-table-column label="企微名称" min-width="200" show-overflow-tooltip><template #default="{ row }"><span class="wecom-account-page__name"><span class="wecom-account-page__name-main"><span class="wecom-account-page__name-text">{{ row.wecomName }}</span><span v-if="genderClass(row)" class="wecom-account-page__gender" :class="genderClass(row)" :title="'性别:' + row.gender"><svg v-if="row.gender === '男'" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="10" cy="14.5" r="5.5"/><path d="M14.5 10L20 4.5M15 4.5h5v5"/></svg><svg v-else viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="9" r="5.5"/><path d="M12 14.5v6M9 18h6"/></svg></span></span><span v-if="row.wecomAlias" class="wecom-account-page__name-alias">{{ row.wecomAlias }}</span></span></template></el-table-column><el-table-column prop="wecomAccount" label="企微账号" min-width="160" show-overflow-tooltip /><el-table-column label="注册主体" min-width="160" show-overflow-tooltip><template #default="{ row }">{{ formatRelation(row.companyProfileName) }}</template></el-table-column><el-table-column label="注册手机号" min-width="170"><template #default="{ row }"><span v-if="row.phoneNumber" class="wecom-account-page__phone"><a class="phone-asset-list-page__external-link" :href="phoneAssetLink(row)" :title="'在手机号码管理中查看 ' + row.phoneNumber">{{ row.phoneNumber }}</a><span class="wecom-account-page__phone-tag" :class="row.phoneLinkMode === 'CREATED' ? 'is-created' : 'is-existing'" :title="row.phoneLinkMode === 'CREATED' ? '新建号码:新增这条企微时自动创建' : '已有号码:关联的是已存在的手机号码管理'">{{ row.phoneLinkMode === 'CREATED' ? '新' : '已' }}</span></span><span v-else>—</span></template></el-table-column><el-table-column label="实名人" min-width="120"><template #default="{ row }"><span v-if="row.realNameOwner" class="wecom-account-page__owner"><span class="wecom-account-page__owner-main">{{ row.realNameOwner }}</span><i v-if="row.realNameOwnerStatus" class="wecom-account-page__owner-dot" :class="{ 'is-left': row.realNameOwnerStatus === '离职' }" :title="'实名状态:' + row.realNameOwnerStatus"></i></span><span v-else>—</span></template></el-table-column><el-table-column label="关联设备" min-width="160" show-overflow-tooltip><template #default="{ row }"><a v-if="deviceAssetLink(row)" class="phone-asset-list-page__external-link" :href="deviceAssetLink(row)" :title="'在设备资产管理中查看 ' + row.deviceName">{{ row.deviceName }}</a><span v-else>—</span></template></el-table-column><el-table-column label="企微号归属人" min-width="140" show-overflow-tooltip><template #default="{ row }">{{ formatRelation(row.operatorPersonName) }}</template></el-table-column><el-table-column label="创建时间" min-width="120"><template #default="{ row }">{{ formatDate(row.createTime) }}</template></el-table-column><el-table-column v-if="canEdit" label="操作" width="120" fixed="right"><template #default="{ row }"><span class="phone-asset-list-page__actions"><el-button link @click="openEdit(row)">编辑</el-button><el-button link type="danger" @click="confirmDelete(row)">删除</el-button></span></template></el-table-column></el-table></div><app-pagination :total="total" :page="filters.page" :size="filters.size" @update:page="changePage" @update:size="changePageSize" /></section>
<el-dialog v-model="dialogVisible" class="phone-asset-modal" modal-class="phone-asset-modal-mask" :title="editingId === null ? '新增企业微信资产' : '编辑企业微信资产'" width="560px" :close-on-click-modal="false" @opened="resetDialogScroll"><el-form class="phone-asset-modal__form" label-width="112px" @submit.prevent="submitCreate"><el-form-item class="phone-asset-modal__form-row" label="企微名称" required><el-input v-model="form.wecomName" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="企微别名"><el-input v-model="form.wecomAlias" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="企微账号"><el-input v-model="form.wecomAccount" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="注册手机号" required><el-autocomplete :model-value="form.phoneNumber" :fetch-suggestions="fetchPhoneSuggestions" maxlength="11" inputmode="numeric" placeholder="输入 11 位手机号" style="width:100%" @update:model-value="limitPhoneNumber"><template #suffix><span class="phone-asset-modal__character-count">{{ form.phoneNumber.length }}/11</span></template></el-autocomplete></el-form-item><el-form-item class="phone-asset-modal__form-row" label="注册主体"><el-select v-model="form.companyProfileId" filterable remote clearable :remote-method="loadCompanies" placeholder="输入公司名称或简称" style="width:100%"><el-option v-for="item in companyOptions" :key="item.id" :label="item.shortName || item.companyName" :value="item.id" /></el-select></el-form-item><el-form-item class="phone-asset-modal__form-row" label="关联设备"><el-select v-model="form.deviceId" filterable remote clearable :remote-method="loadDevices" placeholder="输入设备名称搜索" style="width:100%"><el-option v-for="item in deviceOptions" :key="item.id" :label="item.deviceName" :value="item.id" /></el-select></el-form-item><el-form-item class="phone-asset-modal__form-row" label="企微号归属人"><el-select v-model="form.operatorPersonId" filterable remote clearable :remote-method="loadOwners" placeholder="输入人员姓名" style="width:100%"><el-option v-for="item in ownerOptions" :key="item.id" :label="item.personName" :value="item.id" /></el-select></el-form-item><el-form-item class="phone-asset-modal__form-row" label="实名人"><el-input v-model="form.realNameOwner" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="实名状态"><el-radio-group v-model="form.realNameOwnerStatus"><el-radio value="在职">在职</el-radio><el-radio value="离职">离职</el-radio></el-radio-group></el-form-item><el-form-item class="phone-asset-modal__form-row" label="性别"><el-radio-group v-model="form.gender"><el-radio value="男">男</el-radio><el-radio value="女">女</el-radio></el-radio-group></el-form-item></el-form><template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="submitCreate">保存</el-button></template></el-dialog>
<el-dialog v-model="dialogVisible" class="phone-asset-modal" modal-class="phone-asset-modal-mask" :title="editingId === null ? '新增企业微信资产' : '编辑企业微信资产'" width="560px" :close-on-click-modal="false" @opened="resetDialogScroll"><el-form class="phone-asset-modal__form" label-width="112px" @submit.prevent="submitCreate"><el-form-item class="phone-asset-modal__form-row" label="企微名称" required><el-input v-model="form.wecomName" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="企微别名"><el-input v-model="form.wecomAlias" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="企微账号"><el-input v-model="form.wecomAccount" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="注册手机号" required><el-autocomplete :model-value="form.phoneNumber" :fetch-suggestions="fetchPhoneSuggestions" maxlength="11" inputmode="numeric" placeholder="输入 11 位手机号" style="width:100%" @update:model-value="limitPhoneNumber"><template #suffix><span class="phone-asset-modal__character-count">{{ form.phoneNumber.length }}/11</span></template></el-autocomplete></el-form-item><template v-if="isNewPhone"><div class="phone-asset-modal__form-divider">新增手机号</div><el-form-item class="phone-asset-modal__form-row" label="运营商"><el-select v-model="form.cardType" clearable placeholder="请选择" style="width:100%"><el-option label="移动" value="移动" /><el-option label="电信" value="电信" /><el-option label="联通" value="联通" /><el-option label="电销卡「联通」" value="电销卡「联通」" /><el-option label="广电" value="广电" /><el-option label="虚拟号码" value="虚拟号码" /></el-select></el-form-item><el-form-item class="phone-asset-modal__form-row" label="ICCID" :required="form.cardType !== '虚拟号码'"><el-input v-model="form.iccid" maxlength="20" placeholder="请输入 ICCID" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="手机号实名人" :required="form.cardType !== '虚拟号码'"><el-input v-model="form.phoneRealNameOwner" maxlength="20" placeholder="请输入手机号实名人" /></el-form-item></template><el-form-item class="phone-asset-modal__form-row" label="注册主体"><el-select v-model="form.companyProfileId" filterable remote clearable :remote-method="loadCompanies" placeholder="输入公司名称或简称" style="width:100%"><el-option v-for="item in companyOptions" :key="item.id" :label="item.shortName || item.companyName" :value="item.id" /></el-select></el-form-item><el-form-item class="phone-asset-modal__form-row" label="关联设备"><el-select v-model="form.deviceId" filterable remote clearable :remote-method="loadDevices" placeholder="输入设备名称搜索" style="width:100%"><el-option v-for="item in deviceOptions" :key="item.id" :label="item.deviceName" :value="item.id" /></el-select></el-form-item><el-form-item class="phone-asset-modal__form-row" label="企微号归属人"><el-select v-model="form.operatorPersonId" filterable remote clearable :remote-method="loadOwners" placeholder="输入人员姓名" style="width:100%"><el-option v-for="item in ownerOptions" :key="item.id" :label="item.personName" :value="item.id" /></el-select></el-form-item><el-form-item class="phone-asset-modal__form-row" label="实名人"><el-input v-model="form.realNameOwner" /></el-form-item><el-form-item class="phone-asset-modal__form-row" label="实名状态"><el-radio-group v-model="form.realNameOwnerStatus"><el-radio value="在职">在职</el-radio><el-radio value="离职">离职</el-radio></el-radio-group></el-form-item><el-form-item class="phone-asset-modal__form-row" label="性别"><el-radio-group v-model="form.gender"><el-radio value="男">男</el-radio><el-radio value="女">女</el-radio></el-radio-group></el-form-item></el-form><template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="submitCreate">保存</el-button></template></el-dialog>
</section>
`
};
......@@ -32,6 +32,9 @@ export function searchCompanyProfiles(keyword) { return request(`/api/wecom-acco
/** Code purpose (plain language): searches existing phone assets for the registration-phone input. Related files: WecomAccountView.js, WecomAccountController.java. Data flow: autocomplete -> GET lookup -> selectable numbers. */
export function searchPhoneAssets(keyword) { return request(`/api/wecom-accounts/lookups/phone-assets?keyword=${encodeURIComponent(keyword || '')}`); }
/** 代码作用(白话):查询注册手机号在手机号码管理中是否已存在。关联文件:WecomAccountView.js、WecomAccountController.java。关联逻辑(调用链/数据流):手机号输入 -> GET phone-exists -> 布尔结果 -> 决定是否展开新增手机号区块。 */
export function checkPhoneExists(phoneNumber) { return request(`/api/wecom-accounts/lookups/phone-exists?phoneNumber=${encodeURIComponent(phoneNumber || '')}`); }
/** Code purpose (plain language): searches optional WeCom owners. Related files: WecomAccountView.js, WecomAccountController.java. Data flow: remote select -> GET lookup -> options. */
export function searchCompanyPersons(keyword) { return request(`/api/wecom-accounts/lookups/company-persons?keyword=${encodeURIComponent(keyword || '')}`); }
......
......@@ -51,8 +51,10 @@ body { margin: 0; }
}
.phone-asset-modal {
width: min(560px, 100%) !important;
max-height: calc(100vh - 48px);
height: min(680px, calc(100vh - 48px));
margin: 0 !important;
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid rgba(228, 228, 231, 0.9);
border-radius: 12px;
......@@ -96,15 +98,44 @@ body { margin: 0; }
height: 15px;
}
.phone-asset-modal .el-dialog__body {
max-height: calc(100vh - 206px);
flex: 1 1 auto;
min-height: 0;
padding: 4px 24px 8px;
overflow-y: auto;
}
/* 弹窗正文的滚动条缩窄并去掉轨道色,使其融入表单背景。 */
@media (min-width: 601px) {
.phone-asset-modal .el-dialog__body {
scrollbar-width: thin;
scrollbar-color: #d4d4d8 transparent;
}
.phone-asset-modal .el-dialog__body::-webkit-scrollbar {
width: 6px;
}
.phone-asset-modal .el-dialog__body::-webkit-scrollbar-track {
background: transparent;
}
.phone-asset-modal .el-dialog__body::-webkit-scrollbar-thumb {
border-radius: 999px;
background: #d4d4d8;
}
.phone-asset-modal .el-dialog__body::-webkit-scrollbar-thumb:hover {
background: #a1a1aa;
}
}
.phone-asset-modal__form {
display: grid;
gap: 14px;
}
/* 企微新增注册手机号为系统不存在的号码时,"新增手机号"区块的分隔标题 */
.phone-asset-modal__form-divider {
margin: 4px 0 0;
padding-top: 12px;
border-top: 1px dashed #e4e4e7;
color: #52525b;
font-size: 13px;
font-weight: 600;
}
.phone-asset-modal__form-row.el-form-item {
display: grid;
grid-template-columns: 96px minmax(0, 1fr);
......
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