0bbac7d228
客户在验收里点出来的问题,一次改完。两段改动:
一、平台运营与租户范围(后端为主)
- 新增请求头 X-Tenant-Id:平台运营是唯一跨租户身份,租户拦截器对它整体关掉了过滤,
于是「组织架构 / 账号管理 / 工作台」拿到的是所有租户混在一起的数据 —— 而只有一所
学校有数据时,它和学校管理员看到的完全一样,看不出错(这是最危险的一点)。
现在这些页面必须先选定学校,服务端再把本次请求的租户上下文换成目标租户:
只认平台身份(其它身份带头一律忽略)、目标租户必须存在且启用。
- OrgService.tree / UserService.list 补显式 tenant_id 条件(原来只靠拦截器,而拦截器对
平台不生效);TaskService.seesWholeTenant 改用新的 LoginUser.isPlatformStaff() ——
isPlatformAdmin 回答"当前租户是不是平台租户",代管时它为 false,会让平台退化成普通
用户(任务列表掉进"班主任只看自己带的班"分支,得到空列表);PLATFORM_ADMIN 补
TASK_VIEW(PRD §3.2「查看填报进度看板:平台运营 ✅」)。
- 操作日志对平台永远是空的:SQL 写死 tenant_id = 当前租户,而平台的 tenantId 是 0。
按契约 §13.4 补 tenantId 参数与 log:viewAll:平台默认看全量、可指定某所学校;
学校管理员恒为本租户,传别家返回 40301(不静默忽略,否则会让人以为"筛过了")。
- 开放接口"推送重试"从未生效(F-112):定时扫描缺 @InterceptorIgnore,异常被 log.warn
吞掉;同一方法里"接入方已停用"那条 update 也没有租户上下文,会每 10 秒重试到永远。
E2E 补了"真的重试了"的断言(原来只断言"安排了下次重试")。
二、PC 端走查体验
- 去掉 3 条无用菜单(名单管理 / 表单设计器 / 材料规则):功能都在「收集任务」行内入口,
菜单里那一条点进去只有指路;后端菜单键同步删除,entryAt 那套机制一并删掉。
- 班主任点工作台必弹 403:工作台无条件调了要 org:view / progress:viewAll 的两个接口。
403 走 HTTP 状态码,try/catch 拦不住它的弹窗 —— 改成按权限决定发不发请求。
- 操作日志:分页器被裁在可视区外(新增 .g-list-card 让表体自己滚、表头钉住、分页器留在
卡片底部)+ 去掉页面上那段免责声明;收集任务的状态切换改成单独一行 tab;去掉顶栏假数据
铃铛;配色按客户要求还原。
顺带修两处"检查器自己不严":check-wiring 新增第 ⑦ 项(PC 端引用的权限码必须存在于
PermissionDef —— 本轮我自己把 progress:viewAll 写成 progress:view:all,判断恒为 false,
而编译 / typecheck / lint / E2E 全都不红);verify-docs 的表格列数检查不再把 GFM 转义
竖线当成列分隔符。两个检查器都拿"故意写错的样例"试过,确认真的会红。
验证:typecheck / mvn package / check-wiring(未接线声明 0 条)/ verify-docs(22 项)全过;
e2e-m0 36、e2e-m1-orgs 48、e2e-m0-user 55、e2e-m5-logs 44、e2e-m5-notify-wiring 43、
e2e-m1-tenant-domain 6、e2e-m8-open 61 全绿;浏览器按角色逐个真点复验。
详见 docs/交接摘要.md §一·三十三 与 §一·三十四。
179 lines
5.1 KiB
Vue
179 lines
5.1 KiB
Vue
<!--
|
||
@file HeaderRightBar 组件
|
||
@description 头部右侧工具栏组件,包含项目配置、消息通知、全屏切换、主题切换和用户菜单等功能
|
||
-->
|
||
<template>
|
||
<a-row justify="end" align="center" class="header-right-bar">
|
||
<a-space size="medium">
|
||
<!-- 项目配置按钮 -->
|
||
<a-tooltip content="项目配置" position="bl">
|
||
<a-button size="mini" class="g-hover-btn" @click="handleOpenSettings">
|
||
<template #icon>
|
||
<icon-settings :size="18" />
|
||
</template>
|
||
</a-button>
|
||
</a-tooltip>
|
||
|
||
<!--
|
||
消息通知(铃铛)**暂时去掉**(2026-10-09 客户要求):
|
||
原来的铃铛是一个写死 9 条假数据的下拉框,站内信还没接到这个入口上。
|
||
真要做的时候,数据源是「通知记录」(`/collect/notify`,接口已实现),
|
||
不要恢复成写死的假列表。
|
||
-->
|
||
|
||
<!-- 全屏切换按钮 -->
|
||
<a-tooltip v-if="!['xs', 'sm'].includes(breakpoint)" content="全屏切换" position="bottom">
|
||
<a-button size="mini" class="g-hover-btn" @click="toggle">
|
||
<template #icon>
|
||
<icon-fullscreen v-if="!isFullscreen" :size="18" />
|
||
<icon-fullscreen-exit v-else :size="18" />
|
||
</template>
|
||
</a-button>
|
||
</a-tooltip>
|
||
|
||
<!-- 暗黑模式切换 -->
|
||
<a-tooltip content="主题切换" position="bottom">
|
||
<GiThemeBtn></GiThemeBtn>
|
||
</a-tooltip>
|
||
|
||
<!-- 管理员账户 -->
|
||
<a-dropdown trigger="hover">
|
||
<a-row align="center" :wrap="false" class="header-right-bar__user">
|
||
<!-- 头像:后端未下发 avatarUrl(本项目用姓名首字兜底,避免渲染成裂图) -->
|
||
<a-avatar :size="32" :style="{ backgroundColor: 'rgb(var(--primary-6))' }">
|
||
<img v-if="userStore.avatar" :src="userStore.avatar" alt="" />
|
||
<template v-else>{{ avatarText }}</template>
|
||
</a-avatar>
|
||
<span class="header-right-bar__username">{{ userStore.name }}</span>
|
||
<icon-down />
|
||
</a-row>
|
||
|
||
<template #content>
|
||
<a-doption v-for="item in USER_MENUS" :key="item.key" @click="item.onClick">
|
||
<template #icon>
|
||
<GiIconBox :color="item.iconColor">
|
||
<component :is="item.icon" />
|
||
</GiIconBox>
|
||
</template>
|
||
<span>{{ item.label }}</span>
|
||
</a-doption>
|
||
<a-divider :margin="0" />
|
||
<a-doption @click="handleLogout">
|
||
<template #icon>
|
||
<GiIconBox color="warning">
|
||
<icon-export />
|
||
</GiIconBox>
|
||
</template>
|
||
<span>退出登录</span>
|
||
</a-doption>
|
||
</template>
|
||
</a-dropdown>
|
||
</a-space>
|
||
</a-row>
|
||
|
||
<ChangePasswordModal ref="changePwdRef" />
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { Button, Drawer, Message, Modal } from '@arco-design/web-vue'
|
||
import { useFullscreen } from '@vueuse/core'
|
||
import { useBreakpoint, useTheme } from '@/hooks'
|
||
import { useAppStore, useUserStore } from '@/stores'
|
||
import ChangePasswordModal from './ChangePasswordModal.vue'
|
||
import SettingDrawerPanel from './SettingDrawerPanel.vue'
|
||
|
||
/** 组件名称 */
|
||
defineOptions({ name: 'HeaderRight' })
|
||
|
||
/** 路由实例 */
|
||
const router = useRouter()
|
||
|
||
/** 状态管理 */
|
||
const userStore = useUserStore()
|
||
const appStore = useAppStore()
|
||
const { setThemeColor } = useTheme()
|
||
|
||
/** 响应式断点 */
|
||
const { breakpoint } = useBreakpoint()
|
||
|
||
/** 全屏控制 */
|
||
const { isFullscreen, toggle } = useFullscreen()
|
||
|
||
/** 头像兜底文案:取姓名首字 */
|
||
const avatarText = computed(() => (userStore.name || '—').slice(0, 1))
|
||
|
||
/** 修改密码弹窗 */
|
||
const changePwdRef = useTemplateRef('changePwdRef')
|
||
|
||
/** 用户菜单配置 */
|
||
const USER_MENUS = [
|
||
{
|
||
key: 'password',
|
||
label: '修改密码',
|
||
icon: 'icon-unlock',
|
||
iconColor: 'primary',
|
||
onClick: () => changePwdRef.value?.open()
|
||
}
|
||
]
|
||
|
||
/** 恢复默认项目配置 */
|
||
const handleResetSettings = () => {
|
||
appStore.resetSettings()
|
||
setThemeColor(appStore.themeColor)
|
||
Message.success('已恢复默认配置')
|
||
}
|
||
|
||
/** 打开设置抽屉 */
|
||
const handleOpenSettings = () => {
|
||
Drawer.open({
|
||
title: '项目配置',
|
||
width: 300,
|
||
content: () => h(SettingDrawerPanel),
|
||
footer: () => h(Button, { type: 'primary', long: true, style: { margin: 0 }, onClick: handleResetSettings }, { default: () => '恢复默认配置' })
|
||
})
|
||
}
|
||
|
||
/** 处理退出登录 */
|
||
const handleLogout = () => {
|
||
Modal.warning({
|
||
title: '提示',
|
||
content: '确认退出登录?',
|
||
hideCancel: false,
|
||
closable: true,
|
||
onBeforeOk: async () => {
|
||
try {
|
||
await userStore.logout()
|
||
router.replace('/login')
|
||
return true
|
||
} catch {
|
||
Message.error('退出登录失败')
|
||
return false
|
||
}
|
||
}
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.arco-dropdown-open .arco-icon-down {
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
.header-right-bar {
|
||
&__user {
|
||
color: var(--color-text-1);
|
||
cursor: pointer;
|
||
}
|
||
|
||
&__username {
|
||
margin-left: 10px;
|
||
white-space: nowrap;
|
||
|
||
.arco-icon-down {
|
||
margin-left: 2px;
|
||
transition: all 0.3s;
|
||
}
|
||
}
|
||
}
|
||
</style>
|