Files
sub2api/frontend/src/components/layout/TablePageLayout.vue
erio 6344fa2a86 feat(antigravity): add 403 forbidden status detection, classification and display
Backend:
- Detect and classify 403 responses into three types:
  validation (account needs Google verification),
  violation (terms of service / banned),
  forbidden (generic 403)
- Extract verification/appeal URLs from 403 response body
  (structured JSON parsing with regex fallback)
- Add needs_verify, is_banned, needs_reauth, error_code fields
  to UsageInfo (omitempty for zero impact on other platforms)
- Handle 403 in request path: classify and permanently set account error
- Save validation_url in error_message for degraded path recovery
- Enrich usage with account error on both success and degraded paths
- Add singleflight dedup for usage requests with independent context
- Differentiate cache TTL: success/403 → 3min, errors → 1min
- Return degraded UsageInfo instead of HTTP 500 on quota fetch errors

Frontend:
- Display forbidden status badges with color coding (red for banned,
  amber for needs verification, gray for generic)
- Show clickable verification/appeal URL links
- Display needs_reauth and degraded error states in usage cell
- Add Antigravity tier label badge next to platform type

Tests:
- Comprehensive unit tests for classifyForbiddenType (7 cases)
- Unit tests for extractValidationURL (8 cases including unicode escapes)
- Integration test for FetchQuota forbidden path
2026-03-13 18:22:45 +08:00

113 lines
3.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="table-page-layout" :class="{ 'mobile-mode': isMobile }">
<!-- 固定区域操作按钮 -->
<div v-if="$slots.actions" class="layout-section-fixed">
<slot name="actions" />
</div>
<!-- 固定区域搜索和过滤器 -->
<div v-if="$slots.filters" class="layout-section-fixed">
<slot name="filters" />
</div>
<!-- 滚动区域表格 -->
<div class="layout-section-scrollable">
<div class="card table-scroll-container">
<slot name="table" />
</div>
</div>
<!-- 固定区域分页器 -->
<div v-if="$slots.pagination" class="layout-section-fixed">
<slot name="pagination" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
const isMobile = ref(false)
const checkMobile = () => {
isMobile.value = window.innerWidth < 1024
}
onMounted(() => {
checkMobile()
window.addEventListener('resize', checkMobile)
})
onUnmounted(() => {
window.removeEventListener('resize', checkMobile)
})
</script>
<style scoped>
/* 桌面端Flexbox 布局 */
.table-page-layout {
@apply flex flex-col gap-6;
height: calc(100vh - 64px - 4rem); /* 减去 header + lg:p-8 的上下padding */
}
.layout-section-fixed {
@apply flex-shrink-0;
}
.layout-section-scrollable {
@apply flex-1 min-h-0 flex flex-col;
}
/* 表格滚动容器 - 增强版表体滚动方案 */
.table-scroll-container {
@apply flex flex-col overflow-hidden h-full bg-white dark:bg-dark-800 rounded-2xl border border-gray-200 dark:border-dark-700 shadow-sm;
}
.table-scroll-container :deep(.table-wrapper) {
@apply flex-1 overflow-x-auto overflow-y-auto;
/* 确保横向滚动条显示在最底部 */
scrollbar-gutter: stable;
}
.table-scroll-container :deep(table) {
@apply w-full;
min-width: max-content; /* 关键:确保表格宽度根据内容撑开,从而触发横向滚动 */
display: table; /* 使用标准 table 布局以支持 sticky 列 */
}
.table-scroll-container :deep(thead) {
@apply bg-gray-50/80 dark:bg-dark-800/80 backdrop-blur-sm;
}
.table-scroll-container :deep(tbody) {
/* 保持默认 table-row-group 显示,不使用 block */
}
.table-scroll-container :deep(th) {
@apply px-5 py-4 text-left text-sm font-medium text-gray-600 dark:text-dark-300 border-b border-gray-200 dark:border-dark-700;
}
.table-scroll-container :deep(td) {
@apply px-5 py-4 text-sm text-gray-700 dark:text-gray-300 border-b border-gray-100 dark:border-dark-800;
}
/* 移动端:恢复正常滚动 */
.table-page-layout.mobile-mode .table-scroll-container {
@apply h-auto overflow-visible border-none shadow-none bg-transparent;
}
.table-page-layout.mobile-mode .layout-section-scrollable {
@apply flex-none min-h-fit;
}
.table-page-layout.mobile-mode .table-scroll-container :deep(.table-wrapper) {
@apply overflow-visible;
}
.table-page-layout.mobile-mode .table-scroll-container :deep(table) {
@apply flex-none;
display: table;
min-width: 100%;
}
</style>