feat(server): 新增修改密码接口 POST /api/auth/password(验原密码、作废会话)
This commit is contained in:
1 parent
ad2322d558
commit
4cbf5a74f0
13 files changed
+528
-20
No files matched your search
+37
-1
@@ -1,7 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
import DesktopTitleBar from './components/DesktopTitleBar.vue'
|
||||
|
||||
// 浏览器里没有 desktopShell,包装层必须完全透明,否则会改变原有 Web 布局
|
||||
const isDesktopShell = typeof window !== 'undefined' && Boolean(window.desktopShell)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
<div class="admin-root" :class="{ 'is-desktop-shell': isDesktopShell }">
|
||||
<DesktopTitleBar />
|
||||
<div class="admin-root-body">
|
||||
<RouterView />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-root,
|
||||
.admin-root-body {
|
||||
display: contents;
|
||||
}
|
||||
.is-desktop-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
}
|
||||
.is-desktop-shell .admin-root-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
/* 页面自己写死 100dvh,桌面壳里要让它填满标题栏下方的剩余高度 */
|
||||
.is-desktop-shell .admin-root-body :deep(.admin-shell) {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.is-desktop-shell .admin-root-body :deep(.login-page) {
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,149 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import MxIcon from './MxIcon.vue'
|
||||
|
||||
// preload 先于页面脚本执行,这里读到的值在组件生命周期内不会变;
|
||||
// 浏览器里访问 /admin 时没有 desktopShell,整个组件不渲染
|
||||
const shell = window.desktopShell
|
||||
const visible = Boolean(shell)
|
||||
const maximized = ref(false)
|
||||
let unsubscribe: (() => void) | undefined
|
||||
|
||||
onMounted(() => {
|
||||
if (!shell) return
|
||||
void shell.isMaximized().then((value) => {
|
||||
maximized.value = value
|
||||
})
|
||||
unsubscribe = shell.onMaximizeChange((value) => {
|
||||
maximized.value = value
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unsubscribe?.()
|
||||
})
|
||||
|
||||
function toggleMaximize(): void {
|
||||
shell?.maximize()
|
||||
}
|
||||
|
||||
function handleDoubleClick(event: MouseEvent): void {
|
||||
// 双击落在按钮上不切最大化,否则连点按钮会顺带把窗口切走
|
||||
if (event.target instanceof Element && event.target.closest('button')) return
|
||||
toggleMaximize()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header v-if="visible" class="desktop-titlebar" @dblclick="handleDoubleClick">
|
||||
<div class="desktop-titlebar-brand">
|
||||
<span class="desktop-titlebar-mark" aria-hidden="true"></span>
|
||||
<span class="desktop-titlebar-title">积分榜管理</span>
|
||||
<span class="desktop-titlebar-sub">服务端管理面板</span>
|
||||
</div>
|
||||
|
||||
<div class="desktop-titlebar-controls">
|
||||
<button class="desktop-win-btn" type="button" title="最小化" aria-label="最小化" @click="shell?.minimize()">
|
||||
<MxIcon name="minimize" :size="16" />
|
||||
</button>
|
||||
<button
|
||||
class="desktop-win-btn"
|
||||
type="button"
|
||||
:title="maximized ? '还原' : '最大化'"
|
||||
:aria-label="maximized ? '还原' : '最大化'"
|
||||
@click="toggleMaximize"
|
||||
>
|
||||
<MxIcon :name="maximized ? 'restore' : 'maximize'" :size="16" />
|
||||
</button>
|
||||
<button
|
||||
class="desktop-win-btn desktop-win-btn-close"
|
||||
type="button"
|
||||
title="关闭"
|
||||
aria-label="关闭"
|
||||
@click="shell?.close()"
|
||||
>
|
||||
<MxIcon name="close" :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.desktop-titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
height: 50px;
|
||||
flex: 0 0 auto;
|
||||
padding: 0 8px 0 16px;
|
||||
border-bottom: 1px solid var(--mx-divider);
|
||||
background: var(--mx-s1);
|
||||
color: var(--mx-text);
|
||||
user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
.desktop-titlebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
.desktop-titlebar-mark {
|
||||
width: 8px;
|
||||
height: 22px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(180deg, var(--mx-primary), var(--mx-primary-active));
|
||||
}
|
||||
.desktop-titlebar-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.desktop-titlebar-sub {
|
||||
font-size: 11px;
|
||||
color: var(--mx-t3);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.desktop-titlebar-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
.desktop-win-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--mx-t3);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--mx-d1) var(--mx-ease),
|
||||
color var(--mx-d1) var(--mx-ease),
|
||||
border-color var(--mx-d1) var(--mx-ease);
|
||||
}
|
||||
.desktop-win-btn:hover {
|
||||
background: var(--mx-s-hover);
|
||||
color: var(--mx-text);
|
||||
}
|
||||
.desktop-win-btn:active {
|
||||
background: var(--mx-s-active);
|
||||
}
|
||||
.desktop-win-btn:focus-visible {
|
||||
outline: 2px solid var(--mx-primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
/* 关闭键是唯一的破坏性操作,hover 变红做区分 */
|
||||
.desktop-win-btn-close:hover {
|
||||
border-color: var(--mx-error);
|
||||
background: var(--mx-error);
|
||||
color: var(--mx-error-on);
|
||||
}
|
||||
</style>
|
||||
@@ -20,6 +20,9 @@ export const ICON_PATHS = {
|
||||
bell: 'M6 9a6 6 0 1 1 12 0c0 5 2 6 2 6H4s2-1 2-6ZM10 19a2 2 0 0 0 4 0',
|
||||
push: 'M12 20V6M8 10l4-4 4 4M4 20h16',
|
||||
close: 'M6 6l12 12M18 6 6 18',
|
||||
minimize: 'M5 12h14',
|
||||
maximize: 'M5 5h14v14H5z',
|
||||
restore: 'M8 8h11v11H8zM5 16V5h11',
|
||||
check: 'M5 12.5 10 17l9-10',
|
||||
warning: 'M12 4l9 16H3l9-16Zm0 6v4m0 3v.5',
|
||||
search: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm10 2-4.3-4.3',
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Console 的无边框窗口会在 preload 里注入 window.desktopShell。
|
||||
* 浏览器直接访问 /admin 时它不存在,DesktopTitleBar 据此自动不渲染。
|
||||
*/
|
||||
export interface DesktopShellApi {
|
||||
platform: string
|
||||
minimize: () => void
|
||||
maximize: () => void
|
||||
close: () => void
|
||||
isMaximized: () => Promise<boolean>
|
||||
onMaximizeChange: (callback: (maximized: boolean) => void) => () => void
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
desktopShell?: DesktopShellApi
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user