mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-15 00:06:24 +00:00
feat(frontend): support a draggable title bar in the desktop PWA
This commit is contained in:
@@ -389,6 +389,8 @@ PostgreSQL/Redis 启动密码。日常校验使用 `config --quiet`
|
||||
|
||||
使用 Chrome、Edge 等支持 PWA 的浏览器访问管理端,可通过地址栏或浏览器菜单安装应用,安装后以独立窗口打开站点首页,沿用现有登录流程
|
||||
|
||||
在支持自定义标题栏的桌面浏览器中,可用窗口右上角的按钮显示或隐藏系统标题栏。隐藏后仍可拖动顶部空白区域移动窗口
|
||||
|
||||
安装入口要求 HTTPS,或本机的 `http://localhost`、`http://127.0.0.1`;普通 HTTP 局域网地址不满足条件。入口是否显示还取决于浏览器、访问交互及是否已安装。应用需要联网访问网关,不提供离线使用
|
||||
|
||||
### 启动后的运行设置
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
/>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(display-mode: standalone), (display-mode: window-controls-overlay)"
|
||||
content="#ffffff"
|
||||
/>
|
||||
<title>Codex Proxy RS</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"display_override": ["window-controls-overlay"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.svg",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ZConfigProvider } from '@codex-proxy/ui'
|
||||
import { useDebounceFn, useDevicePixelRatio, useEventListener, useWindowSize } from '@vueuse/core'
|
||||
import { watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterView } from 'vue-router'
|
||||
@@ -7,11 +8,47 @@ import { applyLocale } from '@/i18n/locale'
|
||||
import { uiLocale } from '@/i18n/ui'
|
||||
|
||||
const { locale } = useI18n({ useScope: 'global' })
|
||||
const overlay = (navigator as Navigator & { windowControlsOverlay?: { getTitlebarAreaRect: () => DOMRect } }).windowControlsOverlay
|
||||
if (overlay && matchMedia('(display-mode: standalone), (display-mode: window-controls-overlay)').matches) {
|
||||
const root = document.documentElement
|
||||
root.classList.add('pwa-window')
|
||||
const { height } = useWindowSize({ type: 'outer' })
|
||||
const { pixelRatio } = useDevicePixelRatio()
|
||||
const rememberHeight = () => {
|
||||
const viewport = window.visualViewport?.height ?? window.innerHeight
|
||||
const next = viewport - overlay.getTitlebarAreaRect().bottom
|
||||
const previous = Number.parseFloat(root.style.getPropertyValue('--app-height'))
|
||||
if (!Number.isFinite(previous) || Math.abs(next - previous) >= 1)
|
||||
root.style.setProperty('--app-height', `${next}px`)
|
||||
}
|
||||
// 调整外窗时立即交回 CSS;macOS 还原动画中,外窗和内容视口可能在不同帧才停止变化
|
||||
watch([height, pixelRatio], () => root.style.removeProperty('--app-height'))
|
||||
const settled = useDebounceFn(rememberHeight, 80)
|
||||
useEventListener(window.visualViewport ?? window, 'resize', settled)
|
||||
rememberHeight()
|
||||
}
|
||||
|
||||
watch(locale, applyLocale, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ZConfigProvider :locale="uiLocale">
|
||||
<div class="pwa-titlebar hidden bg-z-bg-container" aria-hidden="true" />
|
||||
<RouterView />
|
||||
</ZConfigProvider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@media (display-mode: window-controls-overlay) {
|
||||
.pwa-titlebar {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 51;
|
||||
top: env(titlebar-area-y);
|
||||
left: env(titlebar-area-x);
|
||||
width: env(titlebar-area-width);
|
||||
height: env(titlebar-area-height);
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -78,7 +78,7 @@ watch(
|
||||
<main class="relative isolate h-dvh min-w-0 flex-1 overflow-hidden">
|
||||
<!-- 让主滚动区自身绘制不透明底色,浏览器可在合成线程滚动 -->
|
||||
<ZScrollbar ref="pageScrollbarRef" class="[&>.z-scrollbar-hidden]:bg-z-bg-layout">
|
||||
<div class="flex min-h-full min-w-0 flex-col p-4 min-[961px]:p-6">
|
||||
<div class="page-slot flex min-h-full min-w-0 flex-col p-4 min-[961px]:p-6">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<component :is="Component" class="min-h-0 flex-1" />
|
||||
</RouterView>
|
||||
@@ -88,7 +88,7 @@ watch(
|
||||
|
||||
<Teleport to="body">
|
||||
<Transition name="mobile-sidebar" @after-leave="restoreFocus">
|
||||
<div v-if="mobileSidebarOpen" :inert="!mobileSidebarOpen" role="presentation" class="fixed inset-0 z-50 min-[961px]:hidden" @keydown="handleKeydown">
|
||||
<div v-if="mobileSidebarOpen" :inert="!mobileSidebarOpen" role="presentation" class="fixed inset-x-0 top-(--app-titlebar-inset,0px) bottom-0 z-50 min-[961px]:hidden" @keydown="handleKeydown">
|
||||
<button
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
|
||||
@@ -130,6 +130,7 @@ export const useThemeStore = defineStore(
|
||||
|
||||
function commitTheme(theme: Theme): void {
|
||||
applyTheme(document.documentElement, theme)
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', theme.tokens['color-bg-container'])
|
||||
appliedThemeSignature = themeSignature(theme)
|
||||
themeRevision.value += 1
|
||||
}
|
||||
|
||||
@@ -1,3 +1,40 @@
|
||||
@import 'tailwindcss/theme.css' layer(theme);
|
||||
@import '@codex-proxy/ui/tailwind.css';
|
||||
@import 'tailwindcss/utilities.css' layer(utilities);
|
||||
|
||||
.pwa-window {
|
||||
--app-height: calc(100dvh - env(titlebar-area-y, 0px) - env(titlebar-area-height, 0px));
|
||||
--app-titlebar-inset: max(0px, 100dvh - var(--app-height));
|
||||
}
|
||||
|
||||
.pwa-window,
|
||||
.pwa-window body {
|
||||
background-color: var(--z-color-bg-container);
|
||||
}
|
||||
|
||||
.pwa-window body {
|
||||
padding-top: var(--app-titlebar-inset);
|
||||
}
|
||||
|
||||
.pwa-window .h-dvh,
|
||||
.pwa-window .page-slot {
|
||||
height: var(--app-height);
|
||||
}
|
||||
|
||||
.pwa-window .min-h-dvh {
|
||||
min-height: var(--app-height);
|
||||
}
|
||||
|
||||
/* 满屏面板不能高于正文;矮窗仍由外层滚动到列表底部 */
|
||||
.pwa-window .page-slot > * {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.pwa-window .page-slot > .h-full {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* 通知挂在 body,固定定位不会避开标题栏 */
|
||||
.pwa-window article[id^='z-notification-'] {
|
||||
translate: 0 var(--app-titlebar-inset);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user