diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index f408c57..701fcb5 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -49,45 +49,55 @@ jobs: id: build_backend run: | set -o pipefail + ok=0 for attempt in 1 2 3; do echo "Build backend attempt $attempt/3..." DOCKER_BUILDKIT=0 docker build \ --tag ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/video-backend:${{ env.IMAGE_TAG }} \ --tag ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/video-backend:latest \ - ./backend 2>&1 | tee /tmp/build.log && break + ./backend 2>&1 | tee /tmp/build.log && { ok=1; break; } echo "Attempt $attempt failed, retrying in 10s..." && sleep 10 done + [ $ok -eq 1 ] || { echo "ERROR: backend build failed after 3 attempts"; exit 1; } + ok=0 for attempt in 1 2 3; do docker push ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/video-backend:${{ env.IMAGE_TAG }} && \ - docker push ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/video-backend:latest && break + docker push ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/video-backend:latest && { ok=1; break; } echo "Push attempt $attempt failed, retrying in 10s..." && sleep 10 done + [ $ok -eq 1 ] || { echo "ERROR: backend push failed after 3 attempts"; exit 1; } - name: Build and Push Web id: build_web run: | set -o pipefail + ok=0 for attempt in 1 2 3; do echo "Build web attempt $attempt/3..." DOCKER_BUILDKIT=0 docker build \ --tag ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/video-web:${{ env.IMAGE_TAG }} \ --tag ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/video-web:latest \ - ./web 2>&1 | tee -a /tmp/build.log && break + ./web 2>&1 | tee -a /tmp/build.log && { ok=1; break; } echo "Attempt $attempt failed, retrying in 10s..." && sleep 10 done + [ $ok -eq 1 ] || { echo "ERROR: web build failed after 3 attempts"; exit 1; } + ok=0 for attempt in 1 2 3; do docker push ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/video-web:${{ env.IMAGE_TAG }} && \ - docker push ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/video-web:latest && break + docker push ${{ env.CR_SERVER_ACTIVE }}/${{ env.CR_ORG }}/video-web:latest && { ok=1; break; } echo "Push attempt $attempt failed, retrying in 10s..." && sleep 10 done + [ $ok -eq 1 ] || { echo "ERROR: web push failed after 3 attempts"; exit 1; } - name: Setup Kubectl run: | if ! command -v kubectl &>/dev/null; then + ok=0 for attempt in 1 2 3; do - curl -LO "https://files.m.daocloud.io/dl.k8s.io/release/v1.28.0/bin/linux/amd64/kubectl" && break + curl -LO "https://files.m.daocloud.io/dl.k8s.io/release/v1.28.0/bin/linux/amd64/kubectl" && { ok=1; break; } echo "Download attempt $attempt failed, retrying in 5s..." && sleep 5 done + [ $ok -eq 1 ] || { echo "ERROR: kubectl download failed after 3 attempts"; exit 1; } chmod +x kubectl && mv kubectl /usr/bin/kubectl fi kubectl version --client @@ -135,6 +145,7 @@ jobs: # All kubectl operations with retry (K3s 内网连接可能抖动) export KUBECTL_TIMEOUT="--request-timeout=4s" + ok=0 for attempt in 1 2 3 4 5; do echo "Deploy attempt $attempt/5..." { @@ -169,10 +180,11 @@ jobs: kubectl $KUBECTL_TIMEOUT rollout restart deployment/video-backend kubectl $KUBECTL_TIMEOUT rollout restart deployment/celery-worker kubectl $KUBECTL_TIMEOUT rollout restart deployment/video-web - } 2>&1 | tee /tmp/deploy.log && break + } 2>&1 | tee /tmp/deploy.log && { ok=1; break; } echo "Attempt $attempt failed, retrying in 30s..." sleep 30 done + [ $ok -eq 1 ] || { echo "ERROR: deploy to K3s failed after 5 attempts — check /tmp/deploy.log"; exit 1; } # ===== Log Center: failure reporting ===== - name: Report failure to Log Center diff --git a/web/src/components/VideoGenerationPage.module.css b/web/src/components/VideoGenerationPage.module.css index 407a6cb..d72f20f 100644 --- a/web/src/components/VideoGenerationPage.module.css +++ b/web/src/components/VideoGenerationPage.module.css @@ -17,6 +17,10 @@ flex: 1; overflow-y: auto; overflow-x: hidden; + /* 关掉浏览器自动 scroll anchoring:往上加载历史时由 handleScroll 里的 + anchor 逻辑统一管,避免浏览器默认的 anchor 与我们手动 +diff 叠加, + 导致慢速滚动 / 慢拖滑动条时页面被推到最底部。 */ + overflow-anchor: none; } .emptyArea { diff --git a/web/src/components/VideoGenerationPage.tsx b/web/src/components/VideoGenerationPage.tsx index 26f6b24..1fd0cd3 100644 --- a/web/src/components/VideoGenerationPage.tsx +++ b/web/src/components/VideoGenerationPage.tsx @@ -22,6 +22,9 @@ export function VideoGenerationPage() { const scrollRef = useRef(null); const prevLastIdRef = useRef(null); const initialLoadRef = useRef(true); + // 防重入 flag:loadMore + anchor 期间,handleScroll 多次触发不再 schedule 多个 rAF, + // 避免 anchor 累加把页面推到底(慢速滚轮 / 慢拖滑动条场景)。 + const loadMoreInFlightRef = useRef(false); const savedScrollTop = useGenerationStore((s) => s.savedScrollTop); const saveScrollPosition = useGenerationStore((s) => s.saveScrollPosition); const [detailTaskId, setDetailTaskId] = useState(null); @@ -76,15 +79,20 @@ export function VideoGenerationPage() { const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; setShowScrollBottom(distanceFromBottom > 300); - // Trigger loadMore when scrolled within 100px of the top - if (scrollRef.current.scrollTop < 100) { + // Trigger loadMore when scrolled within 100px of the top. + // ref flag 守卫:只有第一次进入分支时才 schedule loadMore + anchor; + // 后续 handleScroll(慢速操作下持续触发)直接跳过,避免多次 rAF 排队累加 +diff。 + if (scrollRef.current.scrollTop < 100 && !loadMoreInFlightRef.current) { + loadMoreInFlightRef.current = true; const el = scrollRef.current; const prevHeight = el.scrollHeight; loadMore().then(() => { - // After older tasks are prepended, restore visual position so user doesn't jump + // After older tasks are prepended, restore visual position so user doesn't jump. + // CSS overflow-anchor: none 已禁用浏览器自动 anchor,由这里独立完成。 requestAnimationFrame(() => { const diff = el.scrollHeight - prevHeight; if (diff > 0) el.scrollTop += diff; + loadMoreInFlightRef.current = false; }); }); }