55 <img src =" @/assets/paw.png" alt =" " class =" titlebar-paw" draggable =" false" />
66 <span class =" titlebar-text" >{{ t('app.titleSuffix') }}</span >
77 </div >
8- <button
9- class =" titlebar-close"
10- type =" button"
11- :title =" t('common.close')"
12- :aria-label =" t('common.close')"
13- @click =" closeWindow"
14- >
15- <svg width =" 10" height =" 10" viewBox =" 0 0 10 10" fill =" none" >
16- <path d =" M1 1L9 9M9 1L1 9" stroke =" currentColor" stroke-width =" 1.5" stroke-linecap =" round" />
17- </svg >
18- </button >
8+ <div class =" titlebar-controls" >
9+ <button
10+ class =" titlebar-control"
11+ type =" button"
12+ :title =" t('common.minimize')"
13+ :aria-label =" t('common.minimize')"
14+ @click =" minimizeWindow"
15+ >
16+ <span class =" titlebar-minimize-icon" aria-hidden =" true" ></span >
17+ </button >
18+ <button
19+ class =" titlebar-control"
20+ type =" button"
21+ :title =" maximizeLabel"
22+ :aria-label =" maximizeLabel"
23+ @click =" toggleMaximize"
24+ >
25+ <span class =" titlebar-maximize-icon" :class =" { 'is-restored': isMaximized }" aria-hidden =" true" ></span >
26+ </button >
27+ <button
28+ class =" titlebar-control titlebar-control--close"
29+ type =" button"
30+ :title =" t('common.close')"
31+ :aria-label =" t('common.close')"
32+ @click =" closeWindow"
33+ >
34+ <svg class =" titlebar-close-icon" viewBox =" 0 0 10 10" fill =" none" aria-hidden =" true" focusable =" false" >
35+ <path d =" M1 1L9 9M9 1L1 9" stroke =" currentColor" stroke-width =" 1.5" stroke-linecap =" round" />
36+ </svg >
37+ </button >
38+ </div >
1939 </div >
2040
2141 <div class =" app-shell" >
2646 <div class =" app-body" >
2747 <div v-if =" connectionStore.disconnected" class =" connection-banner" >
2848 <div class =" connection-banner__inner" >
29- ⚠️ {{ t('common.disconnected') }}
49+ {{ t('common.disconnected') }}
3050 </div >
3151 </div >
3252
4767</template >
4868
4969<script setup lang="ts">
70+ import { computed , onBeforeUnmount , onMounted , ref } from ' vue'
5071import Sidebar from ' ./Sidebar.vue'
5172import Header from ' ./Header.vue'
5273import { useI18n } from ' vue-i18n'
5374import { useConnectionStore } from ' @/stores/connection'
5475
5576const { t } = useI18n ()
5677const connectionStore = useConnectionStore ()
78+ const isMaximized = ref (false )
79+ const maximizeLabel = computed (() => isMaximized .value ? t (' common.restore' ) : t (' common.maximize' ))
80+
81+ function getWindowControlApi() {
82+ return window .nekoWindowControl
83+ }
84+
85+ async function refreshMaximizeState() {
86+ const api = getWindowControlApi ()
87+ if (! api || typeof api .isMaximized !== ' function' ) return
88+ try {
89+ isMaximized .value = !! (await api .isMaximized ())
90+ } catch {
91+ // 非桌面窗口环境下忽略状态查询失败
92+ }
93+ }
94+
95+ async function minimizeWindow() {
96+ const api = getWindowControlApi ()
97+ if (! api || typeof api .minimize !== ' function' ) return
98+ try {
99+ await api .minimize ()
100+ } catch {
101+ // 非桌面窗口环境下忽略最小化失败
102+ }
103+ }
104+
105+ async function toggleMaximize() {
106+ const api = getWindowControlApi ()
107+ if (! api || typeof api .maximize !== ' function' ) return
108+ try {
109+ const result = await api .maximize ()
110+ if (result && result .ok && typeof result .isMaximized === ' boolean' ) {
111+ isMaximized .value = result .isMaximized
112+ return
113+ }
114+ await refreshMaximizeState ()
115+ } catch {
116+ // 非桌面窗口环境下忽略最大化失败
117+ }
118+ }
119+
120+ function handleWindowResize() {
121+ void refreshMaximizeState ()
122+ }
57123
58124function closeWindow() {
59125 window .close ()
60126}
127+
128+ onMounted (() => {
129+ void refreshMaximizeState ()
130+ window .addEventListener (' resize' , handleWindowResize )
131+ })
132+
133+ onBeforeUnmount (() => {
134+ window .removeEventListener (' resize' , handleWindowResize )
135+ })
61136 </script >
62137
63138<style scoped>
@@ -68,7 +143,7 @@ function closeWindow() {
68143 overflow : hidden ;
69144}
70145
71- /* ── Title bar (acrylic) ── */
146+ /* 标题栏玻璃效果 */
72147.window-titlebar {
73148 padding : 0 6px 0 12px ;
74149 height : 38px ;
@@ -79,7 +154,6 @@ function closeWindow() {
79154 -webkit-app-region : drag;
80155 user-select : none ;
81156 z-index : 9999 ;
82- /* Acrylic effect — matches react-neko-chat topbar */
83157 background :
84158 linear-gradient (135deg ,
85159 rgba (75 , 212 , 253 , 0.82 ) 0% ,
@@ -117,7 +191,14 @@ function closeWindow() {
117191 text-shadow : 0 1px 2px rgba (0 , 0 , 0 , 0.1 );
118192}
119193
120- .titlebar-close {
194+ .titlebar-controls {
195+ display : flex ;
196+ align-items : center ;
197+ gap : 4px ;
198+ -webkit-app-region : no-drag;
199+ }
200+
201+ .titlebar-control {
121202 -webkit-app-region : no-drag;
122203 background : transparent ;
123204 border : none ;
@@ -132,16 +213,56 @@ function closeWindow() {
132213 transition : background 0.18s , color 0.18s ;
133214}
134215
135- .titlebar-close :hover {
216+ .titlebar-control :hover {
136217 background : rgba (255 , 255 , 255 , 0.18 );
137218 color : #fff ;
138219}
139220
140- .titlebar-close :active {
221+ .titlebar-control :active {
141222 background : rgba (0 , 0 , 0 , 0.08 );
142223}
143224
144- /* ── Shell layout ── */
225+ .titlebar-control--close :hover {
226+ background : rgba (255 , 255 , 255 , 0.22 );
227+ }
228+
229+ .titlebar-minimize-icon {
230+ width : 12px ;
231+ height : 1.5px ;
232+ border-radius : 999px ;
233+ background : currentColor ;
234+ transform : translateY (4px );
235+ }
236+
237+ .titlebar-maximize-icon {
238+ position : relative ;
239+ width : 11px ;
240+ height : 11px ;
241+ border : 1.5px solid currentColor ;
242+ border-radius : 2px ;
243+ }
244+
245+ .titlebar-maximize-icon.is-restored {
246+ transform : translate (1.5px , 1.5px );
247+ }
248+
249+ .titlebar-maximize-icon.is-restored ::before {
250+ content : ' ' ;
251+ position : absolute ;
252+ left : -4px ;
253+ top : -4px ;
254+ width : 11px ;
255+ height : 11px ;
256+ border : 1.5px solid currentColor ;
257+ border-radius : 2px ;
258+ }
259+
260+ .titlebar-close-icon {
261+ width : 10px ;
262+ height : 10px ;
263+ }
264+
265+ /* 主体布局 */
145266.app-shell {
146267 flex : 1 ;
147268 display : flex ;
@@ -188,7 +309,7 @@ function closeWindow() {
188309 background : var (--el-bg-color-page );
189310}
190311
191- /* ── Connection banner ── */
312+ /* 连接状态提示 */
192313.connection-banner {
193314 padding : 8px 20px 0 ;
194315}
@@ -203,7 +324,7 @@ function closeWindow() {
203324 font-weight : 500 ;
204325}
205326
206- /* ── Page transition ── */
327+ /* 页面切换动画 */
207328.page-enter-active {
208329 transition :
209330 opacity 0.3s cubic-bezier (0.22 , 1 , 0.36 , 1 ),
@@ -230,7 +351,7 @@ function closeWindow() {
230351 filter : blur (2px );
231352}
232353
233- /* ── Dark mode acrylic overrides ── */
354+ /* 深色模式覆盖 */
234355html .dark .window-titlebar {
235356 background :
236357 linear-gradient (135deg ,
0 commit comments