chore: 锁屏与通知组件删除(enabled:false的最终清理)

Phase 3 终审 M-1/M-2 摘入口后组件保留待决,本提交最终删除:
- art-screen-lock/ 与 art-notification/ 组件目录
- component.ts screen-lock 全局组件项、headerBar.ts notification 项
- types/config HeaderBarFeatureConfig.notification 字段、
  useHeaderBar shouldShowNotification
- art-header-bar 通知按钮/挂载/showNotice/visibleNotice/
  bodyCloseNotice 与 document 点击监听、.notice-button 样式
- components.d.ts 两条目;crypto-js 依赖全套(env.d.ts 声明/
  package.json/lockfile/optimizeDeps)——唯一消费方是锁屏
- en/zh 孤儿语言键(notice.* / lockScreen.* / topBar.user.lockScreen)
  与 mittBus openLockScreen 事件类型(无发射/订阅方)

grep 零残留;vitest 164 passed;build 通过(主 chunk 358.45→355.84 kB,
crypto-js 出预构建);auto-imports.d.ts 行尾漂移已还原。
CLAUDE.md Phase 4 清理单 6/7 结项(并补记 8/9 随 Task 5E/5D 结项)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-14 10:43:13 +08:00
parent b02326036f
commit 7cdb4fc970
16 changed files with 5 additions and 1128 deletions
+4 -4
View File
@@ -147,10 +147,10 @@
3. 生产反代需规划 admin 域的头像/品牌公开端点:`/api/public/*` 按租户 Host 解析,admin 域直挂必 404——开发期靠 Vite Host 改写代理,生产要么同源反代改 Host(单租户形态)要么前端改用租户域名绝对 URL。
4. `VITE_TENANT_PREVIEW_HOST` 单一预览域名:开发代理只指向一个租户 Host,切换租户时头像等公开资源预览不准(.env.development 已注记)。
5. ~~resolver 平台默认回退链接入 platform_settings~~(Phase 4 Task 5A 完成:`app/platform/settings_store.py` 内存快照 + lifespan 装载 + PUT settings 后 refresh 热刷新;config_views 的 email/ai 两组改三级回退(租户槽位 → 平台设置快照 → env),push 无平台推荐键保持两级;平台设置页"仅存储"注记移除)。
6. **email/test 与 push/test 速率限制**(终审 I-3 记档):两测试端点无限流,复用登录限流器模式收口。
7. **锁屏功能已摘除入口**(终审 M-1):用户菜单项删除 + 全局组件 enabled:false 不再挂载(AES 存 localStorage + 公开模板默认密钥,防御价值为零);代码待 Phase 4 决定删否。
8. **租户切换器 composable 收拢**(终审 M-3 不做记档):仪表盘/会话/配置三页各自内联实现同一模式,收拢归后续。
9. **ECharts 按需收窄**(终审 M-4 不做记档):stats 的 plugins/echarts.ts 全量注册,bundle 体积优化归后续。
6. ~~email/test 与 push/test 速率限制~~(Phase 4 Task 5B 完成:config_routes 内 `SlidingWindowCounter` 滑动窗限流(登录限流器同款风格),每租户每类端点 15 分钟 5 次真实出站,超限 429;校验失败/未配置 400 不消耗预算,超限后 429 优先于一切预检)。
7. ~~锁屏功能已摘除入口~~ + ~~通知组件摘除~~(锁屏 Phase 3 摘入口,Phase 4 Task 5F 删代码:art-screen-lock/art-notification 组件目录、component.ts/headerBar.ts 配置项、useHeaderBar.shouldShowNotification、类型字段、components.d.ts 条目、crypto-js 依赖(env.d.ts 声明/package.json/optimizeDeps/lockfile)、en/zh 孤儿语言键全部清除,grep 零残留)。
8. ~~租户切换器 composable 收拢~~(Phase 4 Task 5E 完成:`admin/src/composables/useTenantOptions.ts` 收拢会话/配置/统计三页租户下拉(默认形态选中首个 + bootstrapTenantContext 引导;stats 用 selectFirst:false 纯数据源);dashboard 卡片墙形态不在此列)。
9. ~~ECharts 按需收窄~~(Phase 4 Task 5D 完成:echarts.use 收窄为 Bar/Line/Pie + Title/Tooltip/Grid/Legend + CanvasRenderer——全仓 grep 证实存活消费方仅 stats 页与 useChart;echarts chunk 767,864 B → 559,737 B(-27%),dist 总体积 -273 KB)。
## 文档索引
-1
View File
@@ -36,7 +36,6 @@
"@vue/reactivity": "^3.5.21",
"@vueuse/core": "^13.9.0",
"axios": "^1.12.2",
"crypto-js": "^4.2.0",
"echarts": "^6.0.0",
"element-plus": "^2.11.2",
"mitt": "^3.0.1",
-8
View File
@@ -26,9 +26,6 @@ importers:
axios:
specifier: ^1.12.2
version: 1.12.2
crypto-js:
specifier: ^4.2.0
version: 4.2.0
echarts:
specifier: ^6.0.0
version: 6.0.0
@@ -1375,9 +1372,6 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
css-functions-list@3.2.3:
resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==}
engines: {node: '>=12 || >=16'}
@@ -4224,8 +4218,6 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
crypto-js@4.2.0: {}
css-functions-list@3.2.3: {}
css-tree@3.1.0:
@@ -109,16 +109,6 @@
</template>
</ElDropdown>
<!-- 通知按钮 -->
<ArtIconButton
v-if="shouldShowNotification"
icon="ri:notification-2-line"
class="notice-button relative"
@click="visibleNotice"
>
<div class="absolute top-2 right-2 size-1.5 !bg-danger rounded-full"></div>
</ArtIconButton>
<!-- 聊天按钮 -->
<ArtIconButton
v-if="shouldShowChat"
@@ -162,9 +152,6 @@
<!-- 标签页 -->
<ArtWorkTab />
<!-- 通知 -->
<ArtNotification v-model:value="showNotice" ref="notice" />
</div>
</template>
@@ -205,7 +192,6 @@
shouldShowBreadcrumb,
shouldShowGlobalSearch,
shouldShowFullscreen,
shouldShowNotification,
shouldShowChat,
shouldShowLanguage,
shouldShowSettings,
@@ -219,9 +205,6 @@
const { language } = storeToRefs(userStore)
const { menuList } = storeToRefs(menuStore)
const showNotice = ref(false)
const notice = ref(null)
// 菜单类型判断
const isLeftMenu = computed(() => menuType.value === MenuTypeEnum.LEFT)
const isDualMenu = computed(() => menuType.value === MenuTypeEnum.DUAL_MENU)
@@ -232,11 +215,6 @@
onMounted(() => {
initLanguage()
document.addEventListener('click', bodyCloseNotice)
})
onUnmounted(() => {
document.removeEventListener('click', bodyCloseNotice)
})
/**
@@ -310,31 +288,6 @@
mittBus.emit('openSearchDialog')
}
/**
* 点击页面其他区域关闭通知面板
* @param {Event} e - 点击事件对象
*/
const bodyCloseNotice = (e: any): void => {
if (!showNotice.value) return
const target = e.target as HTMLElement
// 检查是否点击了通知按钮或通知面板内部
const isNoticeButton = target.closest('.notice-button')
const isNoticePanel = target.closest('.art-notification-panel')
if (!isNoticeButton && !isNoticePanel) {
showNotice.value = false
}
}
/**
* 切换通知面板显示状态
*/
const visibleNotice = (): void => {
showNotice.value = !showNotice.value
}
/**
* 打开聊天窗口
*/
@@ -457,10 +410,6 @@
animation: shrink 0.6s forwards;
}
.notice-button:hover :deep(.art-svg-icon) {
animation: shake 0.5s ease-in-out;
}
.chat-button:hover :deep(.art-svg-icon) {
animation: shake 0.5s ease-in-out;
}
@@ -1,456 +0,0 @@
<!-- 通知组件 -->
<template>
<div
class="art-notification-panel art-card-sm !shadow-xl"
:style="{
transform: show ? 'scaleY(1)' : 'scaleY(0.9)',
opacity: show ? 1 : 0
}"
v-show="visible"
@click.stop
>
<div class="flex-cb px-3.5 mt-3.5">
<span class="text-base font-medium text-g-800">{{ $t('notice.title') }}</span>
<span class="text-xs text-g-800 px-1.5 py-1 c-p select-none rounded hover:bg-g-200">
{{ $t('notice.btnRead') }}
</span>
</div>
<ul class="box-border flex items-end w-full h-12.5 px-3.5 border-b-d">
<li
v-for="(item, index) in barList"
:key="index"
class="h-12 leading-12 mr-5 overflow-hidden text-[13px] text-g-700 c-p select-none"
:class="{ 'bar-active': barActiveIndex === index }"
@click="changeBar(index)"
>
{{ item.name }} ({{ item.num }})
</li>
</ul>
<div class="w-full h-[calc(100%-95px)]">
<div class="h-[calc(100%-60px)] overflow-y-scroll scrollbar-thin">
<!-- 通知 -->
<ul v-show="barActiveIndex === 0">
<li
v-for="(item, index) in noticeList"
:key="index"
class="box-border flex-c px-3.5 py-3.5 c-p last:border-b-0 hover:bg-g-200/60"
>
<div
class="size-9 leading-9 text-center rounded-lg flex-cc"
:class="[getNoticeStyle(item.type).iconClass]"
>
<ArtSvgIcon class="text-lg !bg-transparent" :icon="getNoticeStyle(item.type).icon" />
</div>
<div class="w-[calc(100%-45px)] ml-3.5">
<h4 class="text-sm font-normal leading-5.5 text-g-900">{{ item.title }}</h4>
<p class="mt-1.5 text-xs text-g-500">{{ item.time }}</p>
</div>
</li>
</ul>
<!-- 消息 -->
<ul v-show="barActiveIndex === 1">
<li
v-for="(item, index) in msgList"
:key="index"
class="box-border flex-c px-3.5 py-3.5 c-p last:border-b-0 hover:bg-g-200/60"
>
<div class="w-9 h-9">
<img :src="item.avatar" class="w-full h-full rounded-lg" />
</div>
<div class="w-[calc(100%-45px)] ml-3.5">
<h4 class="text-xs font-normal leading-5.5">{{ item.title }}</h4>
<p class="mt-1.5 text-xs text-g-500">{{ item.time }}</p>
</div>
</li>
</ul>
<!-- 待办 -->
<ul v-show="barActiveIndex === 2">
<li
v-for="(item, index) in pendingList"
:key="index"
class="box-border px-5 py-3.5 last:border-b-0"
>
<h4>{{ item.title }}</h4>
<p class="text-xs text-g-500">{{ item.time }}</p>
</li>
</ul>
<!-- 空状态 -->
<div
v-show="currentTabIsEmpty"
class="relative top-25 h-full text-g-500 text-center !bg-transparent"
>
<ArtSvgIcon icon="system-uicons:inbox" class="text-5xl" />
<p class="mt-3.5 text-xs !bg-transparent"
>{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p
>
</div>
</div>
<div class="relative box-border w-full px-3.5">
<ElButton class="w-full mt-3" @click="handleViewAll" v-ripple>
{{ $t('notice.viewAll') }}
</ElButton>
</div>
</div>
<div class="h-25"></div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch, type Ref, type ComputedRef } from 'vue'
import { useI18n } from 'vue-i18n'
// 导入头像图片
import avatar1 from '@/assets/images/avatar/avatar1.webp'
import avatar2 from '@/assets/images/avatar/avatar2.webp'
import avatar3 from '@/assets/images/avatar/avatar3.webp'
import avatar4 from '@/assets/images/avatar/avatar4.webp'
import avatar5 from '@/assets/images/avatar/avatar5.webp'
import avatar6 from '@/assets/images/avatar/avatar6.webp'
defineOptions({ name: 'ArtNotification' })
interface NoticeItem {
/** 标题 */
title: string
/** 时间 */
time: string
/** 类型 */
type: NoticeType
}
interface MessageItem {
/** 标题 */
title: string
/** 时间 */
time: string
/** 头像 */
avatar: string
}
interface PendingItem {
/** 标题 */
title: string
/** 时间 */
time: string
}
interface BarItem {
/** 名称 */
name: ComputedRef<string>
/** 数量 */
num: number
}
interface NoticeStyle {
/** 图标 */
icon: string
/** icon 样式 */
iconClass: string
}
type NoticeType = 'email' | 'message' | 'collection' | 'user' | 'notice'
const { t } = useI18n()
const props = defineProps<{
value: boolean
}>()
const emit = defineEmits<{
'update:value': [value: boolean]
}>()
const show = ref(false)
const visible = ref(false)
const barActiveIndex = ref(0)
const useNotificationData = () => {
// 通知数据
const noticeList = ref<NoticeItem[]>([
{
title: '新增国际化',
time: '2024-6-13 0:10',
type: 'notice'
},
{
title: '冷月呆呆给你发了一条消息',
time: '2024-4-21 8:05',
type: 'message'
},
{
title: '小肥猪关注了你',
time: '2020-3-17 21:12',
type: 'collection'
},
{
title: '新增使用文档',
time: '2024-02-14 0:20',
type: 'notice'
},
{
title: '小肥猪给你发了一封邮件',
time: '2024-1-20 0:15',
type: 'email'
},
{
title: '菜单mock本地真实数据',
time: '2024-1-17 22:06',
type: 'notice'
}
])
// 消息数据
const msgList = ref<MessageItem[]>([
{
title: '池不胖 关注了你',
time: '2021-2-26 23:50',
avatar: avatar1
},
{
title: '唐不苦 关注了你',
time: '2021-2-21 8:05',
avatar: avatar2
},
{
title: '中小鱼 关注了你',
time: '2020-1-17 21:12',
avatar: avatar3
},
{
title: '何小荷 关注了你',
time: '2021-01-14 0:20',
avatar: avatar4
},
{
title: '誶誶淰 关注了你',
time: '2020-12-20 0:15',
avatar: avatar5
},
{
title: '冷月呆呆 关注了你',
time: '2020-12-17 22:06',
avatar: avatar6
}
])
// 待办数据
const pendingList = ref<PendingItem[]>([])
// 标签栏数据
const barList = computed<BarItem[]>(() => [
{
name: computed(() => t('notice.bar[0]')),
num: noticeList.value.length
},
{
name: computed(() => t('notice.bar[1]')),
num: msgList.value.length
},
{
name: computed(() => t('notice.bar[2]')),
num: pendingList.value.length
}
])
return {
noticeList,
msgList,
pendingList,
barList
}
}
// 样式管理
const useNotificationStyles = () => {
const noticeStyleMap: Record<NoticeType, NoticeStyle> = {
email: {
icon: 'ri:mail-line',
iconClass: 'bg-warning/12 text-warning'
},
message: {
icon: 'ri:volume-down-line',
iconClass: 'bg-success/12 text-success'
},
collection: {
icon: 'ri:heart-3-line',
iconClass: 'bg-danger/12 text-danger'
},
user: {
icon: 'ri:volume-down-line',
iconClass: 'bg-info/12 text-info'
},
notice: {
icon: 'ri:notification-3-line',
iconClass: 'bg-theme/12 text-theme'
}
}
const getNoticeStyle = (type: NoticeType): NoticeStyle => {
const defaultStyle: NoticeStyle = {
icon: 'ri:arrow-right-circle-line',
iconClass: 'bg-theme/12 text-theme'
}
return noticeStyleMap[type] || defaultStyle
}
return {
getNoticeStyle
}
}
// 动画管理
const useNotificationAnimation = () => {
const showNotice = (open: boolean) => {
if (open) {
visible.value = true
setTimeout(() => {
show.value = true
}, 5)
} else {
show.value = false
setTimeout(() => {
visible.value = false
}, 350)
}
}
return {
showNotice
}
}
// 标签页管理
const useTabManagement = (
noticeList: Ref<NoticeItem[]>,
msgList: Ref<MessageItem[]>,
pendingList: Ref<PendingItem[]>,
businessHandlers: {
handleNoticeAll: () => void
handleMsgAll: () => void
handlePendingAll: () => void
}
) => {
const changeBar = (index: number) => {
barActiveIndex.value = index
}
// 检查当前标签页是否为空
const currentTabIsEmpty = computed(() => {
const tabDataMap = [noticeList.value, msgList.value, pendingList.value]
const currentData = tabDataMap[barActiveIndex.value]
return currentData && currentData.length === 0
})
const handleViewAll = () => {
// 查看全部处理器映射
const viewAllHandlers: Record<number, () => void> = {
0: businessHandlers.handleNoticeAll,
1: businessHandlers.handleMsgAll,
2: businessHandlers.handlePendingAll
}
const handler = viewAllHandlers[barActiveIndex.value]
handler?.()
// 关闭通知面板
emit('update:value', false)
}
return {
changeBar,
currentTabIsEmpty,
handleViewAll
}
}
// 业务逻辑处理
const useBusinessLogic = () => {
const handleNoticeAll = () => {
// 处理查看全部通知
console.log('查看全部通知')
}
const handleMsgAll = () => {
// 处理查看全部消息
console.log('查看全部消息')
}
const handlePendingAll = () => {
// 处理查看全部待办
console.log('查看全部待办')
}
return {
handleNoticeAll,
handleMsgAll,
handlePendingAll
}
}
// 组合所有逻辑
const { noticeList, msgList, pendingList, barList } = useNotificationData()
const { getNoticeStyle } = useNotificationStyles()
const { showNotice } = useNotificationAnimation()
const { handleNoticeAll, handleMsgAll, handlePendingAll } = useBusinessLogic()
const { changeBar, currentTabIsEmpty, handleViewAll } = useTabManagement(
noticeList,
msgList,
pendingList,
{ handleNoticeAll, handleMsgAll, handlePendingAll }
)
// 监听属性变化
watch(
() => props.value,
(newValue) => {
showNotice(newValue)
}
)
</script>
<style scoped>
@reference '@styles/core/tailwind.css';
.art-notification-panel {
@apply absolute
top-14.5
right-5
w-90
h-125
overflow-hidden
transition-all
duration-300
origin-top
will-change-[top,left]
max-[640px]:top-[65px]
max-[640px]:right-0
max-[640px]:w-full
max-[640px]:h-[80vh];
}
.bar-active {
color: var(--theme-color) !important;
border-bottom: 2px solid var(--theme-color);
}
.scrollbar-thin::-webkit-scrollbar {
width: 5px !important;
}
.dark .scrollbar-thin::-webkit-scrollbar-track {
background-color: var(--default-box-color);
}
.dark .scrollbar-thin::-webkit-scrollbar-thumb {
background-color: #222 !important;
}
</style>
@@ -1,522 +0,0 @@
<!-- 锁屏 -->
<template>
<div class="layout-lock-screen">
<!-- 开发者工具警告覆盖层 -->
<div
v-if="showDevToolsWarning"
class="fixed top-0 left-0 z-[999999] flex-cc w-full h-full text-white bg-gradient-to-br from-[#1e1e1e] to-black animate-fade-in"
>
<div class="p-5 text-center select-none">
<div class="mb-7.5 text-5xl">🔒</div>
<h1 class="m-0 mb-5 text-3xl font-semibold text-danger">系统已锁定</h1>
<p class="max-w-125 m-0 text-lg leading-relaxed text-white">
检测到开发者工具已打开<br />
为了系统安全,请关闭开发者工具后继续使用
</p>
<div class="mt-7.5 text-sm text-gray-400">Security Lock Activated</div>
</div>
</div>
<!-- 锁屏弹窗 -->
<div v-if="!isLock">
<ElDialog v-model="visible" :width="370" :show-close="false" @open="handleDialogOpen">
<div class="flex-c flex-col">
<img class="w-16 h-16 rounded-full" src="@imgs/user/avatar.webp" alt="用户头像" />
<div class="mt-7.5 mb-3.5 text-base font-medium">{{ userInfo.userName }}</div>
<ElForm
ref="formRef"
:model="formData"
:rules="rules"
class="w-[90%]"
@submit.prevent="handleLock"
>
<ElFormItem prop="password">
<ElInput
v-model="formData.password"
type="password"
:placeholder="$t('lockScreen.lock.inputPlaceholder')"
:show-password="true"
autocomplete="new-password"
ref="lockInputRef"
class="w-full mt-9"
@keyup.enter="handleLock"
>
<template #suffix>
<ElIcon class="c-p" @click="handleLock">
<Lock />
</ElIcon>
</template>
</ElInput>
</ElFormItem>
<ElButton type="primary" class="w-full mt-0.5" @click="handleLock" v-ripple>
{{ $t('lockScreen.lock.btnText') }}
</ElButton>
</ElForm>
</div>
</ElDialog>
</div>
<!-- 解锁界面 -->
<div v-else class="unlock-content">
<div class="flex-c flex-col w-80">
<img class="w-16 h-16 mt-5 rounded-full" src="@imgs/user/avatar.webp" alt="用户头像" />
<div class="mt-3 mb-3.5 text-base font-medium">
{{ userInfo.userName }}
</div>
<ElForm
ref="unlockFormRef"
:model="unlockForm"
:rules="rules"
class="w-full !px-2.5"
@submit.prevent="handleUnlock"
>
<ElFormItem prop="password">
<ElInput
v-model="unlockForm.password"
type="password"
:placeholder="$t('lockScreen.unlock.inputPlaceholder')"
:show-password="true"
autocomplete="new-password"
ref="unlockInputRef"
class="mt-5"
>
<template #suffix>
<ElIcon class="c-p" @click="handleUnlock">
<Unlock />
</ElIcon>
</template>
</ElInput>
</ElFormItem>
<ElButton type="primary" class="w-full mt-2" @click="handleUnlock" v-ripple>
{{ $t('lockScreen.unlock.btnText') }}
</ElButton>
<div class="w-full text-center">
<ElButton
text
class="mt-2.5 !text-g-600 hover:!text-theme hover:!bg-transparent"
@click="toLogin"
>
{{ $t('lockScreen.unlock.backBtnText') }}
</ElButton>
</div>
</ElForm>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Lock, Unlock } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import { useI18n } from 'vue-i18n'
import CryptoJS from 'crypto-js'
import { useUserStore } from '@/store/modules/user'
import { useAuthStore } from '@/store/modules/auth'
import { mittBus } from '@/utils/sys'
// 国际化
const { t } = useI18n()
// 环境变量
const ENCRYPT_KEY = import.meta.env.VITE_LOCK_ENCRYPT_KEY
// Store
const userStore = useUserStore()
const authStore = useAuthStore()
const { info: userInfo, lockPassword, isLock } = storeToRefs(userStore)
// 响应式数据
const visible = ref<boolean>(false)
const lockInputRef = ref<any>(null)
const unlockInputRef = ref<any>(null)
const showDevToolsWarning = ref<boolean>(false)
// 表单相关
const formRef = ref<FormInstance>()
const unlockFormRef = ref<FormInstance>()
const formData = reactive({
password: ''
})
const unlockForm = reactive({
password: ''
})
// 表单验证规则
const rules = computed<FormRules>(() => ({
password: [
{
required: true,
message: t('lockScreen.lock.inputPlaceholder'),
trigger: 'blur'
}
]
}))
// 检测是否为移动设备
const isMobile = () => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
)
}
// 添加禁用控制台的函数
const disableDevTools = () => {
// 禁用右键菜单
const handleContextMenu = (e: Event) => {
if (isLock.value) {
e.preventDefault()
e.stopPropagation()
return false
}
}
document.addEventListener('contextmenu', handleContextMenu, true)
// 禁用开发者工具相关快捷键
const handleKeyDown = (e: KeyboardEvent) => {
if (!isLock.value) return
// 禁用 F12
if (e.key === 'F12') {
e.preventDefault()
e.stopPropagation()
return false
}
// 禁用 Ctrl+Shift+I/J/C/K (开发者工具)
if (e.ctrlKey && e.shiftKey) {
const key = e.key.toLowerCase()
if (['i', 'j', 'c', 'k'].includes(key)) {
e.preventDefault()
e.stopPropagation()
return false
}
}
// 禁用 Ctrl+U (查看源代码)
if (e.ctrlKey && e.key.toLowerCase() === 'u') {
e.preventDefault()
e.stopPropagation()
return false
}
// 禁用 Ctrl+S (保存页面)
if (e.ctrlKey && e.key.toLowerCase() === 's') {
e.preventDefault()
e.stopPropagation()
return false
}
// 禁用 Ctrl+A (全选)
if (e.ctrlKey && e.key.toLowerCase() === 'a') {
e.preventDefault()
e.stopPropagation()
return false
}
// 禁用 Ctrl+P (打印)
if (e.ctrlKey && e.key.toLowerCase() === 'p') {
e.preventDefault()
e.stopPropagation()
return false
}
// 禁用 Ctrl+F (查找)
if (e.ctrlKey && e.key.toLowerCase() === 'f') {
e.preventDefault()
e.stopPropagation()
return false
}
// 禁用 Alt+Tab (切换窗口)
if (e.altKey && e.key === 'Tab') {
e.preventDefault()
e.stopPropagation()
return false
}
// 禁用 Ctrl+Tab (切换标签页)
if (e.ctrlKey && e.key === 'Tab') {
e.preventDefault()
e.stopPropagation()
return false
}
// 禁用 Ctrl+W (关闭标签页)
if (e.ctrlKey && e.key.toLowerCase() === 'w') {
e.preventDefault()
e.stopPropagation()
return false
}
// 禁用 Ctrl+R 和 F5 (刷新页面)
if ((e.ctrlKey && e.key.toLowerCase() === 'r') || e.key === 'F5') {
e.preventDefault()
e.stopPropagation()
return false
}
// 禁用 Ctrl+Shift+R (强制刷新)
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'r') {
e.preventDefault()
e.stopPropagation()
return false
}
}
document.addEventListener('keydown', handleKeyDown, true)
// 禁用选择文本
const handleSelectStart = (e: Event) => {
if (isLock.value) {
e.preventDefault()
return false
}
}
document.addEventListener('selectstart', handleSelectStart, true)
// 禁用拖拽
const handleDragStart = (e: Event) => {
if (isLock.value) {
e.preventDefault()
return false
}
}
document.addEventListener('dragstart', handleDragStart, true)
// 监听开发者工具打开状态(仅在桌面端启用)
let devtools = { open: false }
const threshold = 160
let devToolsInterval: ReturnType<typeof setInterval> | null = null
const checkDevTools = () => {
if (!isLock.value || isMobile()) return
const isDevToolsOpen =
window.outerHeight - window.innerHeight > threshold ||
window.outerWidth - window.innerWidth > threshold
if (isDevToolsOpen && !devtools.open) {
devtools.open = true
showDevToolsWarning.value = true
} else if (!isDevToolsOpen && devtools.open) {
devtools.open = false
showDevToolsWarning.value = false
}
}
// 仅在桌面端启用开发者工具检测
if (!isMobile()) {
devToolsInterval = setInterval(checkDevTools, 500)
}
// 返回清理函数
return () => {
document.removeEventListener('contextmenu', handleContextMenu, true)
document.removeEventListener('keydown', handleKeyDown, true)
document.removeEventListener('selectstart', handleSelectStart, true)
document.removeEventListener('dragstart', handleDragStart, true)
if (devToolsInterval) {
clearInterval(devToolsInterval)
}
}
}
// 工具函数
const verifyPassword = (inputPassword: string, storedPassword: string): boolean => {
try {
const decryptedPassword = CryptoJS.AES.decrypt(storedPassword, ENCRYPT_KEY).toString(
CryptoJS.enc.Utf8
)
return inputPassword === decryptedPassword
} catch (error) {
console.error('密码解密失败:', error)
return false
}
}
// 事件处理函数
const handleKeydown = (event: KeyboardEvent) => {
if (event.altKey && event.key.toLowerCase() === '¬') {
event.preventDefault()
visible.value = true
}
}
const handleDialogOpen = () => {
setTimeout(() => {
lockInputRef.value?.input?.focus()
}, 100)
}
const handleLock = async () => {
if (!formRef.value) return
await formRef.value.validate((valid, fields) => {
if (valid) {
const encryptedPassword = CryptoJS.AES.encrypt(formData.password, ENCRYPT_KEY).toString()
userStore.setLockStatus(true)
userStore.setLockPassword(encryptedPassword)
visible.value = false
formData.password = ''
} else {
console.error('表单验证失败:', fields)
}
})
}
const handleUnlock = async () => {
if (!unlockFormRef.value) return
await unlockFormRef.value.validate((valid, fields) => {
if (valid) {
const isValid = verifyPassword(unlockForm.password, lockPassword.value)
if (isValid) {
try {
userStore.setLockStatus(false)
userStore.setLockPassword('')
unlockForm.password = ''
visible.value = false
showDevToolsWarning.value = false
} catch (error) {
console.error('更新store失败:', error)
}
} else {
// 触发抖动动画
const inputElement = unlockInputRef.value?.$el
if (inputElement) {
inputElement.classList.add('shake-animation')
setTimeout(() => {
inputElement.classList.remove('shake-animation')
}, 300)
}
ElMessage.error(t('lockScreen.pwdError'))
unlockForm.password = ''
}
} else {
console.error('表单验证失败:', fields)
}
})
}
const toLogin = () => {
// 锁屏页"返回登录":先撤销后端 token,再走本地清理 + 跳登录
void authStore.logout()
}
const openLockScreen = () => {
visible.value = true
}
// 监听锁屏状态变化
watch(isLock, (newValue) => {
if (newValue) {
document.body.style.overflow = 'hidden'
setTimeout(() => {
unlockInputRef.value?.input?.focus()
}, 100)
} else {
document.body.style.overflow = 'auto'
showDevToolsWarning.value = false
}
})
// 存储清理函数
let cleanupDevTools: (() => void) | null = null
// 生命周期钩子
onMounted(() => {
mittBus.on('openLockScreen', openLockScreen)
document.addEventListener('keydown', handleKeydown)
if (isLock.value) {
visible.value = true
setTimeout(() => {
unlockInputRef.value?.input?.focus()
}, 100)
}
// 初始化禁用开发者工具功能
cleanupDevTools = disableDevTools()
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
document.body.style.overflow = 'auto'
// 清理禁用开发者工具的事件监听器
if (cleanupDevTools) {
cleanupDevTools()
cleanupDevTools = null
}
})
</script>
<style lang="scss" scoped>
.layout-lock-screen :deep(.el-dialog) {
border-radius: 10px;
}
.unlock-content {
position: fixed;
inset: 0;
z-index: 2500;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background-color: #fff;
background-image: url('@imgs/lock/bg_light.webp');
background-size: cover;
transition: transform 0.3s ease-in-out;
}
.dark {
.unlock-content {
background-image: url('@imgs/lock/bg_dark.webp');
}
}
@keyframes fade-in {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.animate-fade-in {
animation: fade-in 0.3s ease-in-out;
}
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
10%,
30%,
50%,
70%,
90% {
transform: translateX(-10px);
}
20%,
40%,
60%,
80% {
transform: translateX(10px);
}
}
.shake-animation {
animation: shake 0.5s ease-in-out;
}
</style>
-11
View File
@@ -37,17 +37,6 @@ export const globalComponentsConfig: GlobalComponentConfig[] = [
),
enabled: true
},
{
name: '锁屏',
key: 'screen-lock',
component: defineAsyncComponent(
() => import('@/components/core/layouts/art-screen-lock/index.vue')
),
// 入口已摘除(Phase 3 终审 M-1):锁屏把真实密码 AES 存 localStorage
// 且密钥是公开模板默认值,防御价值为零——enabled: false 连全局挂载
// 也不注册;组件代码保留,Phase 4 决定删否。
enabled: false
},
{
name: '聊天窗口',
key: 'chat-window',
-6
View File
@@ -38,12 +38,6 @@ export const headerBarConfig: HeaderBarFeatureConfig = {
enabled: true,
description: '全屏切换功能'
},
notification: {
// 入口已摘除(Phase 3 终审 M-2):模板通知中心是硬编码演示数据,
// 无真实数据源——enabled: false 隐藏铃铛;组件保留,接真数据源时再开。
enabled: false,
description: '通知中心,显示系统通知和消息'
},
chat: {
enabled: true,
description: '聊天功能,提供实时沟通'
-2
View File
@@ -2,7 +2,5 @@
declare module 'nprogress'
declare module 'crypto-js'
// 全局变量声明
declare const __APP_VERSION__: string // 版本号
-6
View File
@@ -83,11 +83,6 @@ export function useHeaderBar() {
return isFeatureEnabled('fullscreen')
})
// 检查通知中心是否显示
const shouldShowNotification = computed(() => {
return isFeatureEnabled('notification')
})
// 检查聊天功能是否显示
const shouldShowChat = computed(() => {
return isFeatureEnabled('chat')
@@ -179,7 +174,6 @@ export function useHeaderBar() {
shouldShowBreadcrumb, // 是否显示面包屑
shouldShowGlobalSearch, // 是否显示全局搜索
shouldShowFullscreen, // 是否显示全屏按钮
shouldShowNotification, // 是否显示通知中心
shouldShowChat, // 是否显示聊天功能
shouldShowLanguage, // 是否显示语言切换
shouldShowSettings, // 是否显示设置面板
-26
View File
@@ -20,7 +20,6 @@
},
"user": {
"changePassword": "Change Password",
"lockScreen": "Lock Screen",
"logout": "Log Out"
},
"guide": {
@@ -122,19 +121,6 @@
"resetFailed": "Reset failed, please refresh the page and try again"
}
},
"notice": {
"title": "Notice",
"btnRead": "Mark as read",
"bar": [
"Notice",
"Message",
"Todo"
],
"text": [
"No"
],
"viewAll": "View all"
},
"worktab": {
"btn": {
"refresh": "Refresh",
@@ -167,18 +153,6 @@
"message": "Welcome back"
}
},
"lockScreen": {
"pwdError": "Password error",
"lock": {
"inputPlaceholder": "Please input lock screen password",
"btnText": "Lock"
},
"unlock": {
"inputPlaceholder": "Please input unlock password",
"btnText": "Unlock",
"backBtnText": "Back to login"
}
},
"greeting": {
"dawn": "Good morning!",
"morning": "Good morning!",
-26
View File
@@ -20,7 +20,6 @@
},
"user": {
"changePassword": "修改密码",
"lockScreen": "锁定屏幕",
"logout": "退出登录"
},
"guide": {
@@ -122,19 +121,6 @@
"resetFailed": "重置失败,请刷新页面后重试"
}
},
"notice": {
"title": "通知",
"btnRead": "标为已读",
"bar": [
"通知",
"消息",
"代办"
],
"text": [
"暂无"
],
"viewAll": "查看全部"
},
"worktab": {
"btn": {
"refresh": "刷新",
@@ -167,18 +153,6 @@
"message": "欢迎回来"
}
},
"lockScreen": {
"pwdError": "密码错误",
"lock": {
"inputPlaceholder": "请输入锁屏密码",
"btnText": "锁定"
},
"unlock": {
"inputPlaceholder": "请输入解锁密码",
"btnText": "解锁",
"backBtnText": "返回登录"
}
},
"greeting": {
"dawn": "凌晨了!",
"morning": "上午好!",
-2
View File
@@ -198,8 +198,6 @@ export interface HeaderBarFeatureConfig {
globalSearch: FeatureConfigItem
/** 全屏功能 */
fullscreen: FeatureConfigItem
/** 通知功能 */
notification: FeatureConfigItem
/** 聊天功能 */
chat: FeatureConfigItem
/** 多语言切换 */
-2
View File
@@ -40,14 +40,12 @@ declare module 'vue' {
ArtLogo: typeof import('./../../components/core/base/art-logo/index.vue')['default']
ArtMenuRight: typeof import('./../../components/core/others/art-menu-right/index.vue')['default']
ArtMixedMenu: typeof import('./../../components/core/layouts/art-menus/art-mixed-menu/index.vue')['default']
ArtNotification: typeof import('./../../components/core/layouts/art-notification/index.vue')['default']
ArtPageContent: typeof import('./../../components/core/layouts/art-page-content/index.vue')['default']
ArtProgressCard: typeof import('./../../components/core/cards/art-progress-card/index.vue')['default']
ArtRadarChart: typeof import('./../../components/core/charts/art-radar-chart/index.vue')['default']
ArtResultPage: typeof import('./../../components/core/views/result/ArtResultPage.vue')['default']
ArtRingChart: typeof import('./../../components/core/charts/art-ring-chart/index.vue')['default']
ArtScatterChart: typeof import('./../../components/core/charts/art-scatter-chart/index.vue')['default']
ArtScreenLock: typeof import('./../../components/core/layouts/art-screen-lock/index.vue')['default']
ArtSearchBar: typeof import('./../../components/core/forms/art-search-bar/index.vue')['default']
ArtSettingsPanel: typeof import('./../../components/core/layouts/art-settings-panel/index.vue')['default']
ArtSidebarMenu: typeof import('./../../components/core/layouts/art-menus/art-sidebar-menu/index.vue')['default']
+1 -4
View File
@@ -13,7 +13,7 @@
* ## 使用场景
*
* - 跨层级组件通信
* - 全局功能触发(设置、搜索、聊天、锁屏等)
* - 全局功能触发(设置、搜索、聊天等)
* - 特效触发(烟花效果)
* - 避免 props 层层传递
*
@@ -36,7 +36,6 @@
* - openSetting: 打开设置面板
* - openSearchDialog: 打开搜索对话框
* - openChat: 打开聊天窗口
* - openLockScreen: 打开锁屏
*
* @module utils/sys/mittBus
* @author Art Design Pro Team
@@ -53,8 +52,6 @@ type Events = {
openSearchDialog: void
// 打开聊天窗口事件 - 无参数
openChat: void
// 打开锁屏事件 - 无参数
openLockScreen: void
}
// 创建类型安全的事件总线实例
-1
View File
@@ -139,7 +139,6 @@ export default ({ mode }: { mode: string }) => {
'echarts/charts',
'echarts/components',
'echarts/renderers',
'crypto-js',
'element-plus/es',
'element-plus/es/components/*/style/css',
'element-plus/es/components/*/style/index'