refactor: standardize code formatting and linting configuration

- Replace .eslintrc.js with .eslintrc.cjs for better ES module compatibility
- Add .prettierrc configuration for consistent code formatting
- Update package.json with new lint and format scripts
- Add nodemon.json for development hot reloading configuration
- Standardize code formatting across all JavaScript and Vue files
- Update web admin SPA with improved linting rules and formatting
- Add prettier configuration to web admin SPA

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
千羽
2025-08-07 18:19:31 +09:00
parent 4a0eba117c
commit 8a74bf5afe
124 changed files with 20878 additions and 18757 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,21 @@
<template>
<div class="min-h-screen gradient-bg p-4 md:p-6">
<div class="gradient-bg min-h-screen p-4 md:p-6">
<!-- 顶部导航 -->
<div class="glass-strong rounded-3xl p-4 md:p-6 mb-6 md:mb-8 shadow-xl">
<div class="flex flex-col md:flex-row justify-between items-center gap-4">
<LogoTitle
<div class="glass-strong mb-6 rounded-3xl p-4 shadow-xl md:mb-8 md:p-6">
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
<LogoTitle
:loading="oemLoading"
:title="oemSettings.siteName"
:subtitle="currentTab === 'stats' ? 'API Key 使用统计' : '使用教程'"
:logo-src="oemSettings.siteIconData || oemSettings.siteIcon"
:subtitle="currentTab === 'stats' ? 'API Key 使用统计' : '使用教程'"
:title="oemSettings.siteName"
/>
<div class="flex items-center gap-3">
<router-link
class="admin-button flex items-center gap-2 rounded-xl px-3 py-2 text-white transition-all duration-300 md:px-4 md:py-2"
to="/dashboard"
class="admin-button rounded-xl px-3 py-2 md:px-4 md:py-2 text-white transition-all duration-300 flex items-center gap-2"
>
<i class="fas fa-cog text-sm" />
<span class="text-xs md:text-sm font-medium">管理后台</span>
<span class="text-xs font-medium md:text-sm">管理后台</span>
</router-link>
</div>
</div>
@@ -24,22 +24,18 @@
<!-- Tab 切换 -->
<div class="mb-6 md:mb-8">
<div class="flex justify-center">
<div class="inline-flex bg-white/10 backdrop-blur-xl rounded-full p-1 shadow-lg border border-white/20 w-full max-w-md md:w-auto">
<div
class="inline-flex w-full max-w-md rounded-full border border-white/20 bg-white/10 p-1 shadow-lg backdrop-blur-xl md:w-auto"
>
<button
:class="[
'tab-pill-button',
currentTab === 'stats' ? 'active' : ''
]"
:class="['tab-pill-button', currentTab === 'stats' ? 'active' : '']"
@click="currentTab = 'stats'"
>
<i class="fas fa-chart-line mr-1 md:mr-2" />
<span class="text-sm md:text-base">统计查询</span>
</button>
<button
:class="[
'tab-pill-button',
currentTab === 'tutorial' ? 'active' : ''
]"
:class="['tab-pill-button', currentTab === 'tutorial' ? 'active' : '']"
@click="currentTab = 'tutorial'"
>
<i class="fas fa-graduation-cap mr-1 md:mr-2" />
@@ -50,50 +46,45 @@
</div>
<!-- 统计内容 -->
<div
v-if="currentTab === 'stats'"
class="tab-content"
>
<div v-if="currentTab === 'stats'" class="tab-content">
<!-- API Key 输入区域 -->
<ApiKeyInput />
<!-- 错误提示 -->
<div
v-if="error"
class="mb-6 md:mb-8"
>
<div class="bg-red-500/20 border border-red-500/30 rounded-xl p-3 md:p-4 text-red-800 backdrop-blur-sm text-sm md:text-base">
<div v-if="error" class="mb-6 md:mb-8">
<div
class="rounded-xl border border-red-500/30 bg-red-500/20 p-3 text-sm text-red-800 backdrop-blur-sm md:p-4 md:text-base"
>
<i class="fas fa-exclamation-triangle mr-2" />
{{ error }}
</div>
</div>
<!-- 统计数据展示区域 -->
<div
v-if="statsData"
class="fade-in"
>
<div class="glass-strong rounded-3xl p-4 md:p-6 shadow-xl">
<div v-if="statsData" class="fade-in">
<div class="glass-strong rounded-3xl p-4 shadow-xl md:p-6">
<!-- 时间范围选择器 -->
<div class="mb-4 md:mb-6 pb-4 md:pb-6 border-b border-gray-200">
<div class="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-4">
<div class="mb-4 border-b border-gray-200 pb-4 md:mb-6 md:pb-6">
<div
class="flex flex-col items-start justify-between gap-3 md:flex-row md:items-center md:gap-4"
>
<div class="flex items-center gap-2 md:gap-3">
<i class="fas fa-clock text-blue-500 text-base md:text-lg" />
<span class="text-base md:text-lg font-medium text-gray-700">统计时间范围</span>
<i class="fas fa-clock text-base text-blue-500 md:text-lg" />
<span class="text-base font-medium text-gray-700 md:text-lg">统计时间范围</span>
</div>
<div class="flex gap-2 w-full md:w-auto">
<button
:class="['period-btn', { 'active': statsPeriod === 'daily' }]"
class="px-4 md:px-6 py-2 text-xs md:text-sm font-medium flex items-center gap-1 md:gap-2 flex-1 md:flex-none justify-center"
<div class="flex w-full gap-2 md:w-auto">
<button
class="flex flex-1 items-center justify-center gap-1 px-4 py-2 text-xs font-medium md:flex-none md:gap-2 md:px-6 md:text-sm"
:class="['period-btn', { active: statsPeriod === 'daily' }]"
:disabled="loading || modelStatsLoading"
@click="switchPeriod('daily')"
>
<i class="fas fa-calendar-day text-xs md:text-sm" />
今日
</button>
<button
:class="['period-btn', { 'active': statsPeriod === 'monthly' }]"
class="px-4 md:px-6 py-2 text-xs md:text-sm font-medium flex items-center gap-1 md:gap-2 flex-1 md:flex-none justify-center"
<button
class="flex flex-1 items-center justify-center gap-1 px-4 py-2 text-xs font-medium md:flex-none md:gap-2 md:px-6 md:text-sm"
:class="['period-btn', { active: statsPeriod === 'monthly' }]"
:disabled="loading || modelStatsLoading"
@click="switchPeriod('monthly')"
>
@@ -108,7 +99,7 @@
<StatsOverview />
<!-- Token 分布和限制配置 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 mb-6 md:mb-8">
<div class="mb-6 grid grid-cols-1 gap-4 md:mb-8 md:gap-6 lg:grid-cols-2">
<TokenDistribution />
<LimitConfig />
</div>
@@ -120,10 +111,7 @@
</div>
<!-- 教程内容 -->
<div
v-if="currentTab === 'tutorial'"
class="tab-content"
>
<div v-if="currentTab === 'tutorial'" class="tab-content">
<div class="glass-strong rounded-3xl shadow-xl">
<TutorialView />
</div>
@@ -162,13 +150,7 @@ const {
oemSettings
} = storeToRefs(apiStatsStore)
const {
queryStats,
switchPeriod,
loadStatsWithApiId,
loadOemSettings,
reset
} = apiStatsStore
const { queryStats, switchPeriod, loadStatsWithApiId, loadOemSettings, reset } = apiStatsStore
// 处理键盘快捷键
const handleKeyDown = (event) => {
@@ -179,7 +161,7 @@ const handleKeyDown = (event) => {
}
event.preventDefault()
}
// ESC 清除数据
if (event.key === 'Escape') {
reset()
@@ -189,15 +171,18 @@ const handleKeyDown = (event) => {
// 初始化
onMounted(() => {
console.log('API Stats Page loaded')
// 加载 OEM 设置
loadOemSettings()
// 检查 URL 参数
const urlApiId = route.query.apiId
const urlApiKey = route.query.apiKey
if (urlApiId && urlApiId.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i)) {
if (
urlApiId &&
urlApiId.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i)
) {
// 如果 URL 中有 apiId直接使用 apiId 加载数据
apiId.value = urlApiId
loadStatsWithApiId()
@@ -205,7 +190,7 @@ onMounted(() => {
// 向后兼容,支持 apiKey 参数
apiKey.value = urlApiKey
}
// 添加键盘事件监听
document.addEventListener('keydown', handleKeyDown)
})
@@ -239,7 +224,7 @@ watch(apiKey, (newValue) => {
left: 0;
right: 0;
bottom: 0;
background:
background:
radial-gradient(circle at 20% 80%, rgba(240, 147, 251, 0.2) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(102, 126, 234, 0.2) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(118, 75, 162, 0.1) 0%, transparent 50%);
@@ -252,7 +237,7 @@ watch(apiKey, (newValue) => {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(25px);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow:
box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.25),
0 0 0 1px rgba(255, 255, 255, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
@@ -275,7 +260,9 @@ watch(apiKey, (newValue) => {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: 1px solid rgba(255, 255, 255, 0.2);
text-decoration: none;
box-shadow: 0 4px 6px -1px rgba(102, 126, 234, 0.3), 0 2px 4px -1px rgba(102, 126, 234, 0.1);
box-shadow:
0 4px 6px -1px rgba(102, 126, 234, 0.3),
0 2px 4px -1px rgba(102, 126, 234, 0.1);
position: relative;
overflow: hidden;
}
@@ -293,7 +280,9 @@ watch(apiKey, (newValue) => {
.admin-button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(102, 126, 234, 0.4), 0 4px 6px -2px rgba(102, 126, 234, 0.15);
box-shadow:
0 10px 15px -3px rgba(102, 126, 234, 0.4),
0 4px 6px -2px rgba(102, 126, 234, 0.15);
}
.admin-button:hover::before {
@@ -315,7 +304,7 @@ watch(apiKey, (newValue) => {
.period-btn.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow:
box-shadow:
0 10px 15px -3px rgba(102, 126, 234, 0.3),
0 4px 6px -2px rgba(102, 126, 234, 0.05);
transform: translateY(-1px);
@@ -365,7 +354,7 @@ watch(apiKey, (newValue) => {
.tab-pill-button.active {
background: white;
color: #764ba2;
box-shadow:
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
@@ -396,13 +385,13 @@ watch(apiKey, (newValue) => {
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(30px);
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,87 +1,74 @@
<template>
<div class="flex items-center justify-center min-h-screen p-4 sm:p-6">
<div class="glass-strong rounded-xl sm:rounded-2xl md:rounded-3xl p-6 sm:p-8 md:p-10 w-full max-w-md shadow-2xl">
<div class="text-center mb-6 sm:mb-8">
<div class="flex min-h-screen items-center justify-center p-4 sm:p-6">
<div
class="glass-strong w-full max-w-md rounded-xl p-6 shadow-2xl sm:rounded-2xl sm:p-8 md:rounded-3xl md:p-10"
>
<div class="mb-6 text-center sm:mb-8">
<!-- 使用自定义布局来保持登录页面的居中大logo样式 -->
<div class="w-16 h-16 sm:w-20 sm:h-20 mx-auto mb-4 sm:mb-6 bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-gray-300/30 rounded-xl sm:rounded-2xl flex items-center justify-center backdrop-blur-sm overflow-hidden">
<div
class="mx-auto mb-4 flex h-16 w-16 items-center justify-center overflow-hidden rounded-xl border border-gray-300/30 bg-gradient-to-br from-blue-500/20 to-purple-500/20 backdrop-blur-sm sm:mb-6 sm:h-20 sm:w-20 sm:rounded-2xl"
>
<template v-if="!oemLoading">
<img
v-if="authStore.oemSettings.siteIconData || authStore.oemSettings.siteIcon"
:src="authStore.oemSettings.siteIconData || authStore.oemSettings.siteIcon"
v-if="authStore.oemSettings.siteIconData || authStore.oemSettings.siteIcon"
alt="Logo"
class="w-10 h-10 sm:w-12 sm:h-12 object-contain"
@error="(e) => e.target.style.display = 'none'"
>
<i
v-else
class="fas fa-cloud text-2xl sm:text-3xl text-gray-700"
class="h-10 w-10 object-contain sm:h-12 sm:w-12"
:src="authStore.oemSettings.siteIconData || authStore.oemSettings.siteIcon"
@error="(e) => (e.target.style.display = 'none')"
/>
<i v-else class="fas fa-cloud text-2xl text-gray-700 sm:text-3xl" />
</template>
<div
v-else
class="w-10 h-10 sm:w-12 sm:h-12 bg-gray-300/50 rounded animate-pulse"
/>
<div v-else class="h-10 w-10 animate-pulse rounded bg-gray-300/50 sm:h-12 sm:w-12" />
</div>
<template v-if="!oemLoading && authStore.oemSettings.siteName">
<h1 class="text-2xl sm:text-3xl font-bold text-white mb-2 header-title">
<h1 class="header-title mb-2 text-2xl font-bold text-white sm:text-3xl">
{{ authStore.oemSettings.siteName }}
</h1>
</template>
<div
v-else-if="oemLoading"
class="h-8 sm:h-9 w-48 sm:w-64 bg-gray-300/50 rounded animate-pulse mx-auto mb-2"
class="mx-auto mb-2 h-8 w-48 animate-pulse rounded bg-gray-300/50 sm:h-9 sm:w-64"
/>
<p class="text-gray-600 text-base sm:text-lg">
管理后台
</p>
<p class="text-base text-gray-600 sm:text-lg">管理后台</p>
</div>
<form
class="space-y-4 sm:space-y-6"
@submit.prevent="handleLogin"
>
<form class="space-y-4 sm:space-y-6" @submit.prevent="handleLogin">
<div>
<label class="block text-sm font-semibold text-gray-900 mb-2 sm:mb-3">用户名</label>
<input
v-model="loginForm.username"
type="text"
required
<label class="mb-2 block text-sm font-semibold text-gray-900 sm:mb-3">用户名</label>
<input
v-model="loginForm.username"
class="form-input w-full"
placeholder="请输入用户名"
>
required
type="text"
/>
</div>
<div>
<label class="block text-sm font-semibold text-gray-900 mb-2 sm:mb-3">密码</label>
<input
v-model="loginForm.password"
type="password"
required
<label class="mb-2 block text-sm font-semibold text-gray-900 sm:mb-3">密码</label>
<input
v-model="loginForm.password"
class="form-input w-full"
placeholder="请输入密码"
>
required
type="password"
/>
</div>
<button
type="submit"
<button
class="btn btn-primary w-full px-4 py-3 text-base font-semibold sm:px-6 sm:py-4 sm:text-lg"
:disabled="authStore.loginLoading"
class="btn btn-primary w-full py-3 sm:py-4 px-4 sm:px-6 text-base sm:text-lg font-semibold"
type="submit"
>
<i
v-if="!authStore.loginLoading"
class="fas fa-sign-in-alt mr-2"
/>
<div
v-if="authStore.loginLoading"
class="loading-spinner mr-2"
/>
<i v-if="!authStore.loginLoading" class="fas fa-sign-in-alt mr-2" />
<div v-if="authStore.loginLoading" class="loading-spinner mr-2" />
{{ authStore.loginLoading ? '登录中...' : '登录' }}
</button>
</form>
<div
v-if="authStore.loginError"
class="mt-4 sm:mt-6 p-3 sm:p-4 bg-red-500/20 border border-red-500/30 rounded-lg sm:rounded-xl text-red-800 text-xs sm:text-sm text-center backdrop-blur-sm"
class="mt-4 rounded-lg border border-red-500/30 bg-red-500/20 p-3 text-center text-xs text-red-800 backdrop-blur-sm sm:mt-6 sm:rounded-xl sm:p-4 sm:text-sm"
>
<i class="fas fa-exclamation-triangle mr-2" />{{ authStore.loginError }}
</div>
@@ -92,7 +79,6 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useAuthStore } from '@/stores/auth'
import LogoTitle from '@/components/common/LogoTitle.vue'
const authStore = useAuthStore()
const oemLoading = computed(() => authStore.oemLoading)
@@ -114,4 +100,4 @@ const handleLogin = async () => {
<style scoped>
/* 组件特定样式已经在全局样式中定义 */
</style>
</style>

View File

@@ -2,76 +2,58 @@
<div class="settings-container">
<div class="card p-4 sm:p-6">
<div class="mb-4 sm:mb-6">
<h3 class="text-lg sm:text-xl font-bold text-gray-900 mb-1 sm:mb-2">
其他设置
</h3>
<p class="text-sm sm:text-base text-gray-600">
自定义网站名称和图标
</p>
<h3 class="mb-1 text-lg font-bold text-gray-900 sm:mb-2 sm:text-xl">其他设置</h3>
<p class="text-sm text-gray-600 sm:text-base">自定义网站名称和图标</p>
</div>
<div
v-if="loading"
class="text-center py-12"
>
<div v-if="loading" class="py-12 text-center">
<div class="loading-spinner mx-auto mb-4" />
<p class="text-gray-500">
正在加载设置...
</p>
<p class="text-gray-500">正在加载设置...</p>
</div>
<!-- 桌面端表格视图 -->
<div
v-else
class="hidden sm:block table-container"
>
<div v-else class="table-container hidden sm:block">
<table class="min-w-full">
<tbody class="divide-y divide-gray-200/50">
<!-- 网站名称 -->
<tr class="table-row">
<td class="px-6 py-4 whitespace-nowrap w-48">
<td class="w-48 whitespace-nowrap px-6 py-4">
<div class="flex items-center">
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-font text-white text-xs" />
<div
class="mr-3 flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-blue-600"
>
<i class="fas fa-font text-xs text-white" />
</div>
<div>
<div class="text-sm font-semibold text-gray-900">
网站名称
</div>
<div class="text-xs text-gray-500">
品牌标识
</div>
<div class="text-sm font-semibold text-gray-900">网站名称</div>
<div class="text-xs text-gray-500">品牌标识</div>
</div>
</div>
</td>
<td class="px-6 py-4">
<input
<input
v-model="oemSettings.siteName"
type="text"
class="form-input w-full max-w-md"
placeholder="Claude Relay Service"
maxlength="100"
>
<p class="text-xs text-gray-500 mt-1">
将显示在浏览器标题和页面头部
</p>
placeholder="Claude Relay Service"
type="text"
/>
<p class="mt-1 text-xs text-gray-500">将显示在浏览器标题和页面头部</p>
</td>
</tr>
<!-- 网站图标 -->
<tr class="table-row">
<td class="px-6 py-4 whitespace-nowrap w-48">
<td class="w-48 whitespace-nowrap px-6 py-4">
<div class="flex items-center">
<div class="w-8 h-8 bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-image text-white text-xs" />
<div
class="mr-3 flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-purple-500 to-purple-600"
>
<i class="fas fa-image text-xs text-white" />
</div>
<div>
<div class="text-sm font-semibold text-gray-900">
网站图标
</div>
<div class="text-xs text-gray-500">
Favicon
</div>
<div class="text-sm font-semibold text-gray-900">网站图标</div>
<div class="text-xs text-gray-500">Favicon</div>
</div>
</div>
</td>
@@ -80,72 +62,62 @@
<!-- 图标预览 -->
<div
v-if="oemSettings.siteIconData || oemSettings.siteIcon"
class="inline-flex items-center gap-3 p-3 bg-gray-50 rounded-lg"
class="inline-flex items-center gap-3 rounded-lg bg-gray-50 p-3"
>
<img
:src="oemSettings.siteIconData || oemSettings.siteIcon"
alt="图标预览"
class="w-8 h-8"
<img
alt="图标预览"
class="h-8 w-8"
:src="oemSettings.siteIconData || oemSettings.siteIcon"
@error="handleIconError"
>
/>
<span class="text-sm text-gray-600">当前图标</span>
<button
class="text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors"
<button
class="rounded-lg px-3 py-1 font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900"
@click="removeIcon"
>
<i class="fas fa-trash mr-1" />删除
</button>
</div>
<!-- 文件上传 -->
<div>
<input
ref="iconFileInput"
type="file"
<input
ref="iconFileInput"
accept=".ico,.png,.jpg,.jpeg,.svg"
class="hidden"
type="file"
@change="handleIconUpload"
>
<button
class="btn btn-success px-4 py-2"
@click="$refs.iconFileInput.click()"
>
/>
<button class="btn btn-success px-4 py-2" @click="$refs.iconFileInput.click()">
<i class="fas fa-upload mr-2" />
上传图标
</button>
<span class="text-xs text-gray-500 ml-3">支持 .ico, .png, .jpg, .svg 格式最大 350KB</span>
<span class="ml-3 text-xs text-gray-500"
>支持 .ico, .png, .jpg, .svg 格式最大 350KB</span
>
</div>
</div>
</td>
</tr>
<!-- 操作按钮 -->
<tr>
<td
class="px-6 py-6"
colspan="2"
>
<td class="px-6 py-6" colspan="2">
<div class="flex items-center justify-between">
<div class="flex gap-3">
<button
:disabled="saving"
<button
class="btn btn-primary px-6 py-3"
:class="{ 'opacity-50 cursor-not-allowed': saving }"
:class="{ 'cursor-not-allowed opacity-50': saving }"
:disabled="saving"
@click="saveOemSettings"
>
<div
v-if="saving"
class="loading-spinner mr-2"
/>
<i
v-else
class="fas fa-save mr-2"
/>
<div v-if="saving" class="loading-spinner mr-2" />
<i v-else class="fas fa-save mr-2" />
{{ saving ? '保存中...' : '保存设置' }}
</button>
<button
class="btn bg-gray-100 text-gray-700 hover:bg-gray-200 px-6 py-3"
<button
class="btn bg-gray-100 px-6 py-3 text-gray-700 hover:bg-gray-200"
:disabled="saving"
@click="resetOemSettings"
>
@@ -153,11 +125,8 @@
重置为默认
</button>
</div>
<div
v-if="oemSettings.updatedAt"
class="text-sm text-gray-500"
>
<div v-if="oemSettings.updatedAt" class="text-sm text-gray-500">
<i class="fas fa-clock mr-1" />
最后更新{{ formatDateTime(oemSettings.updatedAt) }}
</div>
@@ -167,140 +136,118 @@
</tbody>
</table>
</div>
<!-- 移动端卡片视图 -->
<div
v-if="!loading"
class="sm:hidden space-y-4"
>
<div v-if="!loading" class="space-y-4 sm:hidden">
<!-- 网站名称设置卡片 -->
<div class="bg-gray-50 rounded-lg p-4">
<div class="flex items-center gap-3 mb-3">
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-font text-white text-sm" />
<div class="rounded-lg bg-gray-50 p-4">
<div class="mb-3 flex items-center gap-3">
<div
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-blue-600"
>
<i class="fas fa-font text-sm text-white" />
</div>
<div>
<h4 class="text-sm font-semibold text-gray-900">
网站名称
</h4>
<p class="text-xs text-gray-500">
品牌标识
</p>
<h4 class="text-sm font-semibold text-gray-900">网站名称</h4>
<p class="text-xs text-gray-500">品牌标识</p>
</div>
</div>
<div class="space-y-2">
<input
<input
v-model="oemSettings.siteName"
type="text"
class="form-input w-full text-sm"
placeholder="Claude Relay Service"
maxlength="100"
>
<p class="text-xs text-gray-500">
将显示在浏览器标题和页面头部
</p>
placeholder="Claude Relay Service"
type="text"
/>
<p class="text-xs text-gray-500">将显示在浏览器标题和页面头部</p>
</div>
</div>
<!-- 网站图标设置卡片 -->
<div class="bg-gray-50 rounded-lg p-4">
<div class="flex items-center gap-3 mb-3">
<div class="w-10 h-10 bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-image text-white text-sm" />
<div class="rounded-lg bg-gray-50 p-4">
<div class="mb-3 flex items-center gap-3">
<div
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-purple-500 to-purple-600"
>
<i class="fas fa-image text-sm text-white" />
</div>
<div>
<h4 class="text-sm font-semibold text-gray-900">
网站图标
</h4>
<p class="text-xs text-gray-500">
Favicon
</p>
<h4 class="text-sm font-semibold text-gray-900">网站图标</h4>
<p class="text-xs text-gray-500">Favicon</p>
</div>
</div>
<div class="space-y-3">
<!-- 图标预览 -->
<div
v-if="oemSettings.siteIconData || oemSettings.siteIcon"
class="inline-flex items-center gap-3 p-3 bg-white rounded-lg border border-gray-200 w-full"
class="inline-flex w-full items-center gap-3 rounded-lg border border-gray-200 bg-white p-3"
>
<img
:src="oemSettings.siteIconData || oemSettings.siteIcon"
alt="图标预览"
class="w-8 h-8"
<img
alt="图标预览"
class="h-8 w-8"
:src="oemSettings.siteIconData || oemSettings.siteIcon"
@error="handleIconError"
>
<span class="text-sm text-gray-600 flex-1">当前图标</span>
<button
class="text-red-600 hover:text-red-900 text-sm font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors"
/>
<span class="flex-1 text-sm text-gray-600">当前图标</span>
<button
class="rounded-lg px-3 py-1 text-sm font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900"
@click="removeIcon"
>
<i class="fas fa-trash mr-1" />删除
</button>
</div>
<!-- 上传按钮 -->
<input
<input
ref="iconFileInput"
type="file"
accept="image/*"
style="display: none;"
accept="image/*"
style="display: none"
type="file"
@change="handleIconUpload"
>
/>
<div class="flex flex-col gap-2">
<button
class="w-full px-4 py-2 bg-white border border-gray-200 rounded-lg text-sm text-gray-700 hover:bg-gray-50 transition-colors flex items-center justify-center gap-2"
<button
class="flex w-full items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-50"
@click="$refs.iconFileInput.click()"
>
<i class="fas fa-upload" />
上传图标
</button>
<div class="text-xs text-gray-500">
或者输入图标URL
</div>
<input
<div class="text-xs text-gray-500">或者输入图标URL</div>
<input
v-model="oemSettings.siteIcon"
type="url"
class="form-input w-full text-sm"
placeholder="https://example.com/icon.png"
>
type="url"
/>
</div>
<p class="text-xs text-gray-500">
支持 PNGJPEGGIF 格式建议使用正方形图片
</p>
<p class="text-xs text-gray-500">支持 PNGJPEGGIF 格式建议使用正方形图片</p>
</div>
</div>
<!-- 操作按钮 -->
<div class="space-y-3 pt-2">
<button
<button
class="btn btn-primary w-full py-3 text-sm font-semibold"
:disabled="saving"
@click="saveOemSettings"
>
<div
v-if="saving"
class="loading-spinner mr-2"
/>
<i
v-else
class="fas fa-save mr-2"
/>
<div v-if="saving" class="loading-spinner mr-2" />
<i v-else class="fas fa-save mr-2" />
{{ saving ? '保存中...' : '保存设置' }}
</button>
<button
class="btn bg-gray-100 text-gray-700 hover:bg-gray-200 w-full py-3 text-sm"
<button
class="btn w-full bg-gray-100 py-3 text-sm text-gray-700 hover:bg-gray-200"
:disabled="saving"
@click="resetOemSettings"
>
<i class="fas fa-undo mr-2" />
重置为默认
</button>
<div
v-if="oemSettings.updatedAt"
class="text-center text-xs text-gray-500"
>
<div v-if="oemSettings.updatedAt" class="text-center text-xs text-gray-500">
<i class="fas fa-clock mr-1" />
最后更新{{ formatDateTime(oemSettings.updatedAt) }}
</div>
@@ -354,7 +301,7 @@ const saveOemSettings = async () => {
// 重置OEM设置
const resetOemSettings = async () => {
if (!confirm('确定要重置为默认设置吗?\n\n这将清除所有自定义的网站名称和图标设置。')) return
try {
const result = await settingsStore.resetOemSettings()
if (result && result.success) {
@@ -371,14 +318,14 @@ const resetOemSettings = async () => {
const handleIconUpload = async (event) => {
const file = event.target.files[0]
if (!file) return
// 验证文件
const validation = settingsStore.validateIconFile(file)
if (!validation.isValid) {
validation.errors.forEach(error => showToast(error, 'error'))
validation.errors.forEach((error) => showToast(error, 'error'))
return
}
try {
// 转换为Base64
const base64Data = await settingsStore.fileToBase64(file)
@@ -386,7 +333,7 @@ const handleIconUpload = async (event) => {
} catch (error) {
showToast('文件读取失败', 'error')
}
// 清除input的值允许重复选择同一文件
event.target.value = ''
}
@@ -433,11 +380,11 @@ const formatDateTime = settingsStore.formatDateTime
}
.form-input {
@apply w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200;
@apply w-full rounded-lg border border-gray-300 px-4 py-2 transition-all duration-200 focus:border-transparent focus:ring-2 focus:ring-blue-500;
}
.btn {
@apply inline-flex items-center justify-center px-4 py-2 text-sm font-semibold rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2;
@apply inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-semibold transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2;
}
.btn-primary {
@@ -449,6 +396,6 @@ const formatDateTime = settingsStore.formatDateTime
}
.loading-spinner {
@apply w-5 h-5 border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin;
@apply h-5 w-5 animate-spin rounded-full border-2 border-gray-300 border-t-blue-600;
}
</style>
</style>

File diff suppressed because it is too large Load Diff