diff --git a/schoolNewsServ/achievement/src/main/java/org/xyzh/achievement/controller/AchievementController.java b/schoolNewsServ/achievement/src/main/java/org/xyzh/achievement/controller/AchievementController.java index 84a1a84..54e4260 100644 --- a/schoolNewsServ/achievement/src/main/java/org/xyzh/achievement/controller/AchievementController.java +++ b/schoolNewsServ/achievement/src/main/java/org/xyzh/achievement/controller/AchievementController.java @@ -101,6 +101,22 @@ public class AchievementController { return achievementService.getMyAchievementsWithProgress(type); } + /** + * 获取当前用户的等级成就状态(当前等级 + 下一等级) + */ + @GetMapping("/my/level-status") + public ResultDomain> getMyLevelStatus() { + return achievementService.getMyLevelStatus(); + } + + /** + * 获取当前用户“已获得”的成就(不含未获得,支持按类型筛选) + */ + @GetMapping("/my/obtained") + public ResultDomain getMyObtainedAchievements(@RequestParam(required = false, value = "type") Integer type) { + return achievementService.getMyAchievements(type); + } + /** * 检查用户是否已获得成就 */ diff --git a/schoolNewsServ/achievement/src/main/java/org/xyzh/achievement/service/impl/ACHAchievementServiceImpl.java b/schoolNewsServ/achievement/src/main/java/org/xyzh/achievement/service/impl/ACHAchievementServiceImpl.java index 90a114d..c0c5be0 100644 --- a/schoolNewsServ/achievement/src/main/java/org/xyzh/achievement/service/impl/ACHAchievementServiceImpl.java +++ b/schoolNewsServ/achievement/src/main/java/org/xyzh/achievement/service/impl/ACHAchievementServiceImpl.java @@ -274,6 +274,86 @@ public class ACHAchievementServiceImpl implements AchievementService { // ==================== 用户成就管理 ==================== + @Override + public ResultDomain> getMyLevelStatus() { + ResultDomain> rd = new ResultDomain<>(); + try { + // 获取当前登录用户 + TbSysUser user = LoginUtil.getCurrentUser(); + if (user == null) { + rd.fail("请先登录"); + return rd; + } + + String userID = user.getID(); + // 仅查询等级类成就 + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + List levels = achievementMapper.selectUserAchievementsWithProgress( + userID, + 2, + userDeptRoles + ); + + if (levels == null || levels.isEmpty()) { + Map payload = new HashMap<>(); + payload.put("current", null); + payload.put("next", null); + rd.success("无等级成就", payload); + return rd; + } + + // 调试日志:查看查询到的等级成就数量和详情 + logger.info("查询到等级成就数量: {}", levels.size()); + for (AchievementVO vo : levels) { + logger.info("等级成就: id={}, name={}, level={}, obtained={}", + vo.getAchievementID(), vo.getName(), vo.getLevel(), vo.getObtained()); + } + + // Float 等级排序器 + Comparator byLevelAsc = Comparator.comparing( + AchievementVO::getLevel, + Comparator.nullsFirst(Comparator.naturalOrder()) + ); + + // 当前:已获得的最高等级,否则取最低等级 + List obtained = levels.stream() + .filter(a -> Boolean.TRUE.equals(a.getObtained())) + .sorted(byLevelAsc.reversed()) + .collect(Collectors.toList()); + + AchievementVO current; + if (!obtained.isEmpty()) { + current = obtained.get(0); + } else { + current = levels.stream() + .sorted(byLevelAsc) + .findFirst() + .orElse(null); + } + + // 下一等级:比当前等级大的最小等级 + AchievementVO next = null; + if (current != null && current.getLevel() != null) { + float curLevel = current.getLevel(); + next = levels.stream() + .filter(a -> (a.getLevel() != null) && a.getLevel() > curLevel) + .sorted(byLevelAsc) + .findFirst() + .orElse(null); + } + + Map payload = new HashMap<>(); + payload.put("current", current); + payload.put("next", next); + rd.success("获取等级状态成功", payload); + return rd; + } catch (Exception e) { + logger.error("获取等级状态异常: {}", e.getMessage(), e); + rd.fail("获取等级状态失败: " + e.getMessage()); + return rd; + } + } + @Override public ResultDomain getUserAchievements(String userID, Integer type) { ResultDomain resultDomain = new ResultDomain<>(); @@ -283,14 +363,23 @@ public class ACHAchievementServiceImpl implements AchievementService { return resultDomain; } - List list; - if (type != null) { - list = userAchievementMapper.selectByUserIdAndType(userID, type); - } else { - list = userAchievementMapper.selectByUserId(userID); + // 通过联表查询(带权限与进度信息),只取“已获得”的记录 + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + List voList = achievementMapper.selectUserAchievementsWithProgress(userID, type, userDeptRoles); + + List obtainedList = new ArrayList<>(); + for (AchievementVO vo : voList) { + if (Boolean.TRUE.equals(vo.getObtained())) { + TbUserAchievement ua = new TbUserAchievement(); + ua.setID(vo.getUserAchievementID()); + ua.setUserID(userID); + ua.setAchievementID(vo.getAchievementID()); + ua.setObtainTime(vo.getObtainTime()); + obtainedList.add(ua); + } } - resultDomain.success("获取用户成就成功", list); + resultDomain.success("获取用户成就成功", obtainedList); return resultDomain; } catch (Exception e) { logger.error("获取用户成就异常: {}", e.getMessage(), e); @@ -861,8 +950,8 @@ public class ACHAchievementServiceImpl implements AchievementService { } // 检查是否需要更新等级(只有当成就等级高于当前用户等级时才更新) - Integer currentLevel = userInfo.getLevel() != null ? userInfo.getLevel() : 0; - Integer achievementLevel = achievement.getLevel(); + Float currentLevel = userInfo.getLevel() != null ? userInfo.getLevel() : 0; + Float achievementLevel = achievement.getLevel(); if (achievementLevel > currentLevel) { userInfo.setLevel(achievementLevel); diff --git a/schoolNewsServ/achievement/src/main/resources/mapper/AchievementMapper.xml b/schoolNewsServ/achievement/src/main/resources/mapper/AchievementMapper.xml index fd96160..104c80b 100644 --- a/schoolNewsServ/achievement/src/main/resources/mapper/AchievementMapper.xml +++ b/schoolNewsServ/achievement/src/main/resources/mapper/AchievementMapper.xml @@ -363,7 +363,7 @@ diff --git a/schoolNewsServ/api/api-achievement/src/main/java/org/xyzh/api/achievement/AchievementService.java b/schoolNewsServ/api/api-achievement/src/main/java/org/xyzh/api/achievement/AchievementService.java index 86fc7d6..4a1b62f 100644 --- a/schoolNewsServ/api/api-achievement/src/main/java/org/xyzh/api/achievement/AchievementService.java +++ b/schoolNewsServ/api/api-achievement/src/main/java/org/xyzh/api/achievement/AchievementService.java @@ -91,6 +91,12 @@ public interface AchievementService { * @return ResultDomain 成就视图对象列表 */ ResultDomain getMyAchievementsWithProgress(Integer type); + + /** + * @description 获取当前用户的等级成就状态(包含“当前等级成就”和“下一等级成就”) + * @return ResultDomain> { current: AchievementVO|null, next: AchievementVO|null } + */ + ResultDomain> getMyLevelStatus(); /** * @description 检查用户是否已获得成就 diff --git a/schoolNewsWeb/src/apis/achievement/achievement.ts b/schoolNewsWeb/src/apis/achievement/achievement.ts index 45eefd6..928d3c3 100644 --- a/schoolNewsWeb/src/apis/achievement/achievement.ts +++ b/schoolNewsWeb/src/apis/achievement/achievement.ts @@ -108,6 +108,29 @@ export const achievementApi = { return response.data; }, + /** + * 获取当前用户的等级成就状态(当前等级 + 下一等级) + * @returns Promise>> { current: AchievementVO|null, next: AchievementVO|null } + */ + async getMyLevelStatus(): Promise>> { + const response = await api.get>('/achievements/my/level-status'); + return response.data; + }, + + /** + * 获取当前用户“已获得”的成就(不含未获得) + * @param type 成就类型(可选) + * @returns Promise> + */ + async getMyObtainedAchievements(type?: number): Promise> { + const params: Record = {}; + if (type !== undefined) { + params.type = type; + } + const response = await api.get('/achievements/my/obtained', params); + return response.data; + }, + /** * 检查用户是否已获得成就 * @param userID 用户ID diff --git a/schoolNewsWeb/src/utils/iconUtils.ts b/schoolNewsWeb/src/utils/iconUtils.ts index 4cbdda9..b0b4262 100644 --- a/schoolNewsWeb/src/utils/iconUtils.ts +++ b/schoolNewsWeb/src/utils/iconUtils.ts @@ -4,7 +4,7 @@ * @since 2025-10-27 */ -import { PUBLIC_IMG_PATH } from '@/config'; +import { PUBLIC_IMG_PATH, FILE_DOWNLOAD_URL } from '@/config'; /** * 获取图标完整路径 @@ -30,6 +30,12 @@ export function getIconUrl(icon?: string, subPath = 'achievement'): string { return icon; } + // 如果是文件ID(无路径、无扩展名,且长度较长),拼接下载地址 + const isFileId = !icon.includes('/') && !icon.includes('.') && /^(?=.{16,}$)[A-Za-z0-9_-]+$/.test(icon); + if (isFileId) { + return `${FILE_DOWNLOAD_URL}${icon}`; + } + // 如果已经包含完整路径(带 schoolNewsWeb 前缀) if (icon.startsWith('/schoolNewsWeb/')) { return icon; diff --git a/schoolNewsWeb/src/views/admin/manage/achievement/AchievementManagementView.vue b/schoolNewsWeb/src/views/admin/manage/achievement/AchievementManagementView.vue index 29cfafe..28bc8fc 100644 --- a/schoolNewsWeb/src/views/admin/manage/achievement/AchievementManagementView.vue +++ b/schoolNewsWeb/src/views/admin/manage/achievement/AchievementManagementView.vue @@ -152,33 +152,16 @@ /> - - - -
-
- - - -
-
路径: {{ currentAchievement.icon }}
-
完整URL: {{ getIconUrl(currentAchievement.icon) }}
-
-
-
+
@@ -322,6 +305,7 @@ import type { Achievement, UserAchievement } from '@/types'; import { AchievementEnumHelper } from '@/types/enums/achievement-enums'; import { getAchievementIconUrl } from '@/utils/iconUtils'; import { AdminLayout } from '@/views/admin'; +import FileUpload from '@/components/file/FileUpload.vue'; defineOptions({ name: 'AchievementManagementView' @@ -337,7 +321,6 @@ const achieverList = ref([]); // 对话框控制 const achievementDialogVisible = ref(false); const usersDialogVisible = ref(false); -const showIconUpload = ref(false); const isEdit = ref(false); // 筛选条件 @@ -345,6 +328,13 @@ const filter = ref>({}); // 当前操作的成就 const currentAchievement = ref({}); +// 封面绑定(FileUpload cover v-model) +const iconCover = ref(''); +// 简单判断是否为文件ID(与 iconUtils 保持一致) +function isFileId(val?: string) { + if (!val) return false; + return !val.includes('/') && !val.includes('.') && /^(?=.{16,}$)[A-Za-z0-9_-]+$/.test(val); +} // 枚举选项 const achievementTypeOptions = AchievementEnumHelper.getAllAchievementTypeOptions(); @@ -362,7 +352,7 @@ const achievementFormRules = { { required: true, message: '请输入成就描述', trigger: 'blur' } ], icon: [ - { required: true, message: '请输入图标URL', trigger: 'blur' } + { required: true, message: '请上传成就图标', trigger: 'change' } ], type: [ { required: true, message: '请选择成就类型', trigger: 'change' } @@ -405,6 +395,19 @@ function formatConditionValue(conditionType?: number, conditionValue?: number): // 获取图标完整路径 const getIconUrl = getAchievementIconUrl; +// 上传回调 - 将返回的文件URL写入 currentAchievement.icon +function onIconUploadSuccess(files: any[]) { + const f = files && files[0]; + if (!f) return; + // 优先存 fileID,便于环境切换;回退为 fileUrl + if (f.fileID) { + currentAchievement.value.icon = f.fileID; + iconCover.value = f.fileID; + } else if (f.fileUrl) { + currentAchievement.value.icon = f.fileUrl; + } +} + // 加载成就列表 async function loadAchievementList() { try { @@ -435,6 +438,7 @@ function handleAdd() { orderNum: 0 }; achievementDialogVisible.value = true; + iconCover.value = ''; } // 编辑成就 @@ -442,6 +446,7 @@ function handleEdit(row: Achievement) { isEdit.value = true; currentAchievement.value = { ...row }; achievementDialogVisible.value = true; + iconCover.value = isFileId(currentAchievement.value.icon as any) ? (currentAchievement.value.icon as any) : ''; } // 删除成就 diff --git a/schoolNewsWeb/src/views/user/user-center/MyAchievementsView.vue b/schoolNewsWeb/src/views/user/user-center/MyAchievementsView.vue index eb11755..9ce6279 100644 --- a/schoolNewsWeb/src/views/user/user-center/MyAchievementsView.vue +++ b/schoolNewsWeb/src/views/user/user-center/MyAchievementsView.vue @@ -2,7 +2,7 @@
-

我的等级

+

我的等级

@@ -27,7 +27,7 @@
-

我的勋章

+

我的勋章

@@ -65,63 +65,30 @@ import { UserCenterLayout } from '@/views/user/user-center'; // 响应式数据 const loading = ref(false); -const achievements = ref([]); -const selectedType = ref(undefined); -const showOnlyEarned = ref(false); +const currentLevelAch = ref(undefined); +const nextLevelAch = ref(undefined); +const badgeAchievements = ref([]); +const showOnlyEarned = ref(true); // 枚举选项 const achievementTypeOptions = AchievementEnumHelper.getAllAchievementTypeOptions(); -// 进度条颜色 +// 进度条颜色(按“完成度”配色:低完成度=红,高完成度=绿) const progressColor = [ { color: '#f56c6c', percentage: 30 }, { color: '#e6a23c', percentage: 60 }, { color: '#5cb87a', percentage: 100 } ]; -// 已获得数量 +// 已获得数量(后端只返回已获得的勋章) const earnedCount = computed(() => { - return achievements.value.filter(a => a.obtained).length; + return badgeAchievements.value.length; }); -// 总数量 -const totalCount = computed(() => { - return achievements.value.length; -}); - -// 完成率 -const completionRate = computed(() => { - if (totalCount.value === 0) return 0; - return Math.round((earnedCount.value / totalCount.value) * 100); -}); - -// 等级类成就 -const levelAchievements = computed(() => { - return achievements.value.filter(a => a.type === AchievementType.LEVEL); -}); - -// 当前等级成就:优先已获得的最高等级,否则取最低等级 -const currentLevelAchievement = computed(() => { - if (levelAchievements.value.length === 0) return undefined; - const obtainedLevels = levelAchievements.value - .filter(a => a.obtained) - .sort((a, b) => (b.level || 0) - (a.level || 0)); - - if (obtainedLevels.length > 0) { - return obtainedLevels[0]; - } - - // 没有已获得等级时,取等级最低的作为当前目标 - return [...levelAchievements.value].sort((a, b) => (a.level || 0) - (b.level || 0))[0]; -}); - -// 下一级等级成就:当前等级的下一个等级 -const nextLevelAchievement = computed(() => { - if (!currentLevelAchievement.value) return undefined; - const currentLevel = currentLevelAchievement.value.level || 1; - const sortedLevels = [...levelAchievements.value].sort((a, b) => (a.level || 0) - (b.level || 0)); - return sortedLevels.find(a => (a.level || 0) > currentLevel); -}); +// ===== 等级成就部分 ===== +// 后端已返回计算好的当前和下一等级,直接使用 +const currentLevelAchievement = computed(() => currentLevelAch.value); +const nextLevelAchievement = computed(() => nextLevelAch.value); // 当前等级数字与图标 const currentLevel = computed(() => currentLevelAchievement.value?.level ?? 1); @@ -135,9 +102,12 @@ const nextLevelDisplay = computed(() => { return nextLevelAchievement.value ? `Lv.${formatLevelNumber(nextLevelAchievement.value.level)}` : '满级'; }); const nextProgress = computed(() => { - if (!nextLevelAchievement.value) return 100; + // 使用“完成度”:0 -> 100 + if (!nextLevelAchievement.value) return 100; // 无下一级视为已满级 const p = nextLevelAchievement.value.progressPercentage; - if (typeof p === 'number') return Math.max(0, Math.min(100, p)); + if (typeof p === 'number') { + return Math.max(0, Math.min(100, p)); + } const cur = nextLevelAchievement.value.currentValue || 0; const tar = nextLevelAchievement.value.targetValue || 0; if (tar <= 0) return 0; @@ -148,28 +118,17 @@ const nextDeltaText = computed(() => { const cur = nextLevelAchievement.value.currentValue || 0; const tar = nextLevelAchievement.value.targetValue || 0; const need = Math.max(0, tar - cur); - if (need <= 0) return '即将升级'; - return `距离下级:还差 ${formatConditionValue(nextLevelAchievement.value.conditionType, need)}`; + const nextName = nextLevelAchievement.value.name || `Lv.${formatLevelNumber(nextLevelAchievement.value.level)}`; + if (need <= 0) return `即将升级至 ${nextName}`; + return `下一级:${nextName},还差 ${formatConditionValue(nextLevelAchievement.value.conditionType, need)}`; }); -// 勋章类成就 -const badgeAchievements = computed(() => { - return achievements.value.filter(a => a.type === AchievementType.BADGE); -}); +// ===== 勋章成就部分 ===== -// 右侧展示用的勋章成就(支持仅显示已获得) +// 右侧展示用的勋章成就(后端已返回已获得的,前端只需排序) const filteredBadgeAchievements = computed(() => { - let result = badgeAchievements.value; - - if (showOnlyEarned.value) { - result = result.filter(a => a.obtained); - } - - // 排序:已获得的在前,按等级/ID 排序 - return result.sort((a, b) => { - if (a.obtained !== b.obtained) { - return a.obtained ? -1 : 1; - } + // 后端已返回已获得的勋章,按等级/ID 排序即可 + return [...badgeAchievements.value].sort((a, b) => { return (a.level || 0) - (b.level || 0); }); }); @@ -193,8 +152,9 @@ function formatLevelNumber(level?: number | null): string { if (Number.isNaN(num)) { return String(level); } - // 保留两位小数,方便展示类似 Lv.1.20 的效果 - return num.toFixed(2); + // 保留最多两位小数,但去掉多余的尾随 0(例如 1.2 而不是 1.20) + const fixed = num.toFixed(2); + return String(parseFloat(fixed)); } // 格式化日期 @@ -216,15 +176,39 @@ function filterAchievements() { // 触发计算属性重新计算 } -// 加载成就数据 +// 加载等级状态(当前等级 + 下一等级) +async function loadLevelStatus() { + try { + const result = await achievementApi.getMyLevelStatus(); + if (result.success && result.data) { + currentLevelAch.value = result.data.current || undefined; + nextLevelAch.value = result.data.next || undefined; + } + } catch (error) { + console.error('加载等级状态失败:', error); + ElMessage.error('加载等级状态失败'); + } +} + +// 加载已获得的勋章成就 +async function loadObtainedBadges() { + try { + const result = await achievementApi.getMyObtainedAchievements(AchievementType.BADGE); + badgeAchievements.value = result.dataList || []; + } catch (error) { + console.error('加载勋章数据失败:', error); + ElMessage.error('加载勋章数据失败'); + } +} + +// 加载所有成就数据 async function loadAchievements() { try { loading.value = true; - const result = await achievementApi.getMyAchievements(); - achievements.value = result.dataList || []; - } catch (error) { - console.error('加载成就数据失败:', error); - ElMessage.error('加载成就数据失败'); + await Promise.all([ + loadLevelStatus(), + loadObtainedBadges() + ]); } finally { loading.value = false; } @@ -271,6 +255,9 @@ onMounted(() => { .level-word{ height: 28px; } .next-tip{ color: #64748b; font-size: 13px; } .level-range{ width: 100%; display: flex; justify-content: space-between; font-size: 12px; color: #64748b; } + .el-progress{ width: 100%; } + :deep(.el-progress-bar__outer){ background-color: #e6effa; } + :deep(.el-progress-bar__inner){ background-color: #5cb87a; } } } diff --git a/schoolNewsWeb/src/views/user/user-center/UserCenterLayout.vue b/schoolNewsWeb/src/views/user/user-center/UserCenterLayout.vue index f181e94..115c497 100644 --- a/schoolNewsWeb/src/views/user/user-center/UserCenterLayout.vue +++ b/schoolNewsWeb/src/views/user/user-center/UserCenterLayout.vue @@ -86,5 +86,6 @@ const menus = computed(() => { background: white; border-radius: 8px; padding: 20px; + min-height: 60vh; }