feat: 优化移动端响应式设计

- 优化所有页面的移动端适配(手机、平板、PC)
- 修复AccountsView移动端状态显示和按钮功能问题
- 修复ApiKeysView移动端详情展开显示问题
- 移除ApiKeysView不必要的查看按钮
- 修复Dashboard页面PC版时间筛选按钮布局
- 改进所有组件的响应式设计
- 删除dist目录避免构建文件冲突

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-08-03 01:09:26 +08:00
parent f22c8cbfcc
commit ecfc1050d3
23 changed files with 2775 additions and 697 deletions

View File

@@ -0,0 +1,324 @@
<template>
<Teleport to="body">
<div class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
<div class="modal-content w-full max-w-2xl p-8 mx-auto max-h-[90vh] overflow-y-auto custom-scrollbar">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
<i class="fas fa-layer-group text-white text-lg" />
</div>
<div>
<h3 class="text-xl font-bold text-gray-900">
批量创建成功
</h3>
<p class="text-sm text-gray-600">
成功创建 {{ apiKeys.length }} API Key
</p>
</div>
</div>
<button
class="text-gray-400 hover:text-gray-600 transition-colors"
title="直接关闭(不推荐)"
@click="handleDirectClose"
>
<i class="fas fa-times text-xl" />
</button>
</div>
<!-- 警告提示 -->
<div class="bg-amber-50 border-l-4 border-amber-400 p-4 mb-6">
<div class="flex items-start">
<div class="w-6 h-6 bg-amber-400 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fas fa-exclamation-triangle text-white text-sm" />
</div>
<div class="ml-3">
<h5 class="font-semibold text-amber-900 mb-1">
重要提醒
</h5>
<p class="text-sm text-amber-800">
这是您唯一能看到所有 API Key 的机会关闭此窗口后系统将不再显示完整的 API Key请立即下载并妥善保存
</p>
</div>
</div>
</div>
<!-- 统计信息 -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg p-4 border border-blue-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs text-blue-600 font-medium">
创建数量
</p>
<p class="text-2xl font-bold text-blue-900 mt-1">
{{ apiKeys.length }}
</p>
</div>
<div class="w-10 h-10 bg-blue-500 bg-opacity-20 rounded-lg flex items-center justify-center">
<i class="fas fa-key text-blue-600" />
</div>
</div>
</div>
<div class="bg-gradient-to-br from-green-50 to-green-100 rounded-lg p-4 border border-green-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs text-green-600 font-medium">
基础名称
</p>
<p class="text-lg font-bold text-green-900 mt-1 truncate">
{{ baseName }}
</p>
</div>
<div class="w-10 h-10 bg-green-500 bg-opacity-20 rounded-lg flex items-center justify-center">
<i class="fas fa-tag text-green-600" />
</div>
</div>
</div>
<div class="bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg p-4 border border-purple-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs text-purple-600 font-medium">
权限范围
</p>
<p class="text-lg font-bold text-purple-900 mt-1">
{{ getPermissionText() }}
</p>
</div>
<div class="w-10 h-10 bg-purple-500 bg-opacity-20 rounded-lg flex items-center justify-center">
<i class="fas fa-shield-alt text-purple-600" />
</div>
</div>
</div>
<div class="bg-gradient-to-br from-orange-50 to-orange-100 rounded-lg p-4 border border-orange-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs text-orange-600 font-medium">
过期时间
</p>
<p class="text-lg font-bold text-orange-900 mt-1">
{{ getExpiryText() }}
</p>
</div>
<div class="w-10 h-10 bg-orange-500 bg-opacity-20 rounded-lg flex items-center justify-center">
<i class="fas fa-clock text-orange-600" />
</div>
</div>
</div>
</div>
<!-- API Keys 预览 -->
<div class="mb-6">
<div class="flex items-center justify-between mb-3">
<label class="text-sm font-semibold text-gray-700">API Keys 预览</label>
<div class="flex items-center gap-2">
<button
type="button"
class="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1"
@click="togglePreview"
>
<i :class="['fas', showPreview ? 'fa-eye-slash' : 'fa-eye']" />
{{ showPreview ? '隐藏' : '显示' }}预览
</button>
<span class="text-xs text-gray-500">最多显示前10个</span>
</div>
</div>
<div
v-if="showPreview"
class="bg-gray-900 rounded-lg p-4 max-h-48 overflow-y-auto custom-scrollbar"
>
<pre class="text-xs text-gray-300 font-mono">{{ getPreviewText() }}</pre>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex gap-3">
<button
class="flex-1 btn btn-primary py-3 px-6 font-semibold flex items-center justify-center gap-2"
@click="downloadApiKeys"
>
<i class="fas fa-download" />
下载所有 API Keys
</button>
<button
class="px-6 py-3 bg-gray-200 text-gray-800 rounded-xl font-semibold hover:bg-gray-300 transition-colors border border-gray-300"
@click="handleClose"
>
我已保存
</button>
</div>
<!-- 额外提示 -->
<div class="mt-4 p-3 bg-blue-50 rounded-lg border border-blue-200">
<p class="text-xs text-blue-700 flex items-start">
<i class="fas fa-info-circle mr-2 mt-0.5 flex-shrink-0" />
<span>
下载的文件格式为文本文件.txt每行包含一个 API Key
请将文件保存在安全的位置避免泄露
</span>
</p>
</div>
</div>
</div>
</Teleport>
</template>
<script setup>
import { ref, computed } from 'vue'
import { showToast } from '@/utils/toast'
const props = defineProps({
apiKeys: {
type: Array,
required: true
}
})
const emit = defineEmits(['close'])
const showPreview = ref(false)
// 获取基础名称
const baseName = computed(() => {
if (props.apiKeys.length > 0) {
const firstKey = props.apiKeys[0]
// 提取基础名称(去掉 _1, _2 等后缀)
const match = firstKey.name.match(/^(.+)_\d+$/)
return match ? match[1] : firstKey.name
}
return ''
})
// 获取权限文本
const getPermissionText = () => {
if (props.apiKeys.length === 0) return '未知'
const permissions = props.apiKeys[0].permissions
const permissionMap = {
'all': '全部服务',
'claude': '仅 Claude',
'gemini': '仅 Gemini'
}
return permissionMap[permissions] || permissions
}
// 获取过期时间文本
const getExpiryText = () => {
if (props.apiKeys.length === 0) return '未知'
const expiresAt = props.apiKeys[0].expiresAt
if (!expiresAt) return '永不过期'
const expiryDate = new Date(expiresAt)
const now = new Date()
const diffDays = Math.ceil((expiryDate - now) / (1000 * 60 * 60 * 24))
if (diffDays <= 7) return `${diffDays}`
if (diffDays <= 30) return `${Math.ceil(diffDays / 7)}`
if (diffDays <= 365) return `${Math.ceil(diffDays / 30)}个月`
return `${Math.ceil(diffDays / 365)}`
}
// 切换预览显示
const togglePreview = () => {
showPreview.value = !showPreview.value
}
// 获取预览文本
const getPreviewText = () => {
const previewKeys = props.apiKeys.slice(0, 10)
const lines = previewKeys.map((key, index) => {
return `${key.name}: ${key.apiKey || key.key || ''}`
})
if (props.apiKeys.length > 10) {
lines.push(`... 还有 ${props.apiKeys.length - 10} 个 API Key`)
}
return lines.join('\n')
}
// 下载 API Keys
const downloadApiKeys = () => {
// 生成文件内容
const content = props.apiKeys.map(key => {
return `${key.name}: ${key.apiKey || key.key || ''}`
}).join('\n')
// 创建 Blob 对象
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' })
// 创建下载链接
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
// 生成文件名(包含时间戳)
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)
link.download = `api-keys-${baseName.value}-${timestamp}.txt`
// 触发下载
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
// 释放 URL 对象
URL.revokeObjectURL(url)
showToast('API Keys 文件已下载', 'success')
}
// 关闭弹窗(带确认)
const handleClose = async () => {
if (window.showConfirm) {
const confirmed = await window.showConfirm(
'关闭提醒',
'关闭后将无法再次查看这些 API Key请确保已经下载并妥善保存。\n\n确定要关闭吗',
'确定关闭',
'返回下载'
)
if (confirmed) {
emit('close')
}
} else {
// 降级方案
const confirmed = confirm(
'关闭后将无法再次查看这些 API Key请确保已经下载并妥善保存。\n\n确定要关闭吗'
)
if (confirmed) {
emit('close')
}
}
}
// 直接关闭(不带确认)
const handleDirectClose = async () => {
if (window.showConfirm) {
const confirmed = await window.showConfirm(
'确定要关闭吗?',
'您还没有下载 API Keys关闭后将无法再次查看。\n\n强烈建议您先下载保存。',
'仍然关闭',
'返回下载'
)
if (confirmed) {
emit('close')
}
} else {
// 降级方案
const confirmed = confirm(
'您还没有下载 API Keys关闭后将无法再次查看。\n\n确定要关闭吗'
)
if (confirmed) {
emit('close')
}
}
}
</script>
<style scoped>
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
</style>