feat(全栈): 实现简易模式核心功能

**功能概述**:
实现简易模式(Simple Mode),为个人用户和小团队提供简化的使用体验,隐藏复杂的分组、订阅、配额等概念。

**后端改动**:
1. 配置系统
   - 新增 run_mode 配置项(standard/simple)
   - 支持环境变量 RUN_MODE
   - 默认值为 standard

2. 数据库初始化
   - 自动创建3个默认分组:anthropic-default、openai-default、gemini-default
   - 默认分组配置:无并发限制、active状态、非独占
   - 幂等性保证:重复启动不会重复创建

3. 账号管理
   - 创建账号时自动绑定对应平台的默认分组
   - 如果未指定分组,自动查找并绑定默认分组

**前端改动**:
1. 状态管理
   - authStore 新增 isSimpleMode 计算属性
   - 从后端API获取并同步运行模式

2. UI隐藏
   - 侧边栏:隐藏分组管理、订阅管理、兑换码菜单
   - 账号管理页面:隐藏分组列
   - 创建/编辑账号对话框:隐藏分组选择器

3. 路由守卫
   - 限制访问分组、订阅、兑换码相关页面
   - 访问受限页面时自动重定向到仪表板

**配置示例**:
```yaml
run_mode: simple

run_mode: standard
```

**影响范围**:
- 后端:配置、数据库迁移、账号服务
- 前端:认证状态、路由、UI组件
- 部署:配置文件示例

**兼容性**:
- 简易模式和标准模式可无缝切换
- 不需要数据迁移
- 现有数据不受影响
This commit is contained in:
IanShaw027
2025-12-29 03:17:25 +08:00
parent e247be6ead
commit ecfad788d9
29 changed files with 615 additions and 51 deletions

View File

@@ -8,7 +8,7 @@ import type {
LoginRequest,
RegisterRequest,
AuthResponse,
User,
CurrentUserResponse,
SendVerifyCodeRequest,
SendVerifyCodeResponse,
PublicSettings
@@ -70,9 +70,8 @@ export async function register(userData: RegisterRequest): Promise<AuthResponse>
* Get current authenticated user
* @returns User profile data
*/
export async function getCurrentUser(): Promise<User> {
const { data } = await apiClient.get<User>('/auth/me')
return data
export async function getCurrentUser() {
return apiClient.get<CurrentUserResponse>('/auth/me')
}
/**

View File

@@ -964,8 +964,13 @@
</div>
</div>
<!-- Group Selection -->
<GroupSelector v-model="form.group_ids" :groups="groups" :platform="form.platform" />
<!-- Group Selection - 仅标准模式显示 -->
<GroupSelector
v-if="!authStore.isSimpleMode"
v-model="form.group_ids"
:groups="groups"
:platform="form.platform"
/>
</form>
@@ -1076,6 +1081,7 @@
import { ref, reactive, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { useAuthStore } from '@/stores/auth'
import { adminAPI } from '@/api/admin'
import {
useAccountOAuth,
@@ -1102,6 +1108,7 @@ interface OAuthFlowExposed {
}
const { t } = useI18n()
const authStore = useAuthStore()
const oauthStepTitle = computed(() => {
if (form.platform === 'openai') return t('admin.accounts.oauth.openai.title')

View File

@@ -466,8 +466,13 @@
<Select v-model="form.status" :options="statusOptions" />
</div>
<!-- Group Selection -->
<GroupSelector v-model="form.group_ids" :groups="groups" :platform="account?.platform" />
<!-- Group Selection - 仅标准模式显示 -->
<GroupSelector
v-if="!authStore.isSimpleMode"
v-model="form.group_ids"
:groups="groups"
:platform="account?.platform"
/>
</form>
@@ -513,6 +518,7 @@
import { ref, reactive, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { useAuthStore } from '@/stores/auth'
import { adminAPI } from '@/api/admin'
import type { Account, Proxy, Group } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue'
@@ -535,6 +541,7 @@ const emit = defineEmits<{
const { t } = useI18n()
const appStore = useAppStore()
const authStore = useAuthStore()
// Platform-specific hint for Base URL
const baseUrlHint = computed(() => {

View File

@@ -432,8 +432,8 @@ const adminNavItems = computed(() => {
const baseItems = [
{ path: '/admin/dashboard', label: t('nav.dashboard'), icon: DashboardIcon },
{ path: '/admin/users', label: t('nav.users'), icon: UsersIcon, hideInSimpleMode: true },
{ path: '/admin/groups', label: t('nav.groups'), icon: FolderIcon },
{ path: '/admin/subscriptions', label: t('nav.subscriptions'), icon: CreditCardIcon },
{ path: '/admin/groups', label: t('nav.groups'), icon: FolderIcon, hideInSimpleMode: true },
{ path: '/admin/subscriptions', label: t('nav.subscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
{ path: '/admin/accounts', label: t('nav.accounts'), icon: GlobeIcon },
{ path: '/admin/proxies', label: t('nav.proxies'), icon: ServerIcon },
{ path: '/admin/redeem', label: t('nav.redeemCodes'), icon: TicketIcon, hideInSimpleMode: true },

View File

@@ -341,6 +341,23 @@ router.beforeEach((to, _from, next) => {
return
}
// 简易模式下限制访问某些页面
if (authStore.isSimpleMode) {
const restrictedPaths = [
'/admin/groups',
'/admin/subscriptions',
'/admin/redeem',
'/subscriptions',
'/redeem'
]
if (restrictedPaths.some((path) => to.path.startsWith(path))) {
// 简易模式下访问受限页面,重定向到仪表板
next(authStore.isAdmin ? '/admin/dashboard' : '/dashboard')
return
}
}
// All checks passed, allow navigation
next()
})

View File

@@ -4,7 +4,7 @@
*/
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { ref, computed, readonly } from 'vue'
import { authAPI } from '@/api'
import type { User, LoginRequest, RegisterRequest } from '@/types'
@@ -17,6 +17,7 @@ export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const token = ref<string | null>(null)
const runMode = ref<'standard' | 'simple'>('standard')
let refreshIntervalId: ReturnType<typeof setInterval> | null = null
// ==================== Computed ====================
@@ -29,6 +30,8 @@ export const useAuthStore = defineStore('auth', () => {
return user.value?.role === 'admin'
})
const isSimpleMode = computed(() => runMode.value === 'simple')
// ==================== Actions ====================
/**
@@ -168,13 +171,17 @@ export const useAuthStore = defineStore('auth', () => {
}
try {
const updatedUser = await authAPI.getCurrentUser()
user.value = updatedUser
const response = await authAPI.getCurrentUser()
if (response.data.run_mode) {
runMode.value = response.data.run_mode
}
const { run_mode, ...userData } = response.data
user.value = userData
// Update localStorage
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(updatedUser))
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(userData))
return updatedUser
return userData
} catch (error) {
// If refresh fails with 401, clear auth state
if ((error as { status?: number }).status === 401) {
@@ -204,10 +211,12 @@ export const useAuthStore = defineStore('auth', () => {
// State
user,
token,
runMode: readonly(runMode),
// Computed
isAuthenticated,
isAdmin,
isSimpleMode,
// Actions
login,

View File

@@ -64,6 +64,10 @@ export interface AuthResponse {
user: User
}
export interface CurrentUserResponse extends User {
run_mode?: 'standard' | 'simple'
}
// ==================== Subscription Types ====================
export interface Subscription {

View File

@@ -494,6 +494,7 @@
import { ref, reactive, computed, onMounted, onUnmounted, type ComponentPublicInstance } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { useAuthStore } from '@/stores/auth'
import { adminAPI } from '@/api/admin'
import type { Account, Proxy, Group } from '@/types'
import type { Column } from '@/components/common/types'
@@ -522,22 +523,34 @@ import { formatRelativeTime } from '@/utils/format'
const { t } = useI18n()
const appStore = useAppStore()
const authStore = useAuthStore()
// Table columns
const columns = computed<Column[]>(() => [
{ key: 'select', label: '', sortable: false },
{ key: 'name', label: t('admin.accounts.columns.name'), sortable: true },
{ key: 'platform_type', label: t('admin.accounts.columns.platformType'), sortable: false },
{ key: 'concurrency', label: t('admin.accounts.columns.concurrencyStatus'), sortable: false },
{ key: 'status', label: t('admin.accounts.columns.status'), sortable: true },
{ key: 'schedulable', label: t('admin.accounts.columns.schedulable'), sortable: true },
{ key: 'today_stats', label: t('admin.accounts.columns.todayStats'), sortable: false },
{ key: 'groups', label: t('admin.accounts.columns.groups'), sortable: false },
{ key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
{ key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
{ key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
{ key: 'actions', label: t('admin.accounts.columns.actions'), sortable: false }
])
const columns = computed<Column[]>(() => {
const cols: Column[] = [
{ key: 'select', label: '', sortable: false },
{ key: 'name', label: t('admin.accounts.columns.name'), sortable: true },
{ key: 'platform_type', label: t('admin.accounts.columns.platformType'), sortable: false },
{ key: 'concurrency', label: t('admin.accounts.columns.concurrencyStatus'), sortable: false },
{ key: 'status', label: t('admin.accounts.columns.status'), sortable: true },
{ key: 'schedulable', label: t('admin.accounts.columns.schedulable'), sortable: true },
{ key: 'today_stats', label: t('admin.accounts.columns.todayStats'), sortable: false }
]
// 简易模式下不显示分组列
if (!authStore.isSimpleMode) {
cols.push({ key: 'groups', label: t('admin.accounts.columns.groups'), sortable: false })
}
cols.push(
{ key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
{ key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
{ key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
{ key: 'actions', label: t('admin.accounts.columns.actions'), sortable: false }
)
return cols
})
// Filter options
const platformOptions = computed(() => [