From c4a9cad1c50e89365e2a58b50e259af642ed3b8c Mon Sep 17 00:00:00 2001 From: Codex Assistant <codex@example.com> Date: 星期二, 07 十月 2025 16:12:20 +0800 Subject: [PATCH] feat(review): 调整评审详情展示顺序与样式,描述支持多行,项目信息列宽40/60 fix(auth): 登录页与首页循环跳转保护;api.ts 在登录页不再重定向;401分支在登录页不跳转 fix(router): /login 放行策略优化,避免死循环;评审列表跳转到 /project-review/:id/detail fix(frontend): 补齐 utils/appConfig.ts,避免启动白屏 fix(review): 详情页提交评分缺少stageId时回退使用项目详情的stageId feat(backend): ActivityPlayerDetailResponse.playerInfo 补充 avatarUrl/avatar,服务组装时填充用户头像 chore(dev): 启动脚本注入本地JWT密钥,重启前后端 --- backend/src/main/java/com/rongyichuang/player/service/PlayerApplicationService.java | 4 web/src/utils/auth.ts | 44 +++++++++++ backend/src/main/java/com/rongyichuang/player/dto/response/PlayerInfoResponse.java | 8 ++ web/src/utils/appConfig.ts | 15 +++ web/src/router/index.ts | 13 +- web/src/views/review-detail.vue | 69 ++++++++++++++-- backend/src/main/java/com/rongyichuang/player/service/ActivityPlayerDetailService.java | 3 web/src/config/api.ts | 43 ++++++++++ web/src/views/review-list.vue | 2 9 files changed, 177 insertions(+), 24 deletions(-) diff --git a/backend/src/main/java/com/rongyichuang/player/dto/response/PlayerInfoResponse.java b/backend/src/main/java/com/rongyichuang/player/dto/response/PlayerInfoResponse.java index e09ee6f..b467571 100644 --- a/backend/src/main/java/com/rongyichuang/player/dto/response/PlayerInfoResponse.java +++ b/backend/src/main/java/com/rongyichuang/player/dto/response/PlayerInfoResponse.java @@ -14,6 +14,8 @@ private String education; // 瀛﹀巻 private String introduction; // 涓汉浠嬬粛 private String description; // 绠�浠� + private String avatarUrl; // 澶村儚URL锛堢洿鎺ョ敤浜庡墠绔睍绀猴級 + private MediaResponse avatar; // 澶村儚濯掍綋璇︽儏 private PlayerUserInfoResponse userInfo; // 鍏宠仈鐨勭敤鎴蜂俊鎭紝鍖呭惈澶村儚 // Constructors @@ -44,6 +46,12 @@ public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } + public String getAvatarUrl() { return avatarUrl; } + public void setAvatarUrl(String avatarUrl) { this.avatarUrl = avatarUrl; } + + public MediaResponse getAvatar() { return avatar; } + public void setAvatar(MediaResponse avatar) { this.avatar = avatar; } + public PlayerUserInfoResponse getUserInfo() { return userInfo; } public void setUserInfo(PlayerUserInfoResponse userInfo) { this.userInfo = userInfo; } } \ No newline at end of file diff --git a/backend/src/main/java/com/rongyichuang/player/service/ActivityPlayerDetailService.java b/backend/src/main/java/com/rongyichuang/player/service/ActivityPlayerDetailService.java index 1433984..ba7601e 100644 --- a/backend/src/main/java/com/rongyichuang/player/service/ActivityPlayerDetailService.java +++ b/backend/src/main/java/com/rongyichuang/player/service/ActivityPlayerDetailService.java @@ -205,6 +205,9 @@ String avatarUrl = buildFullMediaUrl(avatar.getPath()); userInfo.setAvatarUrl(avatarUrl); userInfo.setAvatar(convertToMediaResponse(avatar)); + // 鍚屾璧嬪�煎埌 playerInfo锛屼究浜庡墠绔洿鎺ヤ娇鐢� playerInfo.avatarUrl + playerInfo.setAvatarUrl(avatarUrl); + playerInfo.setAvatar(convertToMediaResponse(avatar)); log.info("鎵惧埌鐢ㄦ埛澶村儚: {}", avatarUrl); } else { log.info("璋冭瘯锛氭湭鎵惧埌鐢ㄦ埛澶村儚锛寀serId: {}, targetType: {}", userId, MediaTargetType.USER_AVATAR.getValue()); diff --git a/backend/src/main/java/com/rongyichuang/player/service/PlayerApplicationService.java b/backend/src/main/java/com/rongyichuang/player/service/PlayerApplicationService.java index 442d5ec..27bf71a 100644 --- a/backend/src/main/java/com/rongyichuang/player/service/PlayerApplicationService.java +++ b/backend/src/main/java/com/rongyichuang/player/service/PlayerApplicationService.java @@ -83,9 +83,7 @@ if (activityId != null) { q.setParameter("activityId", activityId); } - if (regionId != null) { - q.setParameter("regionId", regionId); - } + if (state != null) { q.setParameter("state", state); } diff --git a/web/src/config/api.ts b/web/src/config/api.ts index c1430be..ecbdb5e 100644 --- a/web/src/config/api.ts +++ b/web/src/config/api.ts @@ -17,9 +17,20 @@ // GraphQL璇锋眰宸ュ叿鍑芥暟 export const graphqlRequest = async (query: string, variables: any = {}) => { - // 鑾峰彇JWT token - const { getToken } = await import('@/utils/auth'); + // 鑾峰彇JWT token涓庡伐鍏� + const { getToken, isTokenExpired, clearAuth } = await import('@/utils/auth'); const token = getToken(); + + // 鑻oken杩囨湡锛岀洿鎺ユ竻鐞嗗苟璺崇櫥褰� + if (!token || isTokenExpired(token)) { + clearAuth(); + // 閬垮厤鍦ㄧ櫥褰曢〉閲嶅璺宠浆閫犳垚鐧藉睆/寰幆 + const atLogin = typeof window !== 'undefined' && window.location && window.location.hash?.startsWith('#/login'); + if (!atLogin) { + window.location.href = '/#/login'; + } + throw new Error('Token expired or missing') + } // 鏋勫缓璇锋眰澶� const headers: Record<string, string> = { @@ -45,13 +56,39 @@ }); if (!response.ok) { + // 澶勭悊401鏈巿鏉� + if (response.status === 401) { + const { clearAuth } = await import('@/utils/auth'); + clearAuth(); + const atLogin = typeof window !== 'undefined' && window.location && window.location.hash?.startsWith('#/login'); + if (!atLogin) { + window.location.href = '/#/login'; + } + } throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); if (result.errors) { - throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`); + const msg = JSON.stringify(result.errors) || '' + // 璇嗗埆璁よ瘉绫婚敊璇叧閿瓧 + const isAuthError = + msg.includes('Unauthorized') || + msg.includes('璁よ瘉') || + msg.includes('unauthorized') || + msg.includes('invalid token') || + msg.includes('expired') + + if (isAuthError) { + const { clearAuth } = await import('@/utils/auth'); + clearAuth(); + const atLogin = typeof window !== 'undefined' && window.location && window.location.hash?.startsWith('#/login'); + if (!atLogin) { + window.location.href = '/#/login'; + } + } + throw new Error(`GraphQL errors: ${msg}`); } return result; diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 617baaf..3aa9c0a 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -1,5 +1,5 @@ import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' -import { isLoggedIn } from '@/utils/auth' +import { isLoggedIn, getToken, isTokenExpired } from '@/utils/auth' const routes: RouteRecordRaw[] = [ { @@ -148,8 +148,9 @@ router.beforeEach((to, from, next) => { // 濡傛灉鏄櫥褰曢〉闈紝鐩存帴鏀捐 if (to.path === '/login') { - // 濡傛灉宸茬粡鐧诲綍锛岄噸瀹氬悜鍒伴椤� - if (isLoggedIn()) { + // 浠呭湪鈥滄湁token涓旀湭杩囨湡涓旀湰鍦板凡璁板綍鐧诲綍淇℃伅鈥濇椂鎵嶄粠鐧诲綍椤佃烦杞埌棣栭〉 + const t = getToken() + if (t && !isTokenExpired(t) && isLoggedIn()) { next('/') } else { next() @@ -157,9 +158,9 @@ return } - // 妫�鏌ユ槸鍚﹀凡鐧诲綍 - if (!isLoggedIn()) { - // 鏈櫥褰曪紝閲嶅畾鍚戝埌鐧诲綍椤� + // 妫�鏌ユ槸鍚﹀凡鐧诲綍涓攖oken鏈繃鏈� + const token = getToken() + if (!token || isTokenExpired(token) || !isLoggedIn()) { next('/login') return } diff --git a/web/src/utils/appConfig.ts b/web/src/utils/appConfig.ts new file mode 100644 index 0000000..9f974ab --- /dev/null +++ b/web/src/utils/appConfig.ts @@ -0,0 +1,15 @@ +// 杞婚噺鐨勫簲鐢ㄩ厤缃姞杞藉櫒锛氬湪鏈櫥褰曠姸鎬佷笅涔熷彲瀹夊叏鎵ц锛屼笉渚濊禆 graphqlRequest锛岄伩鍏嶉噸瀹氬悜寰幆 +export async function loadAppConfig(): Promise<string> { + try { + const res = await fetch('/api/config', { method: 'GET' }) + if (res.ok) { + const data = await res.json().catch(() => ({} as any)) + // 鍏煎鍚庣鏈彁渚涘瓧娈电殑鎯呭喌 + return (data && (data.mediaBaseUrl || data.media_base_url || data.media || '')) || '' + } + } catch { + // 蹇界暐缃戠粶/401閿欒锛岃繑鍥為粯璁ゅ�硷紝淇濊瘉搴旂敤鍙覆鏌撶櫥褰曢〉 + } + // 榛樿杩斿洖绌哄瓧绗︿覆锛屼笉褰卞搷椤甸潰娓叉煋 + return '' +} \ No newline at end of file diff --git a/web/src/utils/auth.ts b/web/src/utils/auth.ts index a55e90c..3cb965e 100644 --- a/web/src/utils/auth.ts +++ b/web/src/utils/auth.ts @@ -75,6 +75,50 @@ return !!(token && userInfo) } +/** + * 瑙f瀽骞跺垽鏂璊WT鏄惁杩囨湡 + * 瑙勫垯锛氳嫢token涓虹┖鎴栨棤娉曡В鏋恊xp锛岃涓鸿繃鏈燂紱exp鍗曚綅涓虹 + */ +export function isTokenExpired(token?: string | null): boolean { + if (!token) return true + try { + const parts = token.split('.') + if (parts.length !== 3) return true + const payloadJson = JSON.parse(decodeBase64Url(parts[1])) + const exp = payloadJson?.exp + if (!exp || typeof exp !== 'number') return true + const now = Math.floor(Date.now() / 1000) + return exp <= now + } catch (e) { + console.error('瑙f瀽JWT澶辫触:', e) + return true + } +} + +/** + * Base64Url 瑙g爜 + */ +function decodeBase64Url(input: string): string { + // 鏇挎崲URL瀹夊叏瀛楃骞惰ˉ榻�'=' + let base64 = input.replace(/-/g, '+').replace(/_/g, '/') + const pad = base64.length % 4 + if (pad) { + base64 += '='.repeat(4 - pad) + } + const decoded = atob(base64) + // 澶勭悊UTF-8 + try { + return decodeURIComponent( + decoded + .split('') + .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) + .join('') + ) + } catch { + return decoded + } +} + // 娓呴櫎鎵�鏈夎璇佹暟鎹� export function clearAuth(): void { removeToken() diff --git a/web/src/views/review-detail.vue b/web/src/views/review-detail.vue index e015e26..848948b 100644 --- a/web/src/views/review-detail.vue +++ b/web/src/views/review-detail.vue @@ -14,12 +14,15 @@ <div class="project-section"> <!-- 椤圭洰鍩烘湰淇℃伅 --> <h4>椤圭洰淇℃伅</h4> - <el-descriptions :column="2" border> - <el-descriptions-item label="椤圭洰鍚嶇О"> - {{ projectDetail.projectName || '鏈~鍐�' }} + <el-descriptions :column="2" border class="project-info"> + <el-descriptions-item label="姣旇禌鍚嶇О" :span="2"> + {{ competitionName || '鏈~鍐�' }} </el-descriptions-item> - <el-descriptions-item label="姣旇禌鍚嶇О"> - {{ projectDetail.activityName }} + <el-descriptions-item label="姣旇禌闃舵" :span="2"> + {{ stageName || projectDetail.activityName || '鏈~鍐�' }} + </el-descriptions-item> + <el-descriptions-item label="鍙傝禌椤圭洰鍚嶇О" :span="2"> + {{ projectDetail.projectName || '鏈~鍐�' }} </el-descriptions-item> <el-descriptions-item label="椤圭洰鎻忚堪" :span="2"> <div class="description-content"> @@ -183,7 +186,7 @@ import { useRoute, useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' import { Document, UserFilled } from '@element-plus/icons-vue' -import { getProjectDetail, getRatingStats, submitRating, getCurrentJudgeRating } from '@/api/projectReview' +import { getProjectDetail, getRatingStats, submitRating, getCurrentJudgeRating, getActiveActivities } from '@/api/projectReview' import { userApi } from '@/api/user' import { getUserInfo } from '@/utils/auth' @@ -195,6 +198,8 @@ const submitting = ref(false) const projectDetail = ref(null) const ratingStats = ref({ ratingCount: 0, averageScore: 0 }) +const competitionName = ref('') +const stageName = ref('') const ratingItems = ref([]) const ratingComment = ref('') const previewVisible = ref(false) @@ -211,7 +216,25 @@ // 璁$畻灞炴�� const projectId = computed(() => route.params.id) -const stageId = computed(() => route.query.stageId) +const stageId = computed(() => route.query.stageId || (projectDetail.value ? projectDetail.value.stageId : null)) + +const loadStageMeta = async () => { + try { + if (!projectDetail.value || !projectDetail.value.stageId) return + const stages = await getActiveActivities() + const stage = (stages || []).find(s => String(s.id) === String(projectDetail.value.stageId)) + if (stage) { + stageName.value = stage.name || '' + competitionName.value = stage.parent?.name || '' + } else { + stageName.value = projectDetail.value.activityName || '' + competitionName.value = '' + } + } catch (e) { + stageName.value = projectDetail.value?.activityName || '' + competitionName.value = '' + } +} // 鏉冮檺楠岃瘉鏂规硶 const checkPermissions = async () => { @@ -331,6 +354,7 @@ try { const data = await getProjectDetail(projectId.value) projectDetail.value = data + await loadStageMeta() // 鍒濆鍖栬瘎鍒嗛」 if (data.ratingForm && data.ratingForm.items) { @@ -373,12 +397,13 @@ return } - // 楠岃瘉stageId - if (!stageId.value) { + // 缁熶竴鑾峰彇stageId锛堜紭鍏堣矾鐢卞弬鏁帮紝鍏舵璇︽儏閲岀殑stageId锛� + const sid = stageId.value ? parseInt(stageId.value) : (projectDetail.value?.stageId ? parseInt(projectDetail.value.stageId) : null) + if (!sid) { ElMessage.error('缂哄皯姣旇禌闃舵淇℃伅锛岃閲嶆柊杩涘叆椤甸潰') return } - + // 楠岃瘉璇勫垎 const hasEmptyScore = ratingItems.value.some(item => item.score === 0 || item.score === null) if (hasEmptyScore) { @@ -397,7 +422,7 @@ const ratingData = { activityPlayerId: parseInt(projectId.value), - stageId: parseInt(stageId.value), + stageId: sid, ratings: ratingItems.value.map(item => ({ itemId: item.id, score: item.score @@ -518,6 +543,7 @@ .description-content { line-height: 1.6; color: #606266; + white-space: pre-wrap; } .attachments { @@ -685,4 +711,25 @@ :deep(.el-card__body) { padding: 16px; } + +/* 浠呴拡瀵归」鐩俊鎭繖缁勬弿杩拌缃爣绛�/鍐呭瀹藉害姣斾緥 */ +.project-info :deep(.el-descriptions__label) { + width: 40% !important; + min-width: 40%; + box-sizing: border-box; +} +.project-info :deep(.el-descriptions__content) { + width: 60% !important; + min-width: 60%; + box-sizing: border-box; +} + +/* 绐勫睆鑷�傚簲锛氬皬灞忔椂鍥為��涓轰笂涓嬬粨鏋� */ +@media (max-width: 768px) { + .project-info :deep(.el-descriptions__label), + .project-info :deep(.el-descriptions__content) { + width: 100% !important; + min-width: 100%; + } +} </style> \ No newline at end of file diff --git a/web/src/views/review-list.vue b/web/src/views/review-list.vue index f2e344d..932877b 100644 --- a/web/src/views/review-list.vue +++ b/web/src/views/review-list.vue @@ -496,7 +496,7 @@ // 鏌ョ湅椤圭洰璇︽儏 const viewDetails = (projectId: number) => { - router.push(`/review/detail/${projectId}`) + router.push(`/project-review/${projectId}/detail`) } // 缁勪欢鎸傝浇鏃跺姞杞芥暟鎹� -- Gitblit v1.8.0