fix(mobile): 修复全站排查发现的移动端布局问题

全站排查各页面移动端表现,修复以下确认会溢出/挤压的布局:

- 分组用户倍率/RPM 上限弹窗:7 列宽表滚动容器补横向滚动(min-w-max)
- 设置页联盟自定义用户表:容器 overflow-hidden 改为 overflow-x-auto
- 模型/分组/端点分布、消费排行、用户仪表盘 5 处甜甜圈图例表:
  移动端纵向堆叠(flex-col sm:flex-row),图例表全宽并支持横向滚动
- 安装向导:步骤指示器移动端只显示序号圆点(hidden sm:inline)避免
  硬溢出,表单两列栅格移动端折叠为单列
- 设置页:支付商品名三列、Beta 策略与帮助图片两列输入行移动端折叠
- 分组创建/编辑弹窗:三个 time 输入的三列行移动端折叠(时间控件有
  固有宽度,窄屏弹窗内会硬溢出)
- 密钥页分组选择浮层:min-w-[380px] 改为视口限宽 + JS 夹取 left,
  对齐 AccountGroupsCell 的定位收敛做法

桌面端布局均保持不变。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
J606y
2026-07-20 22:10:19 +08:00
co-authored by Claude Fable 5
parent 0df490961b
commit 64ab08e4aa
10 changed files with 38 additions and 35 deletions
@@ -102,8 +102,8 @@
<div v-else>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-dark-600">
<div class="max-h-[420px] overflow-y-auto">
<table class="w-full text-sm">
<div class="max-h-[420px] overflow-auto">
<table class="w-full min-w-max text-sm">
<thead class="sticky top-0 z-[1]">
<tr class="border-b border-gray-200 bg-gray-50 dark:border-dark-600 dark:bg-dark-700">
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('admin.groups.columns.userEmail') }}</th>
@@ -126,8 +126,8 @@
<div v-else>
<!-- 表格 -->
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-dark-600">
<div class="max-h-[420px] overflow-y-auto">
<table class="w-full text-sm">
<div class="max-h-[420px] overflow-auto">
<table class="w-full min-w-max text-sm">
<thead class="sticky top-0 z-[1]">
<tr class="border-b border-gray-200 bg-gray-50 dark:border-dark-600 dark:bg-dark-700">
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('admin.groups.columns.userEmail') }}</th>
@@ -71,11 +71,11 @@
<div v-if="loading" class="flex h-48 items-center justify-center">
<LoadingSpinner />
</div>
<div v-else-if="displayEndpointStats.length > 0 && chartData" class="flex items-center gap-6">
<div class="h-48 w-48">
<div v-else-if="displayEndpointStats.length > 0 && chartData" class="flex flex-col items-center gap-4 sm:flex-row sm:gap-6">
<div class="h-48 w-48 shrink-0">
<Doughnut :data="chartData" :options="doughnutOptions" />
</div>
<div class="max-h-48 flex-1 overflow-y-auto">
<div class="max-h-48 w-full min-w-0 flex-1 overflow-auto">
<table class="w-full text-xs">
<thead>
<tr class="text-gray-500 dark:text-gray-400">
@@ -33,11 +33,11 @@
<div v-if="loading" class="flex h-48 items-center justify-center">
<LoadingSpinner />
</div>
<div v-else-if="displayGroupStats.length > 0 && chartData" class="flex items-center gap-6">
<div class="h-48 w-48">
<div v-else-if="displayGroupStats.length > 0 && chartData" class="flex flex-col items-center gap-4 sm:flex-row sm:gap-6">
<div class="h-48 w-48 shrink-0">
<Doughnut :data="chartData" :options="doughnutOptions" />
</div>
<div class="max-h-48 flex-1 overflow-y-auto">
<div class="max-h-48 w-full min-w-0 flex-1 overflow-auto">
<table class="w-full text-xs">
<thead>
<tr class="text-gray-500 dark:text-gray-400">
@@ -101,12 +101,12 @@
</div>
<div
v-else-if="activeView === 'model_distribution' && displayModelStats.length > 0 && chartData"
class="flex items-center gap-6"
class="flex flex-col items-center gap-4 sm:flex-row sm:gap-6"
>
<div class="h-48 w-48">
<div class="h-48 w-48 shrink-0">
<Doughnut :data="chartData" :options="doughnutOptions" />
</div>
<div class="max-h-48 flex-1 overflow-y-auto">
<div class="max-h-48 w-full min-w-0 flex-1 overflow-auto">
<table class="w-full text-xs">
<thead>
<tr class="text-gray-500 dark:text-gray-400">
@@ -182,11 +182,11 @@
>
{{ t('admin.dashboard.failedToLoad') }}
</div>
<div v-else-if="rankingDisplayItems.length > 0 && rankingChartData" class="flex items-center gap-6">
<div class="h-48 w-48">
<div v-else-if="rankingDisplayItems.length > 0 && rankingChartData" class="flex flex-col items-center gap-4 sm:flex-row sm:gap-6">
<div class="h-48 w-48 shrink-0">
<Doughnut :data="rankingChartData" :options="rankingDoughnutOptions" />
</div>
<div class="max-h-48 flex-1 overflow-y-auto">
<div class="max-h-48 w-full min-w-0 flex-1 overflow-auto">
<table class="w-full text-xs">
<thead>
<tr class="text-gray-500 dark:text-gray-400">
@@ -27,12 +27,12 @@
<LoadingSpinner size="md" />
</div>
<h3 class="mb-4 text-sm font-semibold text-gray-900 dark:text-white">{{ t('dashboard.modelDistribution') }}</h3>
<div class="flex items-center gap-6">
<div class="h-48 w-48">
<div class="flex flex-col items-center gap-4 sm:flex-row sm:gap-6">
<div class="h-48 w-48 shrink-0">
<Doughnut v-if="modelData" :data="modelData" :options="doughnutOptions" />
<div v-else class="flex h-full items-center justify-center text-sm text-gray-500 dark:text-gray-400">{{ t('dashboard.noDataAvailable') }}</div>
</div>
<div class="max-h-48 flex-1 overflow-y-auto">
<div class="max-h-48 w-full min-w-0 flex-1 overflow-auto">
<table class="w-full text-xs">
<thead>
<tr class="text-gray-500 dark:text-gray-400">
+2 -2
View File
@@ -1101,7 +1101,7 @@
</div>
<div
v-if="createForm.peak_rate_enabled"
class="mb-4 grid grid-cols-3 gap-3"
class="mb-4 grid grid-cols-1 gap-3 sm:grid-cols-3"
>
<div>
<label class="input-label">{{ t("admin.groups.peakRate.peakStart") }}</label>
@@ -2615,7 +2615,7 @@
</div>
<div
v-if="editForm.peak_rate_enabled"
class="mb-4 grid grid-cols-3 gap-3"
class="mb-4 grid grid-cols-1 gap-3 sm:grid-cols-3"
>
<div>
<label class="input-label">{{ t("admin.groups.peakRate.peakStart") }}</label>
+4 -4
View File
@@ -834,7 +834,7 @@
</span>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<!-- Action -->
<div>
<label
@@ -6344,7 +6344,7 @@
</button>
</div>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-dark-700">
<div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-dark-700">
<table class="min-w-full divide-y divide-gray-200 dark:divide-dark-700">
<thead class="bg-gray-50 dark:bg-dark-800">
<tr>
@@ -6664,7 +6664,7 @@
</div>
<template v-if="form.payment_enabled">
<!-- Row 1: Product name -->
<div class="grid grid-cols-3 gap-3">
<div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
<div>
<label class="input-label">{{
t("admin.settings.payment.productNamePrefix")
@@ -7083,7 +7083,7 @@
</p>
</div>
<!-- Row 5: Help image + text -->
<div class="grid grid-cols-2 gap-3">
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div>
<label class="input-label">{{
t("admin.settings.payment.helpImage")
+7 -7
View File
@@ -38,7 +38,7 @@
<span v-else>{{ index + 1 }}</span>
</div>
<span
class="ml-2 text-sm font-medium"
class="ml-2 hidden text-sm font-medium sm:inline"
:class="
currentStep >= index
? 'text-gray-900 dark:text-white'
@@ -50,7 +50,7 @@
</div>
<div
v-if="index < steps.length - 1"
class="mx-3 h-0.5 w-12"
class="mx-2 h-0.5 w-6 sm:mx-3 sm:w-12"
:class="currentStep > index ? 'bg-primary-500' : 'bg-gray-200 dark:bg-dark-700'"
></div>
</template>
@@ -70,7 +70,7 @@
</p>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label class="input-label">{{ t('setup.database.host') }}</label>
<input
@@ -103,7 +103,7 @@
<Toggle v-model="formData.redis.enable_tls" />
</div>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label class="input-label">{{ t('setup.database.username') }}</label>
<input
@@ -124,7 +124,7 @@
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label class="input-label">{{ t('setup.database.databaseName') }}</label>
<input
@@ -195,7 +195,7 @@
</p>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label class="input-label">{{ t('setup.redis.host') }}</label>
<input
@@ -216,7 +216,7 @@
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label class="input-label">{{ t('setup.redis.password') }}</label>
<input
+6 -3
View File
@@ -1050,7 +1050,7 @@
<div
v-if="groupSelectorKeyId !== null && dropdownPosition"
ref="dropdownRef"
class="animate-in fade-in slide-in-from-top-2 fixed z-[100000020] w-max min-w-[380px] overflow-hidden rounded-xl bg-white shadow-lg ring-1 ring-black/5 duration-200 dark:bg-dark-800 dark:ring-white/10"
class="animate-in fade-in slide-in-from-top-2 fixed z-[100000020] w-max max-w-[calc(100vw-16px)] overflow-hidden rounded-xl bg-white shadow-lg ring-1 ring-black/5 duration-200 sm:min-w-[380px] dark:bg-dark-800 dark:ring-white/10"
style="pointer-events: auto !important;"
:style="{
top: dropdownPosition.top !== undefined ? dropdownPosition.top + 'px' : undefined,
@@ -1605,20 +1605,23 @@ const openGroupSelector = (key: ApiKey) => {
if (buttonEl) {
const rect = buttonEl.getBoundingClientRect()
const dropdownEstHeight = 400 // estimated max dropdown height
const dropdownEstWidth = Math.min(380, window.innerWidth - 16)
const spaceBelow = window.innerHeight - rect.bottom
const spaceAbove = rect.top
// 夹取 left,避免窄屏下浮层超出视口右缘
const left = Math.max(8, Math.min(rect.left, window.innerWidth - dropdownEstWidth - 8))
if (spaceBelow < dropdownEstHeight && spaceAbove > spaceBelow) {
// Not enough space below, pop upward
dropdownPosition.value = {
bottom: window.innerHeight - rect.top + 4,
left: rect.left
left
}
} else {
// Default: pop downward
dropdownPosition.value = {
top: rect.bottom + 4,
left: rect.left
left
}
}
}