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>
......
/* 文件用途(白话):仅为设备资产管理页面提供样式,避免修改正在使用的全局 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>` };
......@@ -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">
......
......@@ -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