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:
@@ -147,10 +147,10 @@
|
|||||||
3. 生产反代需规划 admin 域的头像/品牌公开端点:`/api/public/*` 按租户 Host 解析,admin 域直挂必 404——开发期靠 Vite Host 改写代理,生产要么同源反代改 Host(单租户形态)要么前端改用租户域名绝对 URL。
|
3. 生产反代需规划 admin 域的头像/品牌公开端点:`/api/public/*` 按租户 Host 解析,admin 域直挂必 404——开发期靠 Vite Host 改写代理,生产要么同源反代改 Host(单租户形态)要么前端改用租户域名绝对 URL。
|
||||||
4. `VITE_TENANT_PREVIEW_HOST` 单一预览域名:开发代理只指向一个租户 Host,切换租户时头像等公开资源预览不准(.env.development 已注记)。
|
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 无平台推荐键保持两级;平台设置页"仅存储"注记移除)。
|
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 记档):两测试端点无限流,复用登录限流器模式收口。
|
6. ~~email/test 与 push/test 速率限制~~(Phase 4 Task 5B 完成:config_routes 内 `SlidingWindowCounter` 滑动窗限流(登录限流器同款风格),每租户每类端点 15 分钟 5 次真实出站,超限 429;校验失败/未配置 400 不消耗预算,超限后 429 优先于一切预检)。
|
||||||
7. **锁屏功能已摘除入口**(终审 M-1):用户菜单项删除 + 全局组件 enabled:false 不再挂载(AES 存 localStorage + 公开模板默认密钥,防御价值为零);代码待 Phase 4 决定删否。
|
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 收拢**(终审 M-3 不做记档):仪表盘/会话/配置三页各自内联实现同一模式,收拢归后续。
|
8. ~~租户切换器 composable 收拢~~(Phase 4 Task 5E 完成:`admin/src/composables/useTenantOptions.ts` 收拢会话/配置/统计三页租户下拉(默认形态选中首个 + bootstrapTenantContext 引导;stats 用 selectFirst:false 纯数据源);dashboard 卡片墙形态不在此列)。
|
||||||
9. **ECharts 按需收窄**(终审 M-4 不做记档):stats 的 plugins/echarts.ts 全量注册,bundle 体积优化归后续。
|
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)。
|
||||||
|
|
||||||
## 文档索引
|
## 文档索引
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
"@vue/reactivity": "^3.5.21",
|
"@vue/reactivity": "^3.5.21",
|
||||||
"@vueuse/core": "^13.9.0",
|
"@vueuse/core": "^13.9.0",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"crypto-js": "^4.2.0",
|
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.11.2",
|
"element-plus": "^2.11.2",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
|
|||||||
Generated
-8
@@ -26,9 +26,6 @@ importers:
|
|||||||
axios:
|
axios:
|
||||||
specifier: ^1.12.2
|
specifier: ^1.12.2
|
||||||
version: 1.12.2
|
version: 1.12.2
|
||||||
crypto-js:
|
|
||||||
specifier: ^4.2.0
|
|
||||||
version: 4.2.0
|
|
||||||
echarts:
|
echarts:
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
@@ -1375,9 +1372,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
crypto-js@4.2.0:
|
|
||||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
|
||||||
|
|
||||||
css-functions-list@3.2.3:
|
css-functions-list@3.2.3:
|
||||||
resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==}
|
resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==}
|
||||||
engines: {node: '>=12 || >=16'}
|
engines: {node: '>=12 || >=16'}
|
||||||
@@ -4224,8 +4218,6 @@ snapshots:
|
|||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
crypto-js@4.2.0: {}
|
|
||||||
|
|
||||||
css-functions-list@3.2.3: {}
|
css-functions-list@3.2.3: {}
|
||||||
|
|
||||||
css-tree@3.1.0:
|
css-tree@3.1.0:
|
||||||
|
|||||||
@@ -109,16 +109,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</ElDropdown>
|
</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
|
<ArtIconButton
|
||||||
v-if="shouldShowChat"
|
v-if="shouldShowChat"
|
||||||
@@ -162,9 +152,6 @@
|
|||||||
|
|
||||||
<!-- 标签页 -->
|
<!-- 标签页 -->
|
||||||
<ArtWorkTab />
|
<ArtWorkTab />
|
||||||
|
|
||||||
<!-- 通知 -->
|
|
||||||
<ArtNotification v-model:value="showNotice" ref="notice" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -205,7 +192,6 @@
|
|||||||
shouldShowBreadcrumb,
|
shouldShowBreadcrumb,
|
||||||
shouldShowGlobalSearch,
|
shouldShowGlobalSearch,
|
||||||
shouldShowFullscreen,
|
shouldShowFullscreen,
|
||||||
shouldShowNotification,
|
|
||||||
shouldShowChat,
|
shouldShowChat,
|
||||||
shouldShowLanguage,
|
shouldShowLanguage,
|
||||||
shouldShowSettings,
|
shouldShowSettings,
|
||||||
@@ -219,9 +205,6 @@
|
|||||||
const { language } = storeToRefs(userStore)
|
const { language } = storeToRefs(userStore)
|
||||||
const { menuList } = storeToRefs(menuStore)
|
const { menuList } = storeToRefs(menuStore)
|
||||||
|
|
||||||
const showNotice = ref(false)
|
|
||||||
const notice = ref(null)
|
|
||||||
|
|
||||||
// 菜单类型判断
|
// 菜单类型判断
|
||||||
const isLeftMenu = computed(() => menuType.value === MenuTypeEnum.LEFT)
|
const isLeftMenu = computed(() => menuType.value === MenuTypeEnum.LEFT)
|
||||||
const isDualMenu = computed(() => menuType.value === MenuTypeEnum.DUAL_MENU)
|
const isDualMenu = computed(() => menuType.value === MenuTypeEnum.DUAL_MENU)
|
||||||
@@ -232,11 +215,6 @@
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initLanguage()
|
initLanguage()
|
||||||
document.addEventListener('click', bodyCloseNotice)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
document.removeEventListener('click', bodyCloseNotice)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -310,31 +288,6 @@
|
|||||||
mittBus.emit('openSearchDialog')
|
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;
|
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) {
|
.chat-button:hover :deep(.art-svg-icon) {
|
||||||
animation: shake 0.5s ease-in-out;
|
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>
|
|
||||||
@@ -37,17 +37,6 @@ export const globalComponentsConfig: GlobalComponentConfig[] = [
|
|||||||
),
|
),
|
||||||
enabled: true
|
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: '聊天窗口',
|
name: '聊天窗口',
|
||||||
key: 'chat-window',
|
key: 'chat-window',
|
||||||
|
|||||||
@@ -38,12 +38,6 @@ export const headerBarConfig: HeaderBarFeatureConfig = {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
description: '全屏切换功能'
|
description: '全屏切换功能'
|
||||||
},
|
},
|
||||||
notification: {
|
|
||||||
// 入口已摘除(Phase 3 终审 M-2):模板通知中心是硬编码演示数据,
|
|
||||||
// 无真实数据源——enabled: false 隐藏铃铛;组件保留,接真数据源时再开。
|
|
||||||
enabled: false,
|
|
||||||
description: '通知中心,显示系统通知和消息'
|
|
||||||
},
|
|
||||||
chat: {
|
chat: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
description: '聊天功能,提供实时沟通'
|
description: '聊天功能,提供实时沟通'
|
||||||
|
|||||||
Vendored
-2
@@ -2,7 +2,5 @@
|
|||||||
|
|
||||||
declare module 'nprogress'
|
declare module 'nprogress'
|
||||||
|
|
||||||
declare module 'crypto-js'
|
|
||||||
|
|
||||||
// 全局变量声明
|
// 全局变量声明
|
||||||
declare const __APP_VERSION__: string // 版本号
|
declare const __APP_VERSION__: string // 版本号
|
||||||
|
|||||||
@@ -83,11 +83,6 @@ export function useHeaderBar() {
|
|||||||
return isFeatureEnabled('fullscreen')
|
return isFeatureEnabled('fullscreen')
|
||||||
})
|
})
|
||||||
|
|
||||||
// 检查通知中心是否显示
|
|
||||||
const shouldShowNotification = computed(() => {
|
|
||||||
return isFeatureEnabled('notification')
|
|
||||||
})
|
|
||||||
|
|
||||||
// 检查聊天功能是否显示
|
// 检查聊天功能是否显示
|
||||||
const shouldShowChat = computed(() => {
|
const shouldShowChat = computed(() => {
|
||||||
return isFeatureEnabled('chat')
|
return isFeatureEnabled('chat')
|
||||||
@@ -179,7 +174,6 @@ export function useHeaderBar() {
|
|||||||
shouldShowBreadcrumb, // 是否显示面包屑
|
shouldShowBreadcrumb, // 是否显示面包屑
|
||||||
shouldShowGlobalSearch, // 是否显示全局搜索
|
shouldShowGlobalSearch, // 是否显示全局搜索
|
||||||
shouldShowFullscreen, // 是否显示全屏按钮
|
shouldShowFullscreen, // 是否显示全屏按钮
|
||||||
shouldShowNotification, // 是否显示通知中心
|
|
||||||
shouldShowChat, // 是否显示聊天功能
|
shouldShowChat, // 是否显示聊天功能
|
||||||
shouldShowLanguage, // 是否显示语言切换
|
shouldShowLanguage, // 是否显示语言切换
|
||||||
shouldShowSettings, // 是否显示设置面板
|
shouldShowSettings, // 是否显示设置面板
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"changePassword": "Change Password",
|
"changePassword": "Change Password",
|
||||||
"lockScreen": "Lock Screen",
|
|
||||||
"logout": "Log Out"
|
"logout": "Log Out"
|
||||||
},
|
},
|
||||||
"guide": {
|
"guide": {
|
||||||
@@ -122,19 +121,6 @@
|
|||||||
"resetFailed": "Reset failed, please refresh the page and try again"
|
"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": {
|
"worktab": {
|
||||||
"btn": {
|
"btn": {
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
@@ -167,18 +153,6 @@
|
|||||||
"message": "Welcome back"
|
"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": {
|
"greeting": {
|
||||||
"dawn": "Good morning!",
|
"dawn": "Good morning!",
|
||||||
"morning": "Good morning!",
|
"morning": "Good morning!",
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"changePassword": "修改密码",
|
"changePassword": "修改密码",
|
||||||
"lockScreen": "锁定屏幕",
|
|
||||||
"logout": "退出登录"
|
"logout": "退出登录"
|
||||||
},
|
},
|
||||||
"guide": {
|
"guide": {
|
||||||
@@ -122,19 +121,6 @@
|
|||||||
"resetFailed": "重置失败,请刷新页面后重试"
|
"resetFailed": "重置失败,请刷新页面后重试"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notice": {
|
|
||||||
"title": "通知",
|
|
||||||
"btnRead": "标为已读",
|
|
||||||
"bar": [
|
|
||||||
"通知",
|
|
||||||
"消息",
|
|
||||||
"代办"
|
|
||||||
],
|
|
||||||
"text": [
|
|
||||||
"暂无"
|
|
||||||
],
|
|
||||||
"viewAll": "查看全部"
|
|
||||||
},
|
|
||||||
"worktab": {
|
"worktab": {
|
||||||
"btn": {
|
"btn": {
|
||||||
"refresh": "刷新",
|
"refresh": "刷新",
|
||||||
@@ -167,18 +153,6 @@
|
|||||||
"message": "欢迎回来"
|
"message": "欢迎回来"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lockScreen": {
|
|
||||||
"pwdError": "密码错误",
|
|
||||||
"lock": {
|
|
||||||
"inputPlaceholder": "请输入锁屏密码",
|
|
||||||
"btnText": "锁定"
|
|
||||||
},
|
|
||||||
"unlock": {
|
|
||||||
"inputPlaceholder": "请输入解锁密码",
|
|
||||||
"btnText": "解锁",
|
|
||||||
"backBtnText": "返回登录"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"greeting": {
|
"greeting": {
|
||||||
"dawn": "凌晨了!",
|
"dawn": "凌晨了!",
|
||||||
"morning": "上午好!",
|
"morning": "上午好!",
|
||||||
|
|||||||
@@ -198,8 +198,6 @@ export interface HeaderBarFeatureConfig {
|
|||||||
globalSearch: FeatureConfigItem
|
globalSearch: FeatureConfigItem
|
||||||
/** 全屏功能 */
|
/** 全屏功能 */
|
||||||
fullscreen: FeatureConfigItem
|
fullscreen: FeatureConfigItem
|
||||||
/** 通知功能 */
|
|
||||||
notification: FeatureConfigItem
|
|
||||||
/** 聊天功能 */
|
/** 聊天功能 */
|
||||||
chat: FeatureConfigItem
|
chat: FeatureConfigItem
|
||||||
/** 多语言切换 */
|
/** 多语言切换 */
|
||||||
|
|||||||
-2
@@ -40,14 +40,12 @@ declare module 'vue' {
|
|||||||
ArtLogo: typeof import('./../../components/core/base/art-logo/index.vue')['default']
|
ArtLogo: typeof import('./../../components/core/base/art-logo/index.vue')['default']
|
||||||
ArtMenuRight: typeof import('./../../components/core/others/art-menu-right/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']
|
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']
|
ArtPageContent: typeof import('./../../components/core/layouts/art-page-content/index.vue')['default']
|
||||||
ArtProgressCard: typeof import('./../../components/core/cards/art-progress-card/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']
|
ArtRadarChart: typeof import('./../../components/core/charts/art-radar-chart/index.vue')['default']
|
||||||
ArtResultPage: typeof import('./../../components/core/views/result/ArtResultPage.vue')['default']
|
ArtResultPage: typeof import('./../../components/core/views/result/ArtResultPage.vue')['default']
|
||||||
ArtRingChart: typeof import('./../../components/core/charts/art-ring-chart/index.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']
|
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']
|
ArtSearchBar: typeof import('./../../components/core/forms/art-search-bar/index.vue')['default']
|
||||||
ArtSettingsPanel: typeof import('./../../components/core/layouts/art-settings-panel/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']
|
ArtSidebarMenu: typeof import('./../../components/core/layouts/art-menus/art-sidebar-menu/index.vue')['default']
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* ## 使用场景
|
* ## 使用场景
|
||||||
*
|
*
|
||||||
* - 跨层级组件通信
|
* - 跨层级组件通信
|
||||||
* - 全局功能触发(设置、搜索、聊天、锁屏等)
|
* - 全局功能触发(设置、搜索、聊天等)
|
||||||
* - 特效触发(烟花效果)
|
* - 特效触发(烟花效果)
|
||||||
* - 避免 props 层层传递
|
* - 避免 props 层层传递
|
||||||
*
|
*
|
||||||
@@ -36,7 +36,6 @@
|
|||||||
* - openSetting: 打开设置面板
|
* - openSetting: 打开设置面板
|
||||||
* - openSearchDialog: 打开搜索对话框
|
* - openSearchDialog: 打开搜索对话框
|
||||||
* - openChat: 打开聊天窗口
|
* - openChat: 打开聊天窗口
|
||||||
* - openLockScreen: 打开锁屏
|
|
||||||
*
|
*
|
||||||
* @module utils/sys/mittBus
|
* @module utils/sys/mittBus
|
||||||
* @author Art Design Pro Team
|
* @author Art Design Pro Team
|
||||||
@@ -53,8 +52,6 @@ type Events = {
|
|||||||
openSearchDialog: void
|
openSearchDialog: void
|
||||||
// 打开聊天窗口事件 - 无参数
|
// 打开聊天窗口事件 - 无参数
|
||||||
openChat: void
|
openChat: void
|
||||||
// 打开锁屏事件 - 无参数
|
|
||||||
openLockScreen: void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建类型安全的事件总线实例
|
// 创建类型安全的事件总线实例
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ export default ({ mode }: { mode: string }) => {
|
|||||||
'echarts/charts',
|
'echarts/charts',
|
||||||
'echarts/components',
|
'echarts/components',
|
||||||
'echarts/renderers',
|
'echarts/renderers',
|
||||||
'crypto-js',
|
|
||||||
'element-plus/es',
|
'element-plus/es',
|
||||||
'element-plus/es/components/*/style/css',
|
'element-plus/es/components/*/style/css',
|
||||||
'element-plus/es/components/*/style/index'
|
'element-plus/es/components/*/style/index'
|
||||||
|
|||||||
Reference in New Issue
Block a user