fix(看板娘): 把看板娘拖到右上角时,成就弹框会压在她头上,现在弹框让到她左边
Deploy / deploy (push) Successful in 1m5s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-11-03 16:52:00 +08:00
1 parent cccf7a37be
commit f6f7a0c3af
3 files changed
+37 -3

No files matched your search

@@ -1,11 +1,32 @@
<script setup lang="ts"> <script setup lang="ts">
import AchievementIcon from 'shared/components/AchievementIcon.vue' import AchievementIcon from 'shared/components/AchievementIcon.vue'
import { mascotBox } from 'shared/mascot/guide'
import { useAchievementStore } from 'shared/store/achievement' import { useAchievementStore } from 'shared/store/achievement'
import { RARITY_COLOR } from 'utils/constants' import { RARITY_COLOR } from 'utils/constants'
const store = useAchievementStore() const store = useAchievementStore()
const { current, queue } = storeToRefs(store) const { current, queue } = storeToRefs(store)
const visible = ref(false) const visible = ref(false)
const toastEl = ref<HTMLElement>()
const RIGHT = 24
const TOP = 72
const GAP = 12
/**
* 默认在右上角。看板娘被拖到和它重叠的地方(多半是右上角)时,挪到她左边 ——
* 补发成就时一次会连着弹好几个,一直压在她头上不好看。尺寸按弹出来以后量到的算,
* 还没量到时按最小宽度和大致高度估。
*/
const placement = computed(() => {
if (!mascotBox.shown || !visible.value) return {}
const vw = document.documentElement.clientWidth
const width = toastEl.value?.offsetWidth ?? 300
const height = toastEl.value?.offsetHeight ?? 90
const left = vw - RIGHT - width
const overlaps = mascotBox.x < vw - RIGHT && mascotBox.x + mascotBox.width > left && mascotBox.y < TOP + height && mascotBox.y + mascotBox.height > TOP
return overlaps ? { right: `${vw - mascotBox.x + GAP}px` } : {}
})
let timer: ReturnType<typeof setTimeout> | null = null let timer: ReturnType<typeof setTimeout> | null = null
let gapTimer: ReturnType<typeof setTimeout> | null = null let gapTimer: ReturnType<typeof setTimeout> | null = null
@@ -37,7 +58,7 @@ onUnmounted(() => {
<template> <template>
<Transition name="slide"> <Transition name="slide">
<div v-if="visible && current" class="toast" :style="{ borderColor: RARITY_COLOR[current.rarity] }"> <div v-if="visible && current" ref="toastEl" class="toast" :style="{ borderColor: RARITY_COLOR[current.rarity], ...placement }">
<div class="icon"> <div class="icon">
<AchievementIcon :icon="current.icon" :size="34" /> <AchievementIcon :icon="current.icon" :size="34" />
</div> </div>
@@ -56,7 +77,7 @@ onUnmounted(() => {
.toast { .toast {
position: fixed; position: fixed;
/* 右上角、顶栏下面。原来在右下角,题目页上正好和结果、点评弹窗叠在一起 */ /* 右上角、顶栏下面。原来在右下角,题目页上正好和结果、点评弹窗叠在一起 */
right: 24px; right: 24px; /* 和脚本里的 RIGHT / TOP 一致 */
top: 72px; top: 72px;
z-index: 3000; z-index: 3000;
display: flex; display: flex;
+6 -1
View File
@@ -23,7 +23,7 @@ import { TIME_ZONE_OFFSET_MINUTES } from '@oj2/contract'
import imageUrl from 'shared/assets/mascot/whale.webp' import imageUrl from 'shared/assets/mascot/whale.webp'
import patUrl from 'shared/assets/mascot/whale-pat.gif' import patUrl from 'shared/assets/mascot/whale-pat.gif'
import { MASCOT_LINES, type MascotSituation } from 'shared/mascot/lines' import { MASCOT_LINES, type MascotSituation } from 'shared/mascot/lines'
import { currentMascotGuide, fillLine, type MascotAction, type MascotGuide } from 'shared/mascot/guide' import { currentMascotGuide, fillLine, mascotBox, type MascotAction, type MascotGuide } from 'shared/mascot/guide'
import { logMascotEvent, notePoke, personalize, refreshMascotProfile, speechRequest } from 'shared/mascot/behavior' import { logMascotEvent, notePoke, personalize, refreshMascotProfile, speechRequest } from 'shared/mascot/behavior'
import { useAuthModalStore } from 'shared/store/authModal' import { useAuthModalStore } from 'shared/store/authModal'
import { useCollabStore } from 'shared/store/collab' import { useCollabStore } from 'shared/store/collab'
@@ -352,10 +352,15 @@ watch(silent, (quiet) => quiet && close())
/** 她靠上时气泡往下冒,免得顶出屏幕;在左半边时气泡贴左对齐 */ /** 她靠上时气泡往下冒,免得顶出屏幕;在左半边时气泡贴左对齐 */
const bubbleBelow = computed(() => pos.y < 220) const bubbleBelow = computed(() => pos.y < 220)
// 告诉别的浮层她在哪(成就弹框会让开)
watchEffect(() => Object.assign(mascotBox, { x: pos.x, y: pos.y, width: WIDTH, height: HEIGHT }))
onMounted(() => { onMounted(() => {
window.addEventListener('resize', settle, { passive: true }) window.addEventListener('resize', settle, { passive: true })
mascotBox.shown = true
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
mascotBox.shown = false
window.removeEventListener('resize', settle) window.removeEventListener('resize', settle)
clearTimeout(patTimer) clearTimeout(patTimer)
clearTimeout(quietTimer) clearTimeout(quietTimer)
+8
View File
@@ -81,3 +81,11 @@ export function randomProblemAction(router: Router): MascotAction {
export function goAction(router: Router, label: string, to: string): MascotAction { export function goAction(router: Router, label: string, to: string): MascotAction {
return { label, run: () => router.push(to) } return { label, run: () => router.push(to) }
} }
// ---- 别的浮层要让开她 ----
/**
* 看板娘现在占的地方(视口坐标,拖动中也跟着变)。不在页面上(手机、没挂载)时 shown 为 false。
* 成就弹框靠它让开:学生把她拖进右上角时,弹框正好压在她头上。
*/
export const mascotBox = reactive({ shown: false, x: 0, y: 0, width: 0, height: 0 })