11<template >
2- <header class =" site-top-bar" role =" banner" >
3- <div class =" site-top-bar-inner" >
4- <router-link class =" site-brand" to =" /" aria-label =" 烧仙草冰室,返回首页" >
5- <span class =" site-brand-logo" aria-hidden =" true" >
6- <svg viewBox =" 0 0 24 24" fill =" none" stroke =" currentColor" stroke-width =" 1.8" stroke-linecap =" round" stroke-linejoin =" round" >
7- <!-- cup -->
8- <path d =" M7 6h10l-1.2 14.2a2 2 0 0 1-2 1.8H10.2a2 2 0 0 1-2-1.8L7 6z" />
9- <!-- rim -->
10- <path d =" M6.2 6h11.6" />
11- <!-- ice cubes -->
12- <path d =" M10 9.2h2.6v2.6H10z" />
13- <path d =" M13.6 10.6h2.6v2.6h-2.6z" />
14- <!-- grass jelly layer -->
15- <path d =" M9.2 15.2c1.1-.8 2.4-.8 3.5 0s2.4.8 3.5 0" />
16- </svg >
17- </span >
18- <span class =" site-brand-text" >烧仙草冰室</span >
19- </router-link >
20-
21- <nav class =" site-nav" aria-label =" 主导航" >
22- <button
23- type =" button"
24- class =" site-nav-link site-nav-link-btn site-nav-widgets-btn"
25- @click =" emit('toggle-widgets')"
26- aria-label =" 打开个人信息侧栏"
27- title =" 个人信息"
28- >
29- <span class =" site-nav-ico" aria-hidden =" true" ><Menu /></span >
30- <span class =" site-nav-widgets-text" >个人</span >
31- </button >
32- <router-link class =" site-nav-link" to =" /" :class =" { active: isActive('/') }" >
33- <span class =" site-nav-ico" aria-hidden =" true" ><House /></span >
34- 首页
35- </router-link >
36- <router-link class =" site-nav-link" to =" /all" :class =" { active: isActive('/all') }" >
37- <span class =" site-nav-ico" aria-hidden =" true" ><Notebook /></span >
38- 博客
39- </router-link >
40- <router-link class =" site-nav-link" to =" /about" :class =" { active: isActive('/about') }" >
41- <span class =" site-nav-ico" aria-hidden =" true" ><InfoFilled /></span >
42- 关于
43- </router-link >
44- <div ref =" dropdownRoot" class =" site-nav-dropdown" >
45- <button
46- type =" button"
47- class =" site-nav-link site-nav-link-btn site-nav-dropdown-trigger"
48- :aria-expanded =" dropdownOpen"
49- aria-haspopup =" menu"
50- @click.stop =" toggleDropdown"
51- >
52- <span class =" site-nav-ico" aria-hidden =" true" ><MagicStick /></span >
53- AI实验室 <span class =" site-nav-dropdown-caret" aria-hidden =" true" >▾</span >
54- </button >
55-
56- <div v-if =" dropdownOpen" class =" site-nav-dropdown-menu" role =" menu" >
57- <a
58- class =" site-nav-dropdown-item"
59- href =" http://ai.wecode.xin/"
60- target =" _blank"
61- rel =" noopener noreferrer"
62- role =" menuitem"
63- @click =" closeDropdown"
64- >
65- AI工作平台
66- </a >
67- </div >
68- </div >
2+ <div >
3+ <header class =" site-top-bar" role =" banner" >
4+ <div class =" site-top-bar-inner" >
5+ <div class =" site-top-actions" >
696 <a
70- class =" site-nav-link "
7+ class =" top-action-btn "
718 href =" https://github.com/yyyCode/OpenBlog.git"
729 target =" _blank"
7310 rel =" noopener noreferrer"
11+ aria-label =" 项目源码(GitHub)"
7412 >
75- <span class =" site-nav-ico" aria-hidden =" true" ><Link /></span >
76- 源码
13+ <span class =" top-action-ico" aria-hidden =" true" >
14+ <svg width =" 16" height =" 16" viewBox =" 0 0 24 24" fill =" currentColor" aria-hidden =" true" >
15+ <path
16+ d =" M12 .5C5.73.5.75 5.67.75 12.09c0 5.15 3.28 9.52 7.83 11.06.57.11.78-.25.78-.55 0-.27-.01-1.17-.02-2.12-3.18.71-3.85-1.39-3.85-1.39-.52-1.36-1.28-1.72-1.28-1.72-1.05-.74.08-.73.08-.73 1.16.08 1.77 1.23 1.77 1.23 1.03 1.8 2.7 1.28 3.36.98.1-.77.4-1.28.73-1.57-2.54-.3-5.21-1.3-5.21-5.79 0-1.28.44-2.33 1.16-3.15-.12-.29-.5-1.48.11-3.08 0 0 .95-.31 3.11 1.2.9-.26 1.86-.39 2.82-.39.96 0 1.92.13 2.82.39 2.16-1.51 3.11-1.2 3.11-1.2.61 1.6.23 2.79.11 3.08.72.82 1.16 1.87 1.16 3.15 0 4.5-2.68 5.49-5.23 5.79.41.36.78 1.07.78 2.17 0 1.57-.02 2.83-.02 3.22 0 .3.21.67.79.55 4.55-1.54 7.83-5.91 7.83-11.06C23.25 5.67 18.27.5 12 .5z"
17+ />
18+ </svg >
19+ </span >
20+ 项目源码
7721 </a >
78- <button type =" button" class =" site-nav-link site-nav-link-btn" @click =" onToggle" >
79- <span class =" site-nav-ico" aria-hidden =" true" >
80- <Sunny v-if =" isDark " />
81- <Moon v-else />
22+ <button type =" button" class =" top-action-btn" aria-label =" 问题反馈" @click =" openFeedback" >
23+ <span class =" top-action-ico" aria-hidden =" true" >
24+ <svg width =" 16" height =" 16" viewBox =" 0 0 24 24" fill =" none" stroke =" currentColor" stroke-width =" 2" >
25+ <path d =" M4 4h16v16H4z" />
26+ <path d =" M22 6l-10 7L2 6" />
27+ </svg >
8228 </span >
83- {{ isDark ? '浅色' : '深色' }}
29+ 问题反馈
8430 </button >
85- </nav >
86- </div >
87- </header >
31+ </div >
32+ <button
33+ type =" button"
34+ class =" theme-toggle"
35+ :aria-label =" isDark ? '切换到浅色模式' : '切换到深色模式'"
36+ @click =" onToggle"
37+ >
38+ <span class =" theme-toggle-icon" aria-hidden =" true" >{{ isDark ? '☀' : '☽' }}</span >
39+ <span class =" theme-toggle-text" >{{ isDark ? '浅色' : '深色' }}</span >
40+ </button >
41+ </div >
42+ </header >
43+
44+ <Teleport to="body">
45+ <div v-if =" feedbackOpen" class =" ob-dialog-overlay" role =" dialog" aria-modal =" true" aria-label =" 问题反馈" >
46+ <div class =" ob-dialog card" >
47+ <div class =" ob-dialog-title" >问题反馈</div >
48+ <div class =" ob-dialog-desc" >欢迎提交问题与建议(同一 IP 每天仅可提交一次)。</div >
49+
50+ <div class =" field" style =" margin-top : 14px " >
51+ <div class =" label" >提交者名字</div >
52+ <input v-model =" fbName" class =" input" type =" text" maxlength =" 50" placeholder =" 例如:张三" />
53+ </div >
54+ <div class =" field" >
55+ <div class =" label" >内容</div >
56+ <textarea
57+ v-model =" fbContent"
58+ class =" textarea"
59+ rows =" 6"
60+ maxlength =" 2000"
61+ placeholder =" 请描述你遇到的问题或建议"
62+ ></textarea >
63+ </div >
64+
65+ <div style =" display : flex ; gap : 10px ; justify-content : flex-end ; margin-top : 14px " >
66+ <button type =" button" class =" btn" :disabled =" fbLoading" @click =" closeFeedback" >取消</button >
67+ <button type =" button" class =" btn primary" :disabled =" fbLoading || !canSubmit" @click =" submitFeedback" >
68+ {{ fbLoading ? '提交中…' : '提交' }}
69+ </button >
70+ </div >
71+ </div >
72+ </div >
73+ </Teleport >
74+ </div >
8875</template >
8976
9077<script setup>
91- import { onBeforeUnmount , onMounted , ref } from ' vue'
92- import { useRoute } from ' vue-router'
78+ import { computed , onMounted , ref } from ' vue'
9379import { toggleTheme as applyToggle } from ' ../theme'
94- import { House , InfoFilled , Link , MagicStick , Menu , Moon , Notebook , Sunny } from ' @element-plus/icons-vue'
80+ import { showMessage } from ' ../utils/message'
81+ import { createFeedback } from ' ../api/feedback'
9582
9683const isDark = ref (false )
97- const route = useRoute ()
98- const emit = defineEmits ([' toggle-widgets' ])
99-
100- const dropdownOpen = ref (false )
101- const dropdownRoot = ref (null )
84+ const feedbackOpen = ref (false )
85+ const fbName = ref (' ' )
86+ const fbContent = ref (' ' )
87+ const fbLoading = ref (false )
88+
89+ const canSubmit = computed (() => {
90+ const n = (fbName .value || ' ' ).trim ()
91+ const c = (fbContent .value || ' ' ).trim ()
92+ return n .length > 0 && c .length > 0
93+ })
10294
10395function sync () {
10496 isDark .value = document .documentElement .getAttribute (' data-theme' ) === ' dark'
@@ -108,88 +100,73 @@ onMounted(() => {
108100 sync ()
109101})
110102
111- function toggleDropdown () {
112- dropdownOpen .value = ! dropdownOpen .value
113- }
114-
115- function closeDropdown () {
116- dropdownOpen .value = false
103+ function onToggle () {
104+ applyToggle ()
105+ sync ()
117106}
118107
119- function onDocClick (e ) {
120- const el = dropdownRoot .value
121- if (! el) return
122- // 点击下拉触发器或菜单内部:不收起
123- const target = e? .target
124- if (target && el .contains (target)) return
125- closeDropdown ()
108+ function openFeedback () {
109+ feedbackOpen .value = true
126110}
127111
128- onMounted (() => {
129- document .addEventListener (' click' , onDocClick)
130- })
131-
132- onBeforeUnmount (() => {
133- document .removeEventListener (' click' , onDocClick)
134- })
135-
136- function onToggle () {
137- applyToggle ()
138- sync ()
112+ function closeFeedback () {
113+ if (fbLoading .value ) return
114+ feedbackOpen .value = false
139115}
140116
141- function isActive (path ) {
142- return route .path === path
117+ async function submitFeedback () {
118+ if (! canSubmit .value || fbLoading .value ) return
119+ fbLoading .value = true
120+ try {
121+ await createFeedback ({
122+ submitterName: fbName .value .trim (),
123+ content: fbContent .value .trim ()
124+ })
125+ showMessage (' 提交成功,感谢反馈' )
126+ fbName .value = ' '
127+ fbContent .value = ' '
128+ feedbackOpen .value = false
129+ } catch (e) {
130+ const traceId = e? .traceId ? ` (traceId ${ e .traceId } )` : ' '
131+ const prefix = e? .code ? ` 错误码 ${ e .code } ` : e? .httpStatus ? ` HTTP ${ e .httpStatus } ` : ' '
132+ const msg = e? .message || ' 提交失败'
133+ showMessage (` ${ prefix ? prefix + ' :' : ' ' }${ msg}${ traceId} ` )
134+ } finally {
135+ fbLoading .value = false
136+ }
143137}
144138< / script>
145139
146140< style scoped>
147- .site - nav- dropdown {
148- position: relative;
149- display: inline- flex;
150- }
151-
152- .site - nav- dropdown- trigger {
153- display: inline- flex;
141+ .ob - dialog- overlay {
142+ position: fixed;
143+ inset: 0 ;
144+ z- index: 10060 ;
145+ display: flex;
154146 align- items: center;
155- gap: 6px ;
156- white- space: nowrap;
147+ justify- content: center;
148+ padding: 24px ;
149+ background: rgba (0 , 0 , 0 , 0.42 );
150+ backdrop- filter: blur (6px );
151+ - webkit- backdrop- filter: blur (6px );
157152}
158153
159- .site - nav - dropdown - caret {
160- font - size : 12px ;
161- opacity : 0.85 ;
162- transform : translateY ( - 1px );
154+ .ob - dialog {
155+ width : min ( 520px , 100 % ) ;
156+ padding : 20px 18px 18px ;
157+ box - shadow : 0 16px 48px rgba ( 0 , 0 , 0 , 0.18 );
163158}
164159
165- .site - nav- dropdown- menu {
166- position: absolute;
167- left: 0 ;
168- top: calc (100 % + 8px );
169- background: var (-- bg);
170- border: 1px solid var (-- border);
171- border- radius: 10px ;
172- padding: 6px ;
173- min- width: 160px ;
174- box- shadow: var (-- shadow);
175- z- index: 1000 ;
160+ .ob - dialog- title {
161+ font- size: 18px ;
162+ font- weight: 950 ;
163+ color: var (-- text);
176164}
177165
178- .site - nav- dropdown- item {
179- display: block;
180- text- decoration: none;
181- color: var (-- muted);
182- font- weight: 600 ;
166+ .ob - dialog- desc {
167+ margin- top: 8px ;
183168 font- size: 13px ;
184- padding: 10px 12px ;
185- border- radius: 8px ;
186- transition:
187- background 0 .15s ease,
188- color 0 .15s ease;
189- }
190-
191- .site - nav- dropdown- item: hover {
192- color: var (-- text);
193- background: var (-- accent- bg);
169+ line- height: 1.6 ;
170+ color: var (-- muted);
194171}
195172< / style>
0 commit comments