Commit 3d2fe0a8 by DaiJiezhang

Merge branch 'codex/merge-unmerged-auth-commits' into 'master'

列表分页器统一与界面文案中文化

See merge request !9
parents e8116fb2 10a82a20
......@@ -30,5 +30,5 @@ export default {
return { authState, logout, canVisit, isAdministrator, retryConnection };
},
template: `<main v-if="authState.connectionError" class="auth-network-error" role="alert"><section class="auth-network-error__panel"><h1>网络连接异常</h1><p>暂时无法确认登录状态,请检查网络后重试。</p><button type="button" @click="retryConnection">重新连接</button></section></main><main v-else-if="!authState.ready" class="auth-loading" role="status" aria-live="polite">正在验证登录状态…</main><RouterView v-else-if="$route.path === '/login'" /><div v-else class="app-shell"><aside class="sidebar"><p class="eyebrow">学有为资产</p><h1>学有为资产后台</h1><nav aria-label="主导航"><RouterLink v-if="canVisit('overview')" to="/overview">总览</RouterLink><RouterLink v-if="canVisit('domain')" to="/domain">域名资料</RouterLink><RouterLink v-if="canVisit('company-profile')" to="/company-profiles">公司档案</RouterLink><RouterLink v-if="canVisit('reference-wecom')" to="/reference/wecom">企微资料</RouterLink><RouterLink v-if="canVisit('phone-assets')" to="/phone-assets">手机号资产</RouterLink><RouterLink v-if="isAdministrator()" to="/device-assets">设备资产管理</RouterLink><RouterLink v-if="canVisit('alerts')" to="/alerts">提醒中心</RouterLink><RouterLink v-if="isAdministrator()" class="settings-link" to="/settings/users-permissions"><span aria-hidden="true">⚙</span> 账号与权限</RouterLink></nav><button class="logout-button" @click="logout">退出登录</button></aside><main class="content"><RouterView /></main></div>`
template: `<main v-if="authState.connectionError" class="auth-network-error" role="alert"><section class="auth-network-error__panel"><h1>网络连接异常</h1><p>暂时无法确认登录状态,请检查网络后重试。</p><button type="button" @click="retryConnection">重新连接</button></section></main><main v-else-if="!authState.ready" class="auth-loading" role="status" aria-live="polite">正在验证登录状态…</main><RouterView v-else-if="$route.path === '/login'" /><div v-else class="app-shell"><aside class="sidebar"><h1>学有为资产后台</h1><nav aria-label="主导航"><RouterLink v-if="canVisit('overview')" to="/overview">总览</RouterLink><RouterLink v-if="canVisit('domain')" to="/domain">域名资料</RouterLink><RouterLink v-if="canVisit('company-profile')" to="/company-profiles">公司档案</RouterLink><RouterLink v-if="canVisit('reference-wecom')" to="/reference/wecom">企微资料</RouterLink><RouterLink v-if="canVisit('phone-assets')" to="/phone-assets">手机号资产</RouterLink><RouterLink v-if="isAdministrator()" to="/device-assets">设备资产管理</RouterLink><RouterLink v-if="canVisit('alerts')" to="/alerts">提醒中心</RouterLink><RouterLink v-if="isAdministrator()" class="settings-link" to="/settings/users-permissions"><span aria-hidden="true">⚙</span> 账号与权限</RouterLink></nav><button class="logout-button" @click="logout">退出登录</button></aside><main class="content"><RouterView /></main></div>`
};
import { computed } from 'vue/dist/vue.esm-bundler.js';
/**
* 文件用途(白话):所有数据列表页共用的分页器,保证四个页面的外观、能力和空数据表现完全一致。
* 关联文件:frontend/src/styles/app.css(.app-pagination 样式)、PhoneAssetView.js、DeviceAssetView.js、WecomAccountView.js、CompanyProfileView.js。
* 关联逻辑(调用链/数据流):列表页 total/filters -> 本组件 -> update:page / update:size -> 列表页 loadPage。
*/
export default {
name: 'AppPagination',
props: {
/** 后端返回的总条数,允许为 0。 */
total: { type: Number, default: 0 },
/** 当前页码,从 1 开始。 */
page: { type: Number, default: 1 },
/** 每页条数。 */
size: { type: Number, default: 20 },
/** 每页条数下拉可选值。 */
sizes: { type: Array, default: () => [5, 10, 20, 50] }
},
emits: ['update:page', 'update:size'],
setup(props, { emit }) {
/** 代码作用(白话):把可能为 null/NaN 的总条数收敛成非负整数,避免文案显示 NaN。关联逻辑(数据流):接口 total -> 兜底 -> 文案与页数计算。 */
const safeTotal = computed(() => {
const value = Number(props.total);
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
});
/** 代码作用(白话):无数据时也占住一页,让"第 1/1 页"和页码占位都有意义。 */
const pageCount = computed(() => Math.ceil(safeTotal.value / props.size) || 1);
const isEmpty = computed(() => safeTotal.value === 0);
/**
* 代码作用(白话):喂给 el-pagination 的总数在空数据时改成 1。
* 原因:Element Plus 在 total=0 时算出 0 页,一个页码都不渲染,"上一页 / 下一页"中间会塌陷;
* 给 1 条就能画出一个页码占位,再由 .app-pagination--empty 把它灰化成不可点状态。
*/
const pagerTotal = computed(() => (isEmpty.value ? 1 : safeTotal.value));
/** 代码作用(白话):页数不多时收起跳页框,页码全在眼前时输入页号反而更慢。 */
const layout = computed(() => (pageCount.value > 5 ? 'sizes, prev, pager, next, jumper' : 'sizes, prev, pager, next'));
/** 代码作用(白话):夹住越界页码,避免父级 page 短暂大于总页数时分页器高亮丢失。 */
const currentPage = computed(() => Math.min(Math.max(Number(props.page) || 1, 1), pageCount.value));
/** 代码作用(白话):把页码变更交回列表页,由列表页决定何时请求数据。关联逻辑(调用链):分页器 -> update:page -> changePage -> loadPage。 */
function changePage(next) { emit('update:page', next); }
/** 代码作用(白话):把每页条数变更交回列表页,列表页负责回到第一页再请求。 */
function changeSize(next) { emit('update:size', next); }
return { safeTotal, pageCount, isEmpty, pagerTotal, layout, currentPage, changePage, changeSize };
},
template: `
<footer class="app-pagination" :class="{ 'app-pagination--empty': isEmpty }">
<span class="app-pagination__summary">共 {{ safeTotal }} 条 · 第 {{ currentPage }}/{{ pageCount }} 页</span>
<el-pagination
:layout="layout"
:current-page="currentPage"
:page-size="size"
:page-sizes="sizes"
:total="pagerTotal"
@current-change="changePage"
@size-change="changeSize" />
</footer>
`
};
import { createApp } from 'vue/dist/vue.esm-bundler.js';
import ElementPlus from 'element-plus';
// Element Plus 默认语言是英文,分页器会显示 20/page、Go to 之类;这里显式挂中文包。
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import 'element-plus/dist/index.css';
import App from './App.js';
import AppPagination from './components/AppPagination.js';
import router from './router/index.js';
import './styles/app.css';
import './styles/auth-state.css';
......@@ -11,6 +14,8 @@ import './styles/auth-state.css';
* 关联文件:frontend/index.html、frontend/src/App.js、frontend/src/router/index.js。
* 关联逻辑(调用链/数据流):index.html -> main.js -> createApp() -> router -> 静态参考页或重构占位页。
*/
const application = createApp(App).use(router).use(ElementPlus);
const application = createApp(App).use(router).use(ElementPlus, { locale: zhCn });
// 分页器全局注册:四个列表页共用一套外观与空数据表现,避免各页模板再次分叉。
application.component('AppPagination', AppPagination);
await router.isReady();
application.mount('#app');
......@@ -32,6 +32,11 @@ export default {
const result = await listCompanyProfiles(filters);
records.value = result.records;
total.value = result.total;
// 越界兜底:当前页已无数据(并发删除、条数变化等)时回退到实际最后一页重读,避免出现"共 0 条 · 第 3/1 页"。
if (!result.records.length && filters.page > 1) {
filters.page = Math.max(1, Math.ceil(result.total / filters.size));
await loadPage();
}
} catch (error) {
ElMessage.error(error.message);
} finally {
......@@ -125,6 +130,17 @@ export default {
}
/**
* 代码作用(白话):切换每页条数后回到第一页重新读取,避免停留在原页码导致越界。
* 关联文件:CompanyProfileView.js、AppPagination.js。
* 关联逻辑(调用链/数据流):分页器 -> filters.size/page -> GET 参数 -> 第一页记录。
*/
function changePageSize(size) {
filters.size = size;
filters.page = 1;
loadPage();
}
/**
* 代码作用(白话):统一显示空字段,避免可选信息为空时留下难以辨认的空白。
* 关联文件:CompanyProfileResponse.java、CompanyProfileView.js。
* 关联逻辑(调用链/数据流):API 字段 -> formatValue -> 表格单元格。
......@@ -134,13 +150,13 @@ export default {
}
onMounted(loadPage);
return { canEdit, changePage, dialogVisible, filters, form, formatValue, loading, openCreate, records, resetSearch, saving, scheduleSearch, submitCreate, submitSearch, total };
return { canEdit, changePage, changePageSize, dialogVisible, filters, form, formatValue, loading, openCreate, records, resetSearch, saving, scheduleSearch, submitCreate, submitSearch, total };
},
template: `
<section class="phone-asset-list-page company-profile-page">
<header class="phone-asset-list-page__header"><div><h2>公司档案</h2><p class="wecom-account-page__eyebrow">COMPANY PROFILES</p></div><el-button v-if="canEdit" class="phone-asset-list-page__add" type="primary" @click="openCreate">新增公司档案</el-button></header>
<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" @keydown.enter.prevent="submitSearch" /><el-button @click="resetSearch">重置</el-button></el-form></section>
<section class="phone-asset-list-page__panel phone-asset-list-page__table"><header class="phone-asset-list-page__table-header"><h3>公司档案列表</h3><span>共 {{ total }} 条</span></header><div class="phone-asset-list-page__grid-wrap"><el-table v-loading="loading" :data="records" empty-text="暂无匹配数据" class="phone-asset-list-page__grid company-profile-page__grid"><el-table-column label="公司名称" min-width="200"><template #default="{ row }">{{ formatValue(row.companyName) }}</template></el-table-column><el-table-column label="公司简称" min-width="150"><template #default="{ row }">{{ formatValue(row.shortName) }}</template></el-table-column><el-table-column label="统一社会信用代码" min-width="210"><template #default="{ row }">{{ formatValue(row.unifiedSocialCreditCode) }}</template></el-table-column><el-table-column label="地址" min-width="220" show-overflow-tooltip><template #default="{ row }">{{ formatValue(row.address) }}</template></el-table-column><el-table-column label="联系人" min-width="130"><template #default="{ row }">{{ formatValue(row.contactName) }}</template></el-table-column><el-table-column label="联系方式" min-width="170"><template #default="{ row }">{{ formatValue(row.contactValue) }}</template></el-table-column><el-table-column label="创建时间" min-width="180"><template #default="{ row }">{{ formatValue(row.createTime) }}</template></el-table-column><el-table-column label="更新时间" min-width="180"><template #default="{ row }">{{ formatValue(row.updateTime) }}</template></el-table-column></el-table></div><footer v-if="total" class="phone-asset-list-page__pagination"><span>共 {{ total }} 条</span><el-pagination layout="prev, pager, next" :current-page="filters.page" :page-size="filters.size" :total="total" @current-change="changePage" /></footer></section>
<section class="phone-asset-list-page__panel phone-asset-list-page__table"><header class="phone-asset-list-page__table-header"><h3>公司档案列表</h3><span>共 {{ total }} 条</span></header><div class="phone-asset-list-page__grid-wrap"><el-table v-loading="loading" :data="records" empty-text="暂无匹配数据" class="phone-asset-list-page__grid company-profile-page__grid"><el-table-column label="公司名称" min-width="200"><template #default="{ row }">{{ formatValue(row.companyName) }}</template></el-table-column><el-table-column label="公司简称" min-width="150"><template #default="{ row }">{{ formatValue(row.shortName) }}</template></el-table-column><el-table-column label="统一社会信用代码" min-width="210"><template #default="{ row }">{{ formatValue(row.unifiedSocialCreditCode) }}</template></el-table-column><el-table-column label="地址" min-width="220" show-overflow-tooltip><template #default="{ row }">{{ formatValue(row.address) }}</template></el-table-column><el-table-column label="联系人" min-width="130"><template #default="{ row }">{{ formatValue(row.contactName) }}</template></el-table-column><el-table-column label="联系方式" min-width="170"><template #default="{ row }">{{ formatValue(row.contactValue) }}</template></el-table-column><el-table-column label="创建时间" min-width="180"><template #default="{ row }">{{ formatValue(row.createTime) }}</template></el-table-column><el-table-column label="更新时间" min-width="180"><template #default="{ row }">{{ formatValue(row.updateTime) }}</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="新增公司档案" width="560px">
<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.companyName" maxlength="100" autocomplete="off" placeholder="请输入公司名称" /></el-form-item>
......
......@@ -15,7 +15,7 @@ export default {
let searchTimer=null;
/** Plain purpose: fetch the currently filtered device page. Related files: device-api-client.js, DeviceAssetService.java. Flow: page/filter event -> GET -> records/total -> table. */
async function loadPage(){loading.value=true;try{const result=await listDeviceAssets(filters);records.value=result.records;total.value=result.total;}catch(error){ElMessage.error(error.message);}finally{loading.value=false;}}
async function loadPage(){loading.value=true;try{const result=await listDeviceAssets(filters);records.value=result.records;total.value=result.total;/* 越界兜底:当前页已无数据(删完本页、并发删除等)时回退到实际最后一页重读,避免出现“共 0 条 · 第 3/1 页”。 */if(!result.records.length&&filters.page>1){filters.page=Math.max(1,Math.ceil(result.total/filters.size));await loadPage();}}catch(error){ElMessage.error(error.message);}finally{loading.value=false;}}
/** Plain purpose: return the create-form defaults that obey both dropdown rules. Related files: DeviceAssetSaveRequest.java, DeviceAssetView.js. Flow: create/reset -> defaults -> multipart POST. */
function emptyForm(){return {deviceName:'',userPersonId:null,userUsageStatus:'\u4f7f\u7528\u4e2d',assetRelationStatus:'\u5f85\u786e\u8ba4',imageAttachment1:null,imageAttachment2:null,removeImageAttachment1:false,removeImageAttachment2:false,imageAttachment1Url:'',imageAttachment2Url:''};}
/** Plain purpose: clear temporary preview URLs and restore a new-device form. Related files: DeviceAssetView.js, DeviceAssetFileStorageService.java. Flow: open create/save -> reset -> safe empty dialog. */
......@@ -54,5 +54,5 @@ export default {
onMounted(loadPage);
return {usageStatuses,relationStatuses,loading,saving,dialogVisible,editingId,records,total,filters,form,personOptions,imageViewerVisible,imageViewerUrl,loadPage,openCreate,openEdit,submitForm,confirmDelete,fetchPersonSuggestions,chooseImage,removeImage,previewImage,scheduleSearch,submitSearch,resetSearch,changePage,changePageSize};
},
template:`<section class="device-asset-page"><header class="device-asset-page__header"><div><h2>&#x8bbe;&#x5907;&#x8d44;&#x4ea7;&#x7ba1;&#x7406;</h2><p>DEVICE ASSETS</p></div><el-button type="primary" @click="openCreate">&#x65b0;&#x589e;&#x8bbe;&#x5907;</el-button></header><section class="device-asset-page__panel"><el-form class="device-asset-page__filters" @submit.prevent="submitSearch"><el-input v-model="filters.deviceName" placeholder="&#x8bbe;&#x5907;&#x540d;&#x79f0;" @input="scheduleSearch"/><el-select v-model="filters.userPersonId" filterable remote clearable placeholder="&#x4f7f;&#x7528;&#x4eba;" :remote-method="fetchPersonSuggestions" @change="submitSearch"><el-option v-for="item in personOptions" :key="item.id" :label="item.personName" :value="item.id"/></el-select><el-select v-model="filters.userUsageStatus" clearable placeholder="&#x4f7f;&#x7528;&#x72b6;&#x6001;" @change="submitSearch"><el-option v-for="item in usageStatuses" :key="item" :label="item" :value="item"/></el-select><el-select v-model="filters.assetRelationStatus" clearable placeholder="&#x8d44;&#x4ea7;&#x5173;&#x8054;&#x72b6;&#x6001;" @change="submitSearch"><el-option v-for="item in relationStatuses" :key="item" :label="item" :value="item"/></el-select><el-button @click="resetSearch">&#x91cd;&#x7f6e;</el-button></el-form></section><section class="device-asset-page__panel device-asset-page__table"><div class="device-asset-page__grid-wrap"><el-table class="device-asset-page__grid" :data="records" v-loading="loading" empty-text="&#x6682;&#x65e0;&#x5339;&#x914d;&#x6570;&#x636e;"><el-table-column prop="id" label="ID" width="90"/><el-table-column prop="deviceName" label="&#x8bbe;&#x5907;&#x540d;&#x79f0;" min-width="180"/><el-table-column label="&#x56fe;&#x7247;" width="90"><template #default="{row}"><span v-if="!row.imageAttachment1Url">-</span><span v-else class="device-asset-page__image-cell"><img :src="row.imageAttachment1Url" @click="previewImage(row.imageAttachment1Url)"/></span></template></el-table-column><el-table-column label="&#x4f7f;&#x7528;&#x4eba;" min-width="160"><template #default="{row}">{{row.userPersonName?row.userPersonName+'('+row.userPersonId+')':(row.userPersonId?'--('+row.userPersonId+')':'-')}}</template></el-table-column><el-table-column prop="userUsageStatus" label="&#x4f7f;&#x7528;&#x72b6;&#x6001;" min-width="120"/><el-table-column prop="assetRelationStatus" label="&#x8d44;&#x4ea7;&#x5173;&#x8054;&#x72b6;&#x6001;" min-width="130"/><el-table-column prop="updateTime" label="&#x66f4;&#x65b0;&#x65f6;&#x95f4;" min-width="180"/><el-table-column label="&#x64cd;&#x4f5c;" width="150"><template #default="{row}"><el-button link @click="openEdit(row)">&#x7f16;&#x8f91;</el-button><el-button link type="danger" @click="confirmDelete(row)">&#x5220;&#x9664;</el-button></template></el-table-column></el-table></div><footer v-if="total" class="device-asset-page__pagination"><span>&#x5171; {{total}} &#x6761;</span><el-pagination layout="sizes, prev, pager, next" :current-page="filters.page" :page-size="filters.size" :page-sizes="[5,10,20,50]" :total="total" @current-change="changePage" @size-change="changePageSize"/></footer></section><el-dialog v-model="dialogVisible" :title="editingId===null?'\u65b0\u589e\u8bbe\u5907':'\u7f16\u8f91\u8bbe\u5907'" width="640px"><el-form label-position="top" @submit.prevent="submitForm"><el-row :gutter="16"><el-col :span="12"><el-form-item label="&#x8bbe;&#x5907;&#x540d;&#x79f0;" required><el-input v-model="form.deviceName"/></el-form-item></el-col><el-col :span="12"><el-form-item label="&#x4f7f;&#x7528;&#x4eba;"><el-select v-model="form.userPersonId" filterable remote clearable :remote-method="fetchPersonSuggestions" placeholder="&#x8f93;&#x5165;&#x4eba;&#x5458;&#x59d3;&#x540d;" style="width:100%"><el-option v-for="item in personOptions" :key="item.id" :label="item.personName" :value="item.id"/></el-select></el-form-item></el-col><el-col :span="12"><el-form-item label="&#x4f7f;&#x7528;&#x72b6;&#x6001;" required><el-select v-model="form.userUsageStatus" style="width:100%"><el-option v-for="item in usageStatuses" :key="item" :label="item" :value="item"/></el-select></el-form-item></el-col><el-col :span="12"><el-form-item label="&#x8d44;&#x4ea7;&#x5173;&#x8054;&#x72b6;&#x6001;" required><el-select v-model="form.assetRelationStatus" style="width:100%"><el-option v-for="item in relationStatuses" :key="item" :label="item" :value="item"/></el-select></el-form-item></el-col></el-row><div class="device-asset-page__images"><div v-for="slot in ['imageAttachment1','imageAttachment2']" :key="slot" class="device-asset-page__image-slot"><strong>{{slot==='imageAttachment1'?'\u56fe\u7247\u9644\u4ef6 1':'\u56fe\u7247\u9644\u4ef6 2'}}</strong><div class="device-asset-page__preview"><img v-if="form[slot+'Url']" :src="form[slot+'Url']" @click="previewImage(form[slot+'Url'])"/><span v-else>&#x6682;&#x65e0;&#x56fe;&#x7247;</span></div><el-upload :auto-upload="false" :show-file-list="false" :on-change="file=>chooseImage(slot,file)"><el-button size="small">&#x9009;&#x62e9;&#x56fe;&#x7247;</el-button></el-upload><el-button v-if="form[slot+'Url']" size="small" link type="danger" @click="removeImage(slot)">&#x79fb;&#x9664;</el-button></div></div></el-form><template #footer><el-button @click="dialogVisible=false">&#x53d6;&#x6d88;</el-button><el-button type="primary" :loading="saving" @click="submitForm">&#x4fdd;&#x5b58;</el-button></template></el-dialog><el-image-viewer v-if="imageViewerVisible" :url-list="[imageViewerUrl]" @close="imageViewerVisible=false"/></section>`
template:`<section class="device-asset-page"><header class="device-asset-page__header"><div><h2>&#x8bbe;&#x5907;&#x8d44;&#x4ea7;&#x7ba1;&#x7406;</h2></div><el-button type="primary" @click="openCreate">&#x65b0;&#x589e;&#x8bbe;&#x5907;</el-button></header><section class="device-asset-page__panel"><el-form class="device-asset-page__filters" @submit.prevent="submitSearch"><el-input v-model="filters.deviceName" placeholder="&#x8bbe;&#x5907;&#x540d;&#x79f0;" @input="scheduleSearch"/><el-select v-model="filters.userPersonId" filterable remote clearable placeholder="&#x4f7f;&#x7528;&#x4eba;" :remote-method="fetchPersonSuggestions" @change="submitSearch"><el-option v-for="item in personOptions" :key="item.id" :label="item.personName" :value="item.id"/></el-select><el-select v-model="filters.userUsageStatus" clearable placeholder="&#x4f7f;&#x7528;&#x72b6;&#x6001;" @change="submitSearch"><el-option v-for="item in usageStatuses" :key="item" :label="item" :value="item"/></el-select><el-select v-model="filters.assetRelationStatus" clearable placeholder="&#x8d44;&#x4ea7;&#x5173;&#x8054;&#x72b6;&#x6001;" @change="submitSearch"><el-option v-for="item in relationStatuses" :key="item" :label="item" :value="item"/></el-select><el-button @click="resetSearch">&#x91cd;&#x7f6e;</el-button></el-form></section><section class="device-asset-page__panel device-asset-page__table"><div class="device-asset-page__grid-wrap"><el-table class="device-asset-page__grid" :data="records" v-loading="loading" empty-text="&#x6682;&#x65e0;&#x5339;&#x914d;&#x6570;&#x636e;"><el-table-column prop="id" label="编号" width="90"/><el-table-column prop="deviceName" label="&#x8bbe;&#x5907;&#x540d;&#x79f0;" min-width="180"/><el-table-column label="&#x56fe;&#x7247;" width="90"><template #default="{row}"><span v-if="!row.imageAttachment1Url">-</span><span v-else class="device-asset-page__image-cell"><img :src="row.imageAttachment1Url" @click="previewImage(row.imageAttachment1Url)"/></span></template></el-table-column><el-table-column label="&#x4f7f;&#x7528;&#x4eba;" min-width="160"><template #default="{row}">{{row.userPersonName?row.userPersonName+'('+row.userPersonId+')':(row.userPersonId?'--('+row.userPersonId+')':'-')}}</template></el-table-column><el-table-column prop="userUsageStatus" label="&#x4f7f;&#x7528;&#x72b6;&#x6001;" min-width="120"/><el-table-column prop="assetRelationStatus" label="&#x8d44;&#x4ea7;&#x5173;&#x8054;&#x72b6;&#x6001;" min-width="130"/><el-table-column prop="updateTime" label="&#x66f4;&#x65b0;&#x65f6;&#x95f4;" min-width="180"/><el-table-column label="&#x64cd;&#x4f5c;" width="150"><template #default="{row}"><el-button link @click="openEdit(row)">&#x7f16;&#x8f91;</el-button><el-button link type="danger" @click="confirmDelete(row)">&#x5220;&#x9664;</el-button></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" :title="editingId===null?'\u65b0\u589e\u8bbe\u5907':'\u7f16\u8f91\u8bbe\u5907'" width="640px"><el-form label-position="top" @submit.prevent="submitForm"><el-row :gutter="16"><el-col :span="12"><el-form-item label="&#x8bbe;&#x5907;&#x540d;&#x79f0;" required><el-input v-model="form.deviceName"/></el-form-item></el-col><el-col :span="12"><el-form-item label="&#x4f7f;&#x7528;&#x4eba;"><el-select v-model="form.userPersonId" filterable remote clearable :remote-method="fetchPersonSuggestions" placeholder="&#x8f93;&#x5165;&#x4eba;&#x5458;&#x59d3;&#x540d;" style="width:100%"><el-option v-for="item in personOptions" :key="item.id" :label="item.personName" :value="item.id"/></el-select></el-form-item></el-col><el-col :span="12"><el-form-item label="&#x4f7f;&#x7528;&#x72b6;&#x6001;" required><el-select v-model="form.userUsageStatus" style="width:100%"><el-option v-for="item in usageStatuses" :key="item" :label="item" :value="item"/></el-select></el-form-item></el-col><el-col :span="12"><el-form-item label="&#x8d44;&#x4ea7;&#x5173;&#x8054;&#x72b6;&#x6001;" required><el-select v-model="form.assetRelationStatus" style="width:100%"><el-option v-for="item in relationStatuses" :key="item" :label="item" :value="item"/></el-select></el-form-item></el-col></el-row><div class="device-asset-page__images"><div v-for="slot in ['imageAttachment1','imageAttachment2']" :key="slot" class="device-asset-page__image-slot"><strong>{{slot==='imageAttachment1'?'\u56fe\u7247\u9644\u4ef6 1':'\u56fe\u7247\u9644\u4ef6 2'}}</strong><div class="device-asset-page__preview"><img v-if="form[slot+'Url']" :src="form[slot+'Url']" @click="previewImage(form[slot+'Url'])"/><span v-else>&#x6682;&#x65e0;&#x56fe;&#x7247;</span></div><el-upload :auto-upload="false" :show-file-list="false" :on-change="file=>chooseImage(slot,file)"><el-button size="small">&#x9009;&#x62e9;&#x56fe;&#x7247;</el-button></el-upload><el-button v-if="form[slot+'Url']" size="small" link type="danger" @click="removeImage(slot)">&#x79fb;&#x9664;</el-button></div></div></el-form><template #footer><el-button @click="dialogVisible=false">&#x53d6;&#x6d88;</el-button><el-button type="primary" :loading="saving" @click="submitForm">&#x4fdd;&#x5b58;</el-button></template></el-dialog><el-image-viewer v-if="imageViewerVisible" :url-list="[imageViewerUrl]" @close="imageViewerVisible=false"/></section>`
};
/* 文件用途(白话):仅为设备资产管理页面提供样式,避免修改正在使用的全局 app.css。 */
.device-asset-page{max-width:1680px;margin:0 auto;padding:30px 4px}.device-asset-page__header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.device-asset-page__header h2{margin:0;font-size:28px}.device-asset-page__panel{padding:20px;margin-bottom:16px;background:#fff;border:1px solid #e5e7eb;border-radius:12px}.device-asset-page__filters{display:flex;flex-wrap:wrap;gap:12px}.device-asset-page__filters .el-input,.device-asset-page__filters .el-select{width:210px}.device-asset-page__images{display:flex;gap:14px;flex-wrap:wrap}.device-asset-page__image-slot{width:150px}.device-asset-page__preview{display:flex;width:132px;height:96px;margin:8px 0;align-items:center;justify-content:center;overflow:hidden;border:1px dashed #cbd5e1;border-radius:8px;background:#f8fafc}.device-asset-page__preview img{width:100%;height:100%;object-fit:cover}.device-asset-page__pagination{display:flex;justify-content:space-between;align-items:center;padding-top:16px}.device-asset-page__image-cell img{width:44px;height:44px;object-fit:cover;border-radius:6px;cursor:pointer}/* 固定高度骨架:宽屏下页面锁死一屏,表格内部滚动;断点与 app.css 中的同类规则保持一致。 */
@media(min-width:641px){/* 用 min-height 而不是 height:正常一屏不出外层滚动条,窗口过矮时页面被撑高改由内容区整页滚动。 */.device-asset-page{display:flex;flex-direction:column;min-height:100%;padding-bottom:24px}.device-asset-page__header,.device-asset-page__panel,.device-asset-page__pagination{flex:0 0 auto}.device-asset-page__table{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;margin-bottom:0}/* 表格必须绝对定位:el-table 会用自身内容高度反向撑开父级,留在文档流里 flex 就收缩不下去。 *//* min-height 220px 是兜底:表头约 40px,再留三行左右可视区,低于这个高度就不再压缩表格。 */.device-asset-page__grid-wrap{position:relative;flex:1 1 auto;min-height:220px}/* height 必须显式写:Element Plus 自带 .el-table{height:fit-content},只给 inset 会被它按内容高度顶掉。 */.device-asset-page__grid-wrap > .el-table{position:absolute;inset:0;height:100%}.device-asset-page__grid-wrap > .el-table > .el-table__inner-wrapper{height:100%}.device-asset-page__grid-wrap .el-table__body-wrapper{flex:1 1 auto;min-height:0}.device-asset-page__grid-wrap .el-table__body-wrapper > .el-scrollbar{height:100%}}
@media(max-width:700px){.device-asset-page{padding:20px 0}.device-asset-page__header{align-items:stretch;flex-direction:column;gap:12px}.device-asset-page__filters .el-input,.device-asset-page__filters .el-select{width:100%}.device-asset-page__pagination{align-items:flex-start;flex-direction:column}}
.device-asset-page{max-width:1680px;margin:0 auto;padding:30px 4px}.device-asset-page__header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.device-asset-page__header h2{margin:0;font-size:28px}.device-asset-page__panel{padding:20px;margin-bottom:16px;background:#fff;border:1px solid #e5e7eb;border-radius:12px}.device-asset-page__filters{display:flex;flex-wrap:wrap;gap:12px}.device-asset-page__filters .el-input,.device-asset-page__filters .el-select{width:210px}.device-asset-page__images{display:flex;gap:14px;flex-wrap:wrap}.device-asset-page__image-slot{width:150px}.device-asset-page__preview{display:flex;width:132px;height:96px;margin:8px 0;align-items:center;justify-content:center;overflow:hidden;border:1px dashed #cbd5e1;border-radius:8px;background:#f8fafc}.device-asset-page__preview img{width:100%;height:100%;object-fit:cover}.device-asset-page__image-cell img{width:44px;height:44px;object-fit:cover;border-radius:6px;cursor:pointer}/* 固定高度骨架:宽屏下页面锁死一屏,表格内部滚动;断点与 app.css 中的同类规则保持一致。 */
@media(min-width:641px){/* 用 min-height 而不是 height:正常一屏不出外层滚动条,窗口过矮时页面被撑高改由内容区整页滚动。 */.device-asset-page{display:flex;flex-direction:column;min-height:100%;padding-bottom:24px}.device-asset-page__header,.device-asset-page__panel,.device-asset-page .app-pagination{flex:0 0 auto}.device-asset-page__table{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;margin-bottom:0}/* 表格必须绝对定位:el-table 会用自身内容高度反向撑开父级,留在文档流里 flex 就收缩不下去。 *//* min-height 220px 是兜底:表头约 40px,再留三行左右可视区,低于这个高度就不再压缩表格。 */.device-asset-page__grid-wrap{position:relative;flex:1 1 auto;min-height:220px}/* height 必须显式写:Element Plus 自带 .el-table{height:fit-content},只给 inset 会被它按内容高度顶掉。 */.device-asset-page__grid-wrap > .el-table{position:absolute;inset:0;height:100%}.device-asset-page__grid-wrap > .el-table > .el-table__inner-wrapper{height:100%}.device-asset-page__grid-wrap .el-table__body-wrapper{flex:1 1 auto;min-height:0}.device-asset-page__grid-wrap .el-table__body-wrapper > .el-scrollbar{height:100%}}
@media(max-width:700px){.device-asset-page{padding:20px 0}.device-asset-page__header{align-items:stretch;flex-direction:column;gap:12px}.device-asset-page__filters .el-input,.device-asset-page__filters .el-select{width:100%}}
......@@ -51,6 +51,11 @@ export default {
if (requestId !== latestRequest) return;
records.value = result.records;
total.value = result.total;
// 越界兜底:当前页已无数据(删完本页、并发删除等)时回退到实际最后一页重读,避免出现"共 0 条 · 第 3/1 页"。
if (!result.records.length && filters.page > 1) {
filters.page = Math.max(1, Math.ceil(result.total / filters.size));
await loadPage();
}
} catch (error) {
if (requestId === latestRequest) ElMessage.error(error.message);
} finally {
......@@ -291,7 +296,7 @@ export default {
<el-config-provider :locale="elementLocale"><section class="phone-asset-page phone-asset-list-page">
<header class="phone-asset-list-page__header"><h2>手机号资产</h2><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" aria-label="筛选手机号资产"><el-form class="phone-asset-list-page__filters" @submit.prevent="submitSearch"><el-input v-model="filters.phoneNumber" maxlength="11" inputmode="numeric" placeholder="手机号前3位、后4位或完整号码" @input="limitSearchPhone" @keydown.enter.prevent="submitSearch" /><el-input v-model="filters.iccid" maxlength="20" placeholder="请输入 ICCID" @input="scheduleSearch" @keydown.enter.prevent="submitSearch" /><el-input v-model="filters.realNameOwner" placeholder="请输入实名人" @input="scheduleSearch" @keydown.enter.prevent="submitSearch" /><el-select v-model="filters.disposalStatus" placeholder="使用状态:" clearable @change="changeStatus" @clear="restoreAllDisposalStatuses"><template #prefix>使用状态:</template><el-option label="全部" value="ALL" /><el-option label="正常" value="正常使用" /><el-option label="闲置" value="闲置" /><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"><header class="phone-asset-list-page__table-header"><h3>资产列表</h3><span>共 {{ total }} 条</span></header><div class="phone-asset-list-page__grid-wrap"><el-table v-loading="loading" :data="records" empty-text="暂无匹配数据" class="phone-asset-list-page__grid"><el-table-column prop="phoneNumber" label="手机号" min-width="150" show-overflow-tooltip /><el-table-column label="号码类型" min-width="120"><template #default="{ row }"><a v-if="row.numberType === 'EXTERNAL' && row.sourceAssetType === 'WECOM'" class="phone-asset-list-page__external-link" :href="'#/reference/wecom?phoneAssetId=' + row.id">外部号码</a><span v-else>{{ row.numberType === 'EXTERNAL' ? '外部号码' : '自有号码' }}</span></template></el-table-column><el-table-column prop="cardType" label="运营商" min-width="110" show-overflow-tooltip /><el-table-column prop="iccid" label="ICCID" min-width="220" show-overflow-tooltip /><el-table-column prop="realNameOwner" label="实名人" min-width="150" show-overflow-tooltip /><el-table-column prop="managementType" label="管理模式" min-width="120" show-overflow-tooltip /><el-table-column label="使用状态" min-width="130"><template #default="{ row }"><span class="phone-asset-list-page__status"><i :class="['phone-asset-list-page__status-dot', row.disposalStatus]"></i>{{ formatDisposalStatus(row.disposalStatus) }}</span></template></el-table-column><el-table-column prop="deviceId" label="关联设备(ID)" min-width="150" show-overflow-tooltip /><el-table-column v-if="canEdit" label="操作" width="120"><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><footer v-if="total" class="phone-asset-list-page__pagination"><span>共 {{ total }} 条</span><el-pagination layout="sizes, prev, pager, next, jumper" :current-page="filters.page" :page-size="filters.size" :page-sizes="[5,10,20,50]" :total="total" @current-change="changePage" @size-change="changePageSize" /></footer></section>
<section class="phone-asset-list-page__panel phone-asset-list-page__table"><header class="phone-asset-list-page__table-header"><h3>资产列表</h3><span>共 {{ total }} 条</span></header><div class="phone-asset-list-page__grid-wrap"><el-table v-loading="loading" :data="records" empty-text="暂无匹配数据" class="phone-asset-list-page__grid"><el-table-column prop="phoneNumber" label="手机号" min-width="150" show-overflow-tooltip /><el-table-column label="号码类型" min-width="120"><template #default="{ row }"><a v-if="row.numberType === 'EXTERNAL' && row.sourceAssetType === 'WECOM'" class="phone-asset-list-page__external-link" :href="'#/reference/wecom?phoneAssetId=' + row.id">外部号码</a><span v-else>{{ row.numberType === 'EXTERNAL' ? '外部号码' : '自有号码' }}</span></template></el-table-column><el-table-column prop="cardType" label="运营商" min-width="110" show-overflow-tooltip /><el-table-column prop="iccid" label="ICCID" min-width="220" show-overflow-tooltip /><el-table-column prop="realNameOwner" label="实名人" min-width="150" show-overflow-tooltip /><el-table-column prop="managementType" label="管理模式" min-width="120" show-overflow-tooltip /><el-table-column label="使用状态" min-width="130"><template #default="{ row }"><span class="phone-asset-list-page__status"><i :class="['phone-asset-list-page__status-dot', row.disposalStatus]"></i>{{ formatDisposalStatus(row.disposalStatus) }}</span></template></el-table-column><el-table-column prop="deviceId" label="关联设备(ID)" min-width="150" show-overflow-tooltip /><el-table-column v-if="canEdit" label="操作" width="120"><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" @opened="resetDialogScroll">
<el-form class="phone-asset-modal__form" label-width="96px">
<el-form-item class="phone-asset-modal__form-row" label="手机号" required>
......
......@@ -27,4 +27,4 @@ export default { setup() { const users = ref([]); const lockedAccounts = ref([])
function openPassword(user) { target.value = user; password.value = ''; passwordOpen.value = true; }
/** 代码作用(白话):把新密码提交给后端并立即从页面内存清除;关联文件:system-user-api-client.js、V1__system_user_auth_permissions.sql。关联逻辑(调用链/数据流):新密码 -> BCrypt -> 数据库触发器 -> 旧会话失效。 */
async function savePassword() { try { await resetSystemUserPassword(target.value.id, password.value); password.value = ''; passwordOpen.value = false; ElMessage.success('密码已更新,旧会话已失效'); await load(); } catch (error) { ElMessage.error(error.message || '密码更新失败'); } }
load(); return { users, lockedAccounts, loading, dialogOpen, passwordOpen, form, pages, roles, editingId, isDeveloper, isAdministratorRole, target, password, load, openCreate, openEdit, save, openPassword, savePassword, unlockAccount, formatLockTime }; }, template: `<section class="user-permission-page"><header class="page-header"><div><p class="eyebrow">设置 / 账号与权限</p><h2>账号与权限</h2><p>开发者账号 Jeddy 不会在此页面显示;逐页权限由此面板独立配置。</p></div><el-button type="primary" @click="openCreate">新增账号</el-button></header><section v-if="lockedAccounts.length" class="reference-card user-permission-page__locked"><h3>登录锁定({{ lockedAccounts.length }})</h3><p>连续输错密码 20 次的账号会被锁定 24 小时。若为误触发或被他人恶意锁定,可在此立即解除。</p><el-table :data="lockedAccounts"><el-table-column prop="username" label="账号名" /><el-table-column prop="failures" label="失败次数" width="110" /><el-table-column label="锁定至"><template #default="scope">{{ formatLockTime(scope.row.lockedUntil) }}</template></el-table-column><el-table-column label="操作" width="140"><template #default="scope"><el-button link type="primary" @click="unlockAccount(scope.row)">解除锁定</el-button></template></el-table-column></el-table></section><section class="reference-card user-permission-page__accounts"><div class="user-permission-page__grid-wrap"><el-table :data="users" v-loading="loading"><el-table-column prop="username" label="用户名" /><el-table-column prop="roleCode" label="角色" /><el-table-column prop="status" label="状态" /><el-table-column label="操作" width="210"><template #default="scope"><el-button link @click="openEdit(scope.row)">编辑权限</el-button><el-button v-if="isDeveloper" link type="primary" @click="openPassword(scope.row)">设置密码</el-button></template></el-table-column></el-table></div></section><el-dialog v-model="dialogOpen" :title="editingId ? '编辑账号与权限' : '新增账号'" width="680px"><el-form label-width="112px"><el-form-item label="用户名" v-if="!editingId"><el-input v-model="form.username" /></el-form-item><el-form-item label="角色"><el-select v-model="form.roleCode"><el-option v-for="role in roles" :key="role.value" :label="role.label" :value="role.value" /></el-select></el-form-item><el-form-item label="账号状态" v-if="editingId"><el-select v-model="form.status"><el-option label="启用" value="ACTIVE" /><el-option label="禁用" value="DISABLED" /></el-select></el-form-item><el-form-item label="初始密码" v-if="!editingId && isDeveloper"><el-input v-model="form.password" type="password" show-password /><small>12-72 位</small></el-form-item><el-alert v-if="!editingId && !isDeveloper" title="超级管理员创建的账号会保持禁用且不设密码,需由开发者设置密码后再启用。" type="info" :closable="false" /><el-divider>页面权限</el-divider><div v-if="isAdministratorRole" class="permission-hint">超级管理员固定拥有全部页面的编辑权限。</div><el-form-item v-for="page in pages" :key="page.key" :label="page.label"><el-radio-group v-model="form.pagePermissions[page.key]" :disabled="isAdministratorRole"><el-radio value="NONE">无权限</el-radio><el-radio value="READ">只读</el-radio><el-radio value="EDIT">编辑</el-radio></el-radio-group></el-form-item></el-form><template #footer><el-button @click="dialogOpen=false">取消</el-button><el-button type="primary" @click="save">保存</el-button></template></el-dialog><el-dialog v-model="passwordOpen" title="设置账号密码" width="420px"><p>仅开发者可以设置密码:{{ target?.username }}</p><el-input v-model="password" type="password" show-password placeholder="12-72 位且不含用户名" /><template #footer><el-button @click="passwordOpen=false">取消</el-button><el-button type="primary" @click="savePassword">确认设置</el-button></template></el-dialog></section>` };
load(); return { users, lockedAccounts, loading, dialogOpen, passwordOpen, form, pages, roles, editingId, isDeveloper, isAdministratorRole, target, password, load, openCreate, openEdit, save, openPassword, savePassword, unlockAccount, formatLockTime }; }, template: `<section class="user-permission-page"><header class="page-header"><div><h2>账号与权限</h2><p>开发者账号 Jeddy 不会在此页面显示;逐页权限由此面板独立配置。</p></div><el-button type="primary" @click="openCreate">新增账号</el-button></header><section v-if="lockedAccounts.length" class="reference-card user-permission-page__locked"><h3>登录锁定({{ lockedAccounts.length }})</h3><p>连续输错密码 20 次的账号会被锁定 24 小时。若为误触发或被他人恶意锁定,可在此立即解除。</p><el-table :data="lockedAccounts"><el-table-column prop="username" label="账号名" /><el-table-column prop="failures" label="失败次数" width="110" /><el-table-column label="锁定至"><template #default="scope">{{ formatLockTime(scope.row.lockedUntil) }}</template></el-table-column><el-table-column label="操作" width="140"><template #default="scope"><el-button link type="primary" @click="unlockAccount(scope.row)">解除锁定</el-button></template></el-table-column></el-table></section><section class="reference-card user-permission-page__accounts"><div class="user-permission-page__grid-wrap"><el-table :data="users" v-loading="loading"><el-table-column prop="username" label="用户名" /><el-table-column prop="roleCode" label="角色" /><el-table-column prop="status" label="状态" /><el-table-column label="操作" width="210"><template #default="scope"><el-button link @click="openEdit(scope.row)">编辑权限</el-button><el-button v-if="isDeveloper" link type="primary" @click="openPassword(scope.row)">设置密码</el-button></template></el-table-column></el-table></div></section><el-dialog v-model="dialogOpen" :title="editingId ? '编辑账号与权限' : '新增账号'" width="680px"><el-form label-width="112px"><el-form-item label="用户名" v-if="!editingId"><el-input v-model="form.username" /></el-form-item><el-form-item label="角色"><el-select v-model="form.roleCode"><el-option v-for="role in roles" :key="role.value" :label="role.label" :value="role.value" /></el-select></el-form-item><el-form-item label="账号状态" v-if="editingId"><el-select v-model="form.status"><el-option label="启用" value="ACTIVE" /><el-option label="禁用" value="DISABLED" /></el-select></el-form-item><el-form-item label="初始密码" v-if="!editingId && isDeveloper"><el-input v-model="form.password" type="password" show-password /><small>12-72 位</small></el-form-item><el-alert v-if="!editingId && !isDeveloper" title="超级管理员创建的账号会保持禁用且不设密码,需由开发者设置密码后再启用。" type="info" :closable="false" /><el-divider>页面权限</el-divider><div v-if="isAdministratorRole" class="permission-hint">超级管理员固定拥有全部页面的编辑权限。</div><el-form-item v-for="page in pages" :key="page.key" :label="page.label"><el-radio-group v-model="form.pagePermissions[page.key]" :disabled="isAdministratorRole"><el-radio value="NONE">无权限</el-radio><el-radio value="READ">只读</el-radio><el-radio value="EDIT">编辑</el-radio></el-radio-group></el-form-item></el-form><template #footer><el-button @click="dialogOpen=false">取消</el-button><el-button type="primary" @click="save">保存</el-button></template></el-dialog><el-dialog v-model="passwordOpen" title="设置账号密码" width="420px"><p>仅开发者可以设置密码:{{ target?.username }}</p><el-input v-model="password" type="password" show-password placeholder="12-72 位且不含用户名" /><template #footer><el-button @click="passwordOpen=false">取消</el-button><el-button type="primary" @click="savePassword">确认设置</el-button></template></el-dialog></section>` };
......@@ -23,7 +23,8 @@ export default {
/** 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() {
loading.value = true;
try { const result = await listWecomAccounts(filters); records.value = result.records; total.value = result.total; } catch (error) { ElMessage.error(error.message); } finally { loading.value = false; }
// 越界兜底:当前页已无数据(并发删除、条数变化等)时回退到实际最后一页重读,避免出现"共 0 条 · 第 3/1 页"。
try { const result = await listWecomAccounts(filters); records.value = result.records; total.value = result.total; if (!result.records.length && filters.page > 1) { filters.page = Math.max(1, Math.ceil(result.total / filters.size)); await loadPage(); } } catch (error) { ElMessage.error(error.message); } finally { loading.value = false; }
}
/** Code purpose (plain language): resets the create form to the business defaults. Related files: WecomAccountSaveRequest.java. Data flow: add button -> reset -> dialog form. */
......@@ -84,17 +85,20 @@ export default {
/** Code purpose (plain language): loads the chosen pagination page. Related files: WecomAccountView.js. Data flow: pagination -> filters.page -> loadPage. */
function changePage(page) { filters.page = page; loadPage(); }
/** Code purpose (plain language): applies a new page size and returns to the first page. Related files: WecomAccountView.js, AppPagination.js. Data flow: pagination -> filters.size/page -> loadPage. */
function changePageSize(size) { filters.size = size; filters.page = 1; loadPage(); }
/** Code purpose (plain language): formats an optional referenced ID for table display. Related files: WecomAccountResponse.java. Data flow: API record -> formatter -> table cell. */
function formatRelation(name, id) { return id === null || id === undefined ? '—' : `${name || '—'}(ID:${id})`; }
onMounted(loadPage);
return { canEdit, changePage, companyOptions, dialogVisible, fetchPhoneSuggestions, filters, form, formatRelation, limitPhoneNumber, loadCompanies, loadOwners, loading, openCreate, ownerOptions, records, resetDialogScroll, resetSearch, restoreAllCompanyProfiles, restoreAllRealNameStatuses, saving, scheduleSearch, submitCreate, submitSearch, total };
return { canEdit, changePage, changePageSize, companyOptions, dialogVisible, fetchPhoneSuggestions, filters, form, formatRelation, limitPhoneNumber, loadCompanies, loadOwners, loading, openCreate, 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><p class="wecom-account-page__eyebrow">WECOM ACCOUNTS</p></div><el-button v-if="canEdit" class="phone-asset-list-page__add" type="primary" @click="openCreate">新增企业微信资产</el-button></header>
<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"><header class="phone-asset-list-page__table-header"><h3>资产列表</h3><span>共 {{ total }} 条</span></header><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 prop="id" label="企业微信资产 ID" min-width="140" /><el-table-column prop="wecomName" label="企微名称" min-width="150" show-overflow-tooltip /><el-table-column prop="wecomAlias" label="企微别名" min-width="180" show-overflow-tooltip /><el-table-column prop="wecomAccount" label="企微账号" min-width="160" show-overflow-tooltip /><el-table-column label="注册主体" min-width="180"><template #default="{ row }">{{ formatRelation(row.companyProfileName, row.companyProfileId) }}</template></el-table-column><el-table-column label="注册手机号" min-width="180"><template #default="{ row }">{{ formatRelation(row.phoneNumber, row.phoneAssetId) }}</template></el-table-column><el-table-column label="关联方式" min-width="120"><template #default="{ row }">{{ row.phoneLinkMode === 'CREATED' ? '新建号码' : '已有号码' }}</template></el-table-column><el-table-column prop="realNameOwner" label="实名人" min-width="120" /><el-table-column prop="realNameOwnerStatus" label="实名状态" min-width="110" /><el-table-column prop="gender" label="性别" min-width="90" /><el-table-column label="企微号归属人" min-width="180"><template #default="{ row }">{{ formatRelation(row.operatorPersonName, row.operatorPersonId) }}</template></el-table-column><el-table-column prop="createTime" label="创建时间" min-width="180" /></el-table></div><footer v-if="total" class="phone-asset-list-page__pagination"><span>共 {{ total }} 条</span><el-pagination layout="prev, pager, next" :current-page="filters.page" :page-size="filters.size" :total="total" @current-change="changePage" /></footer></section>
<section class="phone-asset-list-page__panel phone-asset-list-page__table"><header class="phone-asset-list-page__table-header"><h3>资产列表</h3><span>共 {{ total }} 条</span></header><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 prop="id" label="企业微信资产 ID" min-width="140" /><el-table-column prop="wecomName" label="企微名称" min-width="150" show-overflow-tooltip /><el-table-column prop="wecomAlias" label="企微别名" min-width="180" show-overflow-tooltip /><el-table-column prop="wecomAccount" label="企微账号" min-width="160" show-overflow-tooltip /><el-table-column label="注册主体" min-width="180"><template #default="{ row }">{{ formatRelation(row.companyProfileName, row.companyProfileId) }}</template></el-table-column><el-table-column label="注册手机号" min-width="180"><template #default="{ row }">{{ formatRelation(row.phoneNumber, row.phoneAssetId) }}</template></el-table-column><el-table-column label="关联方式" min-width="120"><template #default="{ row }">{{ row.phoneLinkMode === 'CREATED' ? '新建号码' : '已有号码' }}</template></el-table-column><el-table-column prop="realNameOwner" label="实名人" min-width="120" /><el-table-column prop="realNameOwnerStatus" label="实名状态" min-width="110" /><el-table-column prop="gender" label="性别" min-width="90" /><el-table-column label="企微号归属人" min-width="180"><template #default="{ row }">{{ formatRelation(row.operatorPersonName, row.operatorPersonId) }}</template></el-table-column><el-table-column prop="createTime" label="创建时间" min-width="180" /></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="新增企业微信资产" 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.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>
`
......
......@@ -26,7 +26,7 @@ export default {
template: `
<section class="reference-page">
<header class="page-header">
<div><p class="eyebrow">LEGACY REFERENCE</p><h2>{{ content.title }}</h2><p>{{ content.description }}</p></div>
<div><h2>{{ content.title }}</h2><p>{{ content.description }}</p></div>
<span class="reference-badge">旧界面参考,不可操作</span>
</header>
<article class="reference-card">
......
......@@ -12,13 +12,13 @@ body { margin: 0; }
.login-brand { display: flex; align-items: center; justify-content: center; gap: 16px; margin: 0 auto 14px; }
.login-brand__mark { display: grid; grid-template-columns: repeat(3, 4px); gap: 3px; align-content: center; justify-content: center; width: 56px; height: 56px; overflow: hidden; border: 1px solid rgba(255, 255, 255, .18); border-radius: 18px; background: linear-gradient(145deg, rgba(170, 208, 255, .22), rgba(109, 146, 255, .10)); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .30), 0 12px 30px rgba(68, 121, 255, .18); }
.login-brand__mark span { width: 4px; border-radius: 999px; background: rgba(255, 255, 255, .92); box-shadow: 0 0 8px rgba(187, 218, 255, .72); }.login-brand__mark span:nth-child(1) { height: 15px; }.login-brand__mark span:nth-child(2) { height: 25px; }.login-brand__mark span:nth-child(3) { height: 19px; }
.login-brand__eyebrow { margin: 0 0 5px; color: rgba(211, 226, 255, .66); font-size: 10px; font-weight: 600; letter-spacing: .16em; }.login-brand h1 { margin: 0; color: var(--login-text-primary); font-size: clamp(22px, 2vw, 28px); font-weight: 700; line-height: 1.25; letter-spacing: .04em; text-shadow: 0 4px 18px rgba(5, 12, 28, .25); white-space: nowrap; }
.login-brand h1 { margin: 0; color: var(--login-text-primary); font-size: clamp(22px, 2vw, 28px); font-weight: 700; line-height: 1.25; letter-spacing: .04em; text-shadow: 0 4px 18px rgba(5, 12, 28, .25); white-space: nowrap; }
.login-panel__intro { margin: 0 0 26px; color: var(--login-text-secondary); font-size: 14px; text-align: center; }
.login-form__item.el-form-item { margin-bottom: 16px; }.login-form__input .el-input__wrapper { min-height: 60px; padding: 0 18px; border: 1px solid var(--login-field-border); border-radius: 18px; background: linear-gradient(160deg, rgba(255, 255, 255, .012), rgba(255, 255, 255, .003)); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .035), inset 0 -1px 0 rgba(255, 255, 255, .008), 0 6px 18px rgba(5, 10, 22, .025); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); transition: border-color .2s ease, box-shadow .2s ease, background .2s ease; }.login-form__input .el-input__wrapper:hover { border-color: rgba(211, 225, 255, .13); }.login-form__input .el-input__wrapper.is-focus { border-color: var(--login-field-focus); background: linear-gradient(160deg, rgba(255, 255, 255, .025), rgba(255, 255, 255, .007)); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .055), 0 0 0 4px rgba(136, 182, 255, .055), 0 10px 24px rgba(4, 11, 24, .055); }.login-form__input .el-input__inner { color: var(--login-text-primary); }.login-form__input .el-input__inner::placeholder { color: rgba(224, 231, 255, .58); }.login-form__input .el-input__password { color: rgba(229, 236, 255, .74); }.login-form__input .el-input__password:hover { color: rgba(255, 255, 255, .96); }
.login-submit.el-button { width: 100%; height: 60px; margin-top: 8px; border: 1px solid rgba(205, 224, 255, .18); border-radius: 18px; background: linear-gradient(135deg, rgba(194, 223, 255, .46) 0%, rgba(120, 168, 255, .46) 40%, rgba(84, 127, 255, .50) 100%); color: #fff; font-size: 17px; font-weight: 700; letter-spacing: .08em; box-shadow: inset 0 1px 0 rgba(255, 255, 255, .35), 0 18px 30px rgba(60, 103, 207, .18), 0 4px 10px rgba(4, 9, 18, .16); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); transition: transform .2s ease, box-shadow .2s ease, opacity .2s ease, background .2s ease; }.login-submit.el-button:hover { transform: translateY(-1px); background: linear-gradient(135deg, rgba(194, 223, 255, .56), rgba(120, 168, 255, .56) 40%, rgba(84, 127, 255, .60)); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .42), 0 22px 36px rgba(60, 103, 207, .22), 0 6px 14px rgba(4, 9, 18, .22); }.login-submit.el-button:active { transform: translateY(1px) scale(.99); }.login-submit.el-button.is-loading, .login-submit.el-button.is-disabled { opacity: .76; transform: none; }
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { .login-panel { background: linear-gradient(145deg, rgba(10, 18, 37, .72), rgba(4, 8, 19, .62)); }.login-form__input .el-input__wrapper { background: rgba(7, 15, 32, .72); } }
@media (max-width: 980px) { .login-page { justify-content: center; padding-right: 24px; padding-left: 24px; background-position: 64% center; }.login-panel { margin: 0 auto; } }
@media (max-width: 640px) { .login-page { align-items: center; min-height: 100dvh; padding: 18px; background-position: 63% center; }.login-panel { padding: 26px 22px 24px; border-radius: 26px; }.login-brand { gap: 12px; justify-content: flex-start; }.login-brand__mark { width: 48px; height: 48px; border-radius: 16px; }.login-brand h1 { font-size: clamp(19px, 5.6vw, 22px); letter-spacing: .02em; }.login-brand__eyebrow { font-size: 9px; }.login-panel__intro { margin-bottom: 22px; text-align: left; }.login-form__input .el-input__wrapper, .login-submit.el-button { min-height: 56px; height: 56px; font-size: 16px; }.login-form__input .el-input__inner { font-size: 16px; } }
@media (max-width: 640px) { .login-page { align-items: center; min-height: 100dvh; padding: 18px; background-position: 63% center; }.login-panel { padding: 26px 22px 24px; border-radius: 26px; }.login-brand { gap: 12px; justify-content: flex-start; }.login-brand__mark { width: 48px; height: 48px; border-radius: 16px; }.login-brand h1 { font-size: clamp(19px, 5.6vw, 22px); letter-spacing: .02em; }.login-panel__intro { margin-bottom: 22px; text-align: left; }.login-form__input .el-input__wrapper, .login-submit.el-button { min-height: 56px; height: 56px; font-size: 16px; }.login-form__input .el-input__inner { font-size: 16px; } }
.logout-button { width: 100%; margin-top: 24px; padding: 10px 12px; border: 1px solid #496b8c; border-radius: 8px; color: #cbd5e1; background: transparent; cursor: pointer; }.logout-button:hover { color:#fff; background:#1f3b59; }
/* 后台外壳:把高度锁死在一屏,滚动交给侧栏和内容区各自承担,列表页才能按剩余空间计算表格高度。 */
.app-shell { height: 100dvh; min-height: 100dvh; overflow: hidden; display: grid; grid-template-columns: 232px minmax(0, 1fr); }
......@@ -30,7 +30,6 @@ body { margin: 0; }
.sidebar .settings-link { margin-top: 16px; border-top: 1px solid #385673; border-radius: 0; padding-top: 18px; }
.content { min-width: 0; height: 100%; overflow-y: auto; padding: 40px; }
.user-permission-page { max-width: 1180px; margin: 0 auto; }.user-permission-page small { display:block; margin-top:6px; color:#64748b; }.permission-hint { margin:-4px 0 16px; color:#64748b; font-size:13px; }
.eyebrow { margin: 0 0 8px; color: #5f8fcb; font-size: 12px; font-weight: 700; letter-spacing: .08em; }
.reference-page, .placeholder { max-width: 1180px; margin: 0 auto; }
.page-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 28px; }
.page-header h2, .placeholder h2 { margin: 0 0 8px; font-size: 28px; }
......@@ -292,10 +291,31 @@ body { margin: 0; }
.phone-asset-list-page__filters .el-input__wrapper,.phone-asset-list-page__filters .el-select__wrapper { min-height:38px; border-radius:6px; box-shadow:0 0 0 1px #e5e5e8 inset; }.phone-asset-list-page__filters .el-button{height:38px;border-radius:6px;color:#52525b;border-color:#e5e5e8}.phone-asset-list-page__filters .el-input__wrapper:hover,.phone-asset-list-page__filters .el-select__wrapper:hover{box-shadow:0 0 0 1px #d4d4d8 inset}
.phone-asset-list-page__table { overflow:hidden; }.phone-asset-list-page__table-header{display:flex;align-items:center;gap:9px;min-height:62px;padding:0 22px;border-bottom:1px solid #e5e5e8}.phone-asset-list-page__table-header h3{margin:0;color:#18181b;font-size:15px;font-weight:650}.phone-asset-list-page__table-header span{color:#8b8b93;font-size:13px}
.phone-asset-list-page__grid .el-table__header th.el-table__cell{height:46px;background:#fafafa;color:#71717a;font-size:13px;font-weight:600}.phone-asset-list-page__grid .el-table__cell{height:56px;color:#3f3f46;font-size:14px}.phone-asset-list-page__grid .el-table__row:hover>td.el-table__cell{background:#fafafa}.phone-asset-list-page__status{display:inline-flex;align-items:center;gap:8px;white-space:nowrap}.phone-asset-list-page__status-dot{width:7px;height:7px;border-radius:50%;background:#a1a1aa;box-shadow:0 0 0 3px rgba(161,161,170,.12)}.phone-asset-list-page__status-dot.正常使用{background:#2f9e68;box-shadow:0 0 0 3px rgba(47,158,104,.1)}.phone-asset-list-page__status-dot.闲置,.phone-asset-list-page__status-dot.停机{background:#d28b24;box-shadow:0 0 0 3px rgba(210,139,36,.11)}.phone-asset-list-page__actions{display:inline-flex;gap:14px}.phone-asset-list-page__actions .el-button{padding:0;color:#3f3f46;font-size:13px}.phone-asset-list-page__actions .el-button--danger{color:#e5484d}
.phone-asset-list-page__pagination{display:flex;align-items:center;justify-content:space-between;gap:20px;min-height:66px;padding:12px 22px;border-top:1px solid #e5e5e8;color:#71717a;font-size:13px}.phone-asset-list-page__pagination .el-pagination{justify-content:flex-end}.phone-asset-list-page__pagination .el-pager li,.phone-asset-list-page__pagination .btn-prev,.phone-asset-list-page__pagination .btn-next{min-width:34px;height:34px;border:1px solid #e5e5e8;border-radius:6px;background:#fff}.phone-asset-list-page__pagination .el-pager li.is-active{background:#18181b;color:#fff}
@media(max-width:900px){.phone-asset-list-page{padding:26px 0 40px}.phone-asset-list-page__pagination{align-items:flex-start;flex-direction:column}.phone-asset-list-page__pagination .el-pagination{width:100%;justify-content:space-between}}
@media(max-width:640px){.phone-asset-list-page{padding:20px 0 32px}.phone-asset-list-page__header{align-items:stretch;flex-direction:column;margin-bottom:18px}.phone-asset-list-page__add.el-button{width:100%}.phone-asset-list-page__header h2{font-size:25px}.phone-asset-list-page__search{padding:18px}.phone-asset-list-page__filters,.phone-asset-list-page__filters .el-input,.phone-asset-list-page__filters .el-select,.phone-asset-list-page__filters .el-button{width:100%!important}.phone-asset-list-page__table-header,.phone-asset-list-page__pagination{padding-left:18px;padding-right:18px}.phone-asset-list-page__pagination .el-pagination__jump{display:none}}
.wecom-account-page{min-width:0;max-width:100%;overflow-x:hidden}.wecom-account-page__eyebrow{margin:7px 0 0;color:#71717a;font-size:12px;font-weight:700;letter-spacing:.12em}.wecom-account-page .phone-asset-list-page__table,.wecom-account-page__grid{min-width:0;max-width:100%}.wecom-account-page__grid .el-scrollbar__wrap{overflow-x:auto!important}.phone-asset-list-page__external-link{color:#409eff;text-decoration:none}.phone-asset-list-page__external-link:hover{color:#337ecc;text-decoration:none}
/* 列表页统一分页器(AppPagination.js):容器高度恒定,空数据时按钮灰化但不塌陷,四个列表页共用。 */
.app-pagination{display:flex;align-items:center;justify-content:space-between;gap:20px;min-height:66px;padding:12px 22px;border-top:1px solid #e5e5e8;color:#71717a;font-size:13px}
.app-pagination__summary{white-space:nowrap}
/* 分段容器型:上一页 + 页码 + 下一页三段拼成一条浅灰槽,当前页是槽内浮起的白块。 */
.app-pagination .el-pagination{justify-content:flex-end;gap:0}
/* 每页条数与跳页框留在槽外,只有翻页控件进槽。 */
.app-pagination .el-pagination__sizes{margin-right:12px}
.app-pagination .el-pagination__jump{margin-left:14px}
.app-pagination .btn-prev,.app-pagination .el-pager,.app-pagination .btn-next{margin:0;padding:3px 0;background:#efeff1}
.app-pagination .btn-prev,.app-pagination .btn-next{min-width:30px;height:34px;border:0;border-radius:0;color:#71717a}
.app-pagination .btn-prev{padding-left:3px;border-radius:10px 0 0 10px}
.app-pagination .btn-next{padding-right:3px;border-radius:0 10px 10px 0}
.app-pagination .btn-prev:hover:not(:disabled),.app-pagination .btn-next:hover:not(:disabled){color:#18181b}
/* Element Plus 给 button:disabled 硬塞了白底,会在灰槽两端啃出缺口,这里按槽色盖回去。 */
.app-pagination .btn-prev:disabled,.app-pagination .btn-next:disabled{background:#efeff1;color:#c4c4cc}
.app-pagination .el-pager{display:flex;align-items:center;gap:2px}
.app-pagination .el-pager li{min-width:30px;height:28px;margin:0;border:0;border-radius:8px;background:transparent;color:#71717a;font-weight:400}
.app-pagination .el-pager li:hover{background:#ebebed;color:#18181b}
/* 白块 + 一层极淡投影就是"浮起"的全部:再重就抢过表格内容了。 */
.app-pagination .el-pager li.is-active{background:#fff;color:#18181b;font-weight:500;box-shadow:0 1px 2px rgba(24,24,27,.1)}
/* 空数据态:灰槽保留保证高度不跳,但槽内页码不浮起白块,与"有数据但只有一页"区分开。 */
.app-pagination--empty .el-pager li,.app-pagination--empty .el-pager li.is-active,.app-pagination--empty .el-pager li:hover{background:transparent;box-shadow:none;color:#c4c4cc;font-weight:400;cursor:not-allowed}
@media(max-width:900px){.phone-asset-list-page{padding:26px 0 40px}.app-pagination{align-items:flex-start;flex-direction:column;gap:10px}.app-pagination .el-pagination{width:100%;justify-content:space-between}}
@media(max-width:640px){.phone-asset-list-page{padding:20px 0 32px}.phone-asset-list-page__header{align-items:stretch;flex-direction:column;margin-bottom:18px}.phone-asset-list-page__add.el-button{width:100%}.phone-asset-list-page__header h2{font-size:25px}.phone-asset-list-page__search{padding:18px}.phone-asset-list-page__filters,.phone-asset-list-page__filters .el-input,.phone-asset-list-page__filters .el-select,.phone-asset-list-page__filters .el-button{width:100%!important}.phone-asset-list-page__table-header,.app-pagination{padding-left:18px;padding-right:18px}.app-pagination .el-pagination__jump{display:none}}
.wecom-account-page{min-width:0;max-width:100%;overflow-x:hidden}.wecom-account-page .phone-asset-list-page__table,.wecom-account-page__grid{min-width:0;max-width:100%}.wecom-account-page__grid .el-scrollbar__wrap{overflow-x:auto!important}.phone-asset-list-page__external-link{color:#409eff;text-decoration:none}.phone-asset-list-page__external-link:hover{color:#337ecc;text-decoration:none}
/* 登录页视觉微调:输入本体保持透明,玻璃质感由外层承载,避免影响其他业务表单。 */
.login-page {
......@@ -540,7 +560,7 @@ body { margin: 0; }
.phone-asset-list-page__header,
.phone-asset-list-page__search,
.phone-asset-list-page__table-header,
.phone-asset-list-page__pagination { flex: 0 0 auto; }
.phone-asset-list-page .app-pagination { flex: 0 0 auto; }
.phone-asset-list-page__table { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
/* 表格必须绝对定位:el-table 会用自身内容高度反向撑开父级,留在文档流里 flex 就收缩不下去。
min-height 220px 是兜底:表头约 46px,再留三行左右可视区,低于这个高度就不再压缩表格。 */
......
......@@ -77,7 +77,7 @@ test('keeps a referenced device visible after protected delete response', async
await page.route('**/api/device-assets**', async route=>{const request=route.request();const data=request.method()==='GET'?{records:[{id:8,deviceName:'protected-device',imageAttachment1Url:null,imageAttachment2Url:null,userPersonId:null,userPersonName:null,userUsageStatus:'\u4f7f\u7528\u4e2d',assetRelationStatus:'\u5df2\u5173\u8054',createTime:'',updateTime:''}],total:1,page:1,size:20}:null;const response=request.method()==='DELETE'?{code:400,message:'\u8bbe\u5907\u4ecd\u88ab\u624b\u673a\u53f7\u8d44\u4ea7\u5f15\u7528\uff0c\u4e0d\u80fd\u5220\u9664',data:null}:{code:200,message:'success',data};await route.fulfill({contentType:'application/json',body:JSON.stringify(response)});});
await page.goto('/asset/#/device-assets');
await page.locator('.el-table').getByRole('button',{name:'\u5220\u9664'}).click();
await page.getByRole('button',{name:'OK'}).click();
await page.getByRole('button',{name:'确定'}).click();
await expect(page.getByText('\u8bbe\u5907\u4ecd\u88ab\u624b\u673a\u53f7\u8d44\u4ea7\u5f15\u7528\uff0c\u4e0d\u80fd\u5220\u9664')).toBeVisible();
await expect(page.locator('.el-table').getByText('protected-device',{exact:true})).toBeVisible();
});
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