Skip to content

Commit a471feb

Browse files
author
yqz
committed
大更新
1 parent 837e684 commit a471feb

21 files changed

Lines changed: 748 additions & 17 deletions

.cursor/rules/rule.mdc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
alwaysApply: true
3+
---
4+
5+
任何前端代码的编写都要求和之前项目整体的前端的风格,颜色搭配保持一致

frontend/src/api/feedback-admin.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { request } from './http'
2+
3+
export function fetchPendingFeedback(page = 0, size = 50) {
4+
return request(`/api/v1/admin/feedback/pending?page=${page}&size=${size}`, {
5+
method: 'GET',
6+
withAuth: true
7+
})
8+
}
9+

frontend/src/api/feedback.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { request } from './http'
2+
3+
export function createFeedback(payload) {
4+
return request('/api/v1/feedback', {
5+
method: 'POST',
6+
body: JSON.stringify(payload)
7+
})
8+
}
9+

frontend/src/api/http.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ function resolveApiBase() {
1010
const v = import.meta.env.VITE_API_BASE
1111
if (v === '') return ''
1212
if (v != null && v !== '') return v
13-
return import.meta.env.DEV ? 'http://8.138.32.217:8082' : ''
13+
// 开发环境默认走同域 /api(由 Vite proxy 转到本机后端),避免误连远端导致 token/权限不一致
14+
return ''
1415
}
1516

1617
export const API_BASE = resolveApiBase()
@@ -69,12 +70,14 @@ export async function request(path, options = {}) {
6970
const err = new Error(msg)
7071
err.code = json.code
7172
err.httpStatus = res.status
73+
err.traceId = json.traceId
7274
throw err
7375
}
7476

7577
if (!res.ok) {
7678
const err = new Error(`HTTP ${res.status}`)
7779
err.httpStatus = res.status
80+
err.traceId = json?.traceId
7881
throw err
7982
}
8083

frontend/src/components/BlogHeader.vue

Lines changed: 118 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<template>
2-
<header class="site-top-bar" role="banner">
3-
<div class="site-top-bar-inner">
4-
<div class="site-top-actions">
2+
<div>
3+
<header class="site-top-bar" role="banner">
4+
<div class="site-top-bar-inner">
5+
<div class="site-top-actions">
56
<a
67
class="top-action-btn"
78
href="https://github.com/yyyCode/OpenBlog.git"
@@ -18,15 +19,14 @@
1819
</span>
1920
项目源码
2021
</a>
21-
<button type="button" class="top-action-btn" aria-label="面试内容" @click="onInterviewClick">
22+
<button type="button" class="top-action-btn" aria-label="问题反馈" @click="openFeedback">
2223
<span class="top-action-ico" aria-hidden="true">
2324
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
24-
<path d="M9 2h6v3H9z" />
25-
<path d="M7 5h10a2 2 0 0 1 2 2v12a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3V7a2 2 0 0 1 2-2z" />
26-
<path d="M9 11h6M9 15h6" />
25+
<path d="M4 4h16v16H4z" />
26+
<path d="M22 6l-10 7L2 6" />
2727
</svg>
2828
</span>
29-
面试内容
29+
问题反馈
3030
</button>
3131
</div>
3232
<button
@@ -37,17 +37,60 @@
3737
>
3838
<span class="theme-toggle-icon" aria-hidden="true">{{ isDark ? '☀' : '☽' }}</span>
3939
<span class="theme-toggle-text">{{ isDark ? '浅色' : '深色' }}</span>
40-
</button>
41-
</div>
42-
</header>
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>
4375
</template>
4476

4577
<script setup>
46-
import { onMounted, ref } from 'vue'
78+
import { computed, onMounted, ref } from 'vue'
4779
import { toggleTheme as applyToggle } from '../theme'
4880
import { showMessage } from '../utils/message'
81+
import { createFeedback } from '../api/feedback'
4982
5083
const isDark = ref(false)
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+
})
5194
5295
function sync() {
5396
isDark.value = document.documentElement.getAttribute('data-theme') === 'dark'
@@ -62,7 +105,68 @@ function onToggle() {
62105
sync()
63106
}
64107
65-
function onInterviewClick() {
66-
showMessage('功能暂未开放')
108+
function openFeedback() {
109+
feedbackOpen.value = true
110+
}
111+
112+
function closeFeedback() {
113+
if (fbLoading.value) return
114+
feedbackOpen.value = false
115+
}
116+
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+
}
67137
}
68138
</script>
139+
140+
<style scoped>
141+
.ob-dialog-overlay {
142+
position: fixed;
143+
inset: 0;
144+
z-index: 10060;
145+
display: flex;
146+
align-items: center;
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);
152+
}
153+
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);
158+
}
159+
160+
.ob-dialog-title {
161+
font-size: 18px;
162+
font-weight: 950;
163+
color: var(--text);
164+
}
165+
166+
.ob-dialog-desc {
167+
margin-top: 8px;
168+
font-size: 13px;
169+
line-height: 1.6;
170+
color: var(--muted);
171+
}
172+
</style>

frontend/src/layouts/ConsoleLayout.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@
7474
</span>
7575
评论
7676
</router-link>
77+
<router-link to="/console/feedback" class="console-nav-item" active-class="active">
78+
<span class="console-nav-ico" aria-hidden="true">
79+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
80+
<path d="M4 4h16v16H4z" />
81+
<path d="M22 6l-10 7L2 6" />
82+
</svg>
83+
</span>
84+
问题反馈
85+
</router-link>
7786
<router-link to="/console/attachments" class="console-nav-item" active-class="active">
7887
<span class="console-nav-ico" aria-hidden="true">
7988
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">

frontend/src/router/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import ConsoleChangelogView from '../views/ConsoleChangelogView.vue'
1717
import ConsoleAttachmentsView from '../views/ConsoleAttachmentsView.vue'
1818
import ConsoleCommentsView from '../views/ConsoleCommentsView.vue'
1919
import ConsoleSystemView from '../views/ConsoleSystemView.vue'
20+
import ConsoleFeedbackView from '../views/ConsoleFeedbackView.vue'
2021

2122
const routes = [
2223
{
@@ -44,6 +45,7 @@ const routes = [
4445
{ path: 'articles/manage', name: 'consoleArticleManage', component: ConsoleArticleManageView },
4546
{ path: 'articles/new', name: 'consoleArticleCompose', component: ConsoleArticleComposeView },
4647
{ path: 'changelog', name: 'consoleChangelog', component: ConsoleChangelogView },
48+
{ path: 'feedback', name: 'consoleFeedback', component: ConsoleFeedbackView },
4749
{ path: 'attachments', name: 'consoleAttachments', component: ConsoleAttachmentsView },
4850
{ path: 'comments', name: 'consoleComments', component: ConsoleCommentsView },
4951
{ path: 'system', name: 'consoleSystem', component: ConsoleSystemView }

frontend/src/views/ConsoleChangelogView.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,10 @@ async function saveChangelog() {
198198
} catch (e) {
199199
const apiCode = e?.code
200200
const httpStatus = e?.httpStatus
201+
const traceId = e?.traceId ? `traceId ${e.traceId}` : ''
201202
const prefix = apiCode ? `错误码 ${apiCode}` : httpStatus ? `HTTP ${httpStatus}` : ''
202-
changelogError.value = prefix ? `${prefix}${e?.message || '保存失败'}` : e?.message || '保存失败'
203+
const head = [prefix, traceId].filter(Boolean).join('')
204+
changelogError.value = head ? `${head}${e?.message || '保存失败'}` : e?.message || '保存失败'
203205
changelogSuccess.value = ''
204206
}
205207
}
@@ -216,8 +218,10 @@ async function removeChangelog() {
216218
} catch (e) {
217219
const apiCode = e?.code
218220
const httpStatus = e?.httpStatus
221+
const traceId = e?.traceId ? `traceId ${e.traceId}` : ''
219222
const prefix = apiCode ? `错误码 ${apiCode}` : httpStatus ? `HTTP ${httpStatus}` : ''
220-
changelogError.value = prefix ? `${prefix}:${e?.message || '删除失败'}` : e?.message || '删除失败'
223+
const head = [prefix, traceId].filter(Boolean).join(',')
224+
changelogError.value = head ? `${head}:${e?.message || '删除失败'}` : e?.message || '删除失败'
221225
changelogSuccess.value = ''
222226
}
223227
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<template>
2+
<div class="console-page">
3+
<header class="console-page-header">
4+
<div class="console-page-title">
5+
<h1>问题反馈</h1>
6+
<div style="color: var(--console-muted, var(--muted)); font-size: 13px; margin-top: 6px">
7+
展示所有待处理反馈(同一 IP 每天仅可提交一次)。
8+
</div>
9+
</div>
10+
<div>
11+
<button class="btn" style="padding: 8px 12px" @click="load">刷新</button>
12+
</div>
13+
</header>
14+
15+
<div class="console-card console-inner-card">
16+
<div v-if="loading" style="color: var(--console-muted, var(--muted))">加载中...</div>
17+
<div v-else-if="items.length === 0" style="color: var(--console-muted, var(--muted))">暂无待处理反馈</div>
18+
19+
<div v-else class="fb-list">
20+
<div v-for="f in items" :key="f.id" class="fb-item">
21+
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 12px">
22+
<div style="font-weight: 950; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">
23+
{{ f.submitterName || '匿名' }}
24+
</div>
25+
<div style="color: var(--console-muted, var(--muted)); font-size: 12px; white-space: nowrap">
26+
{{ formatDateTime(f.createdAt) }}
27+
</div>
28+
</div>
29+
<div style="margin-top: 10px; white-space: pre-wrap; line-height: 1.7">{{ f.content }}</div>
30+
<div style="margin-top: 10px; color: var(--console-muted, var(--muted)); font-size: 12px">
31+
提交日:{{ f.submitDay || '' }} · 状态:{{ f.status || '' }} · ID:{{ f.id }}
32+
</div>
33+
</div>
34+
</div>
35+
36+
<div v-if="error" class="error" style="margin-top: 10px">{{ error }}</div>
37+
</div>
38+
</div>
39+
</template>
40+
41+
<script setup>
42+
import { onMounted, ref } from 'vue'
43+
import { fetchPendingFeedback } from '../api/feedback-admin'
44+
45+
const loading = ref(false)
46+
const items = ref([])
47+
const error = ref('')
48+
49+
function formatDateTime(v) {
50+
if (!v) return ''
51+
const t = new Date(v).getTime()
52+
if (Number.isNaN(t)) return ''
53+
return new Date(t).toISOString().replace('T', ' ').slice(0, 19)
54+
}
55+
56+
async function load() {
57+
loading.value = true
58+
error.value = ''
59+
try {
60+
const resp = await fetchPendingFeedback(0, 100)
61+
items.value = resp?.items || []
62+
} catch (e) {
63+
const traceId = e?.traceId ? `traceId ${e.traceId}` : ''
64+
const prefix = e?.code ? `错误码 ${e.code}` : e?.httpStatus ? `HTTP ${e.httpStatus}` : ''
65+
const head = [prefix, traceId].filter(Boolean).join('')
66+
error.value = head ? `${head}${e?.message || '加载失败'}` : e?.message || '加载失败'
67+
} finally {
68+
loading.value = false
69+
}
70+
}
71+
72+
onMounted(() => load())
73+
</script>
74+
75+
<style scoped>
76+
.fb-list {
77+
display: flex;
78+
flex-direction: column;
79+
gap: 12px;
80+
}
81+
82+
.fb-item {
83+
padding: 14px 14px;
84+
border: 1px solid var(--border);
85+
border-radius: 12px;
86+
background: var(--surface-soft);
87+
}
88+
</style>
89+

src/main/java/com/yqz/openblog/common/GlobalExceptionHandler.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import org.slf4j.LoggerFactory;
88
import org.springframework.web.bind.annotation.ExceptionHandler;
99
import org.springframework.web.bind.annotation.RestControllerAdvice;
10+
import org.springframework.security.access.AccessDeniedException;
11+
import org.springframework.security.authorization.AuthorizationDeniedException;
1012

1113
/**
1214
* 统一异常处理(MVP)。
@@ -29,6 +31,15 @@ public ResponseEntity<ApiResponse<Object>> onValidation(MethodArgumentNotValidEx
2931
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResponse.fail(4001, "参数校验失败"));
3032
}
3133

34+
/**
35+
* 方法级鉴权(@PreAuthorize)失败会抛出 AuthorizationDeniedException;
36+
* 以前会落到兜底 Exception -> 5001,导致前端误判为“服务器异常”。
37+
*/
38+
@ExceptionHandler({AuthorizationDeniedException.class, AccessDeniedException.class})
39+
public ResponseEntity<ApiResponse<Object>> onAccessDenied(Exception ex) {
40+
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiResponse.fail(4030, "无权限"));
41+
}
42+
3243
@ExceptionHandler(Exception.class)
3344
public ResponseEntity<ApiResponse<Object>> onOther(Exception ex) {
3445
log.error("unhandled server error", ex);

0 commit comments

Comments
 (0)