Commit ecfad788 authored by IanShaw027's avatar IanShaw027
Browse files

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组件
- 部署:配置文件示例

**兼容性**:
- 简易模式和标准模式可无缝切换
- 不需要数据迁移
- 现有数据不受影响
parent e247be6e
...@@ -36,6 +36,7 @@ services: ...@@ -36,6 +36,7 @@ services:
- SERVER_HOST=0.0.0.0 - SERVER_HOST=0.0.0.0
- SERVER_PORT=8080 - SERVER_PORT=8080
- SERVER_MODE=${SERVER_MODE:-release} - SERVER_MODE=${SERVER_MODE:-release}
- RUN_MODE=${RUN_MODE:-standard}
# ======================================================================= # =======================================================================
# Database Configuration (PostgreSQL) # Database Configuration (PostgreSQL)
......
...@@ -8,7 +8,7 @@ import type { ...@@ -8,7 +8,7 @@ import type {
LoginRequest, LoginRequest,
RegisterRequest, RegisterRequest,
AuthResponse, AuthResponse,
User, CurrentUserResponse,
SendVerifyCodeRequest, SendVerifyCodeRequest,
SendVerifyCodeResponse, SendVerifyCodeResponse,
PublicSettings PublicSettings
...@@ -70,9 +70,8 @@ export async function register(userData: RegisterRequest): Promise<AuthResponse> ...@@ -70,9 +70,8 @@ export async function register(userData: RegisterRequest): Promise<AuthResponse>
* Get current authenticated user * Get current authenticated user
* @returns User profile data * @returns User profile data
*/ */
export async function getCurrentUser(): Promise<User> { export async function getCurrentUser() {
const { data } = await apiClient.get<User>('/auth/me') return apiClient.get<CurrentUserResponse>('/auth/me')
return data
} }
/** /**
......
...@@ -964,8 +964,13 @@ ...@@ -964,8 +964,13 @@
</div> </div>
</div> </div>
<!-- Group Selection --> <!-- Group Selection - 仅标准模式显示 -->
<GroupSelector v-model="form.group_ids" :groups="groups" :platform="form.platform" /> <GroupSelector
v-if="!authStore.isSimpleMode"
v-model="form.group_ids"
:groups="groups"
:platform="form.platform"
/>
</form> </form>
...@@ -1076,6 +1081,7 @@ ...@@ -1076,6 +1081,7 @@
import { ref, reactive, computed, watch } from 'vue' import { ref, reactive, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import { useAuthStore } from '@/stores/auth'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
import { import {
useAccountOAuth, useAccountOAuth,
...@@ -1102,6 +1108,7 @@ interface OAuthFlowExposed { ...@@ -1102,6 +1108,7 @@ interface OAuthFlowExposed {
} }
const { t } = useI18n() const { t } = useI18n()
const authStore = useAuthStore()
const oauthStepTitle = computed(() => { const oauthStepTitle = computed(() => {
if (form.platform === 'openai') return t('admin.accounts.oauth.openai.title') if (form.platform === 'openai') return t('admin.accounts.oauth.openai.title')
......
...@@ -466,8 +466,13 @@ ...@@ -466,8 +466,13 @@
<Select v-model="form.status" :options="statusOptions" /> <Select v-model="form.status" :options="statusOptions" />
</div> </div>
<!-- Group Selection --> <!-- Group Selection - 仅标准模式显示 -->
<GroupSelector v-model="form.group_ids" :groups="groups" :platform="account?.platform" /> <GroupSelector
v-if="!authStore.isSimpleMode"
v-model="form.group_ids"
:groups="groups"
:platform="account?.platform"
/>
</form> </form>
...@@ -513,6 +518,7 @@ ...@@ -513,6 +518,7 @@
import { ref, reactive, computed, watch } from 'vue' import { ref, reactive, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import { useAuthStore } from '@/stores/auth'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
import type { Account, Proxy, Group } from '@/types' import type { Account, Proxy, Group } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
...@@ -535,6 +541,7 @@ const emit = defineEmits<{ ...@@ -535,6 +541,7 @@ const emit = defineEmits<{
const { t } = useI18n() const { t } = useI18n()
const appStore = useAppStore() const appStore = useAppStore()
const authStore = useAuthStore()
// Platform-specific hint for Base URL // Platform-specific hint for Base URL
const baseUrlHint = computed(() => { const baseUrlHint = computed(() => {
......
...@@ -432,8 +432,8 @@ const adminNavItems = computed(() => { ...@@ -432,8 +432,8 @@ const adminNavItems = computed(() => {
const baseItems = [ const baseItems = [
{ path: '/admin/dashboard', label: t('nav.dashboard'), icon: DashboardIcon }, { path: '/admin/dashboard', label: t('nav.dashboard'), icon: DashboardIcon },
{ path: '/admin/users', label: t('nav.users'), icon: UsersIcon, hideInSimpleMode: true }, { path: '/admin/users', label: t('nav.users'), icon: UsersIcon, hideInSimpleMode: true },
{ path: '/admin/groups', label: t('nav.groups'), icon: FolderIcon }, { path: '/admin/groups', label: t('nav.groups'), icon: FolderIcon, hideInSimpleMode: true },
{ path: '/admin/subscriptions', label: t('nav.subscriptions'), icon: CreditCardIcon }, { path: '/admin/subscriptions', label: t('nav.subscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
{ path: '/admin/accounts', label: t('nav.accounts'), icon: GlobeIcon }, { path: '/admin/accounts', label: t('nav.accounts'), icon: GlobeIcon },
{ path: '/admin/proxies', label: t('nav.proxies'), icon: ServerIcon }, { path: '/admin/proxies', label: t('nav.proxies'), icon: ServerIcon },
{ path: '/admin/redeem', label: t('nav.redeemCodes'), icon: TicketIcon, hideInSimpleMode: true }, { path: '/admin/redeem', label: t('nav.redeemCodes'), icon: TicketIcon, hideInSimpleMode: true },
......
...@@ -341,6 +341,23 @@ router.beforeEach((to, _from, next) => { ...@@ -341,6 +341,23 @@ router.beforeEach((to, _from, next) => {
return 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 // All checks passed, allow navigation
next() next()
}) })
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
*/ */
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed, readonly } from 'vue'
import { authAPI } from '@/api' import { authAPI } from '@/api'
import type { User, LoginRequest, RegisterRequest } from '@/types' import type { User, LoginRequest, RegisterRequest } from '@/types'
...@@ -17,6 +17,7 @@ export const useAuthStore = defineStore('auth', () => { ...@@ -17,6 +17,7 @@ export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null) const user = ref<User | null>(null)
const token = ref<string | null>(null) const token = ref<string | null>(null)
const runMode = ref<'standard' | 'simple'>('standard')
let refreshIntervalId: ReturnType<typeof setInterval> | null = null let refreshIntervalId: ReturnType<typeof setInterval> | null = null
// ==================== Computed ==================== // ==================== Computed ====================
...@@ -29,6 +30,8 @@ export const useAuthStore = defineStore('auth', () => { ...@@ -29,6 +30,8 @@ export const useAuthStore = defineStore('auth', () => {
return user.value?.role === 'admin' return user.value?.role === 'admin'
}) })
const isSimpleMode = computed(() => runMode.value === 'simple')
// ==================== Actions ==================== // ==================== Actions ====================
/** /**
...@@ -168,13 +171,17 @@ export const useAuthStore = defineStore('auth', () => { ...@@ -168,13 +171,17 @@ export const useAuthStore = defineStore('auth', () => {
} }
try { try {
const updatedUser = await authAPI.getCurrentUser() const response = await authAPI.getCurrentUser()
user.value = updatedUser if (response.data.run_mode) {
runMode.value = response.data.run_mode
}
const { run_mode, ...userData } = response.data
user.value = userData
// Update localStorage // Update localStorage
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(updatedUser)) localStorage.setItem(AUTH_USER_KEY, JSON.stringify(userData))
return updatedUser return userData
} catch (error) { } catch (error) {
// If refresh fails with 401, clear auth state // If refresh fails with 401, clear auth state
if ((error as { status?: number }).status === 401) { if ((error as { status?: number }).status === 401) {
...@@ -204,10 +211,12 @@ export const useAuthStore = defineStore('auth', () => { ...@@ -204,10 +211,12 @@ export const useAuthStore = defineStore('auth', () => {
// State // State
user, user,
token, token,
runMode: readonly(runMode),
// Computed // Computed
isAuthenticated, isAuthenticated,
isAdmin, isAdmin,
isSimpleMode,
// Actions // Actions
login, login,
......
...@@ -64,6 +64,10 @@ export interface AuthResponse { ...@@ -64,6 +64,10 @@ export interface AuthResponse {
user: User user: User
} }
export interface CurrentUserResponse extends User {
run_mode?: 'standard' | 'simple'
}
// ==================== Subscription Types ==================== // ==================== Subscription Types ====================
export interface Subscription { export interface Subscription {
......
...@@ -494,6 +494,7 @@ ...@@ -494,6 +494,7 @@
import { ref, reactive, computed, onMounted, onUnmounted, type ComponentPublicInstance } from 'vue' import { ref, reactive, computed, onMounted, onUnmounted, type ComponentPublicInstance } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import { useAuthStore } from '@/stores/auth'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
import type { Account, Proxy, Group } from '@/types' import type { Account, Proxy, Group } from '@/types'
import type { Column } from '@/components/common/types' import type { Column } from '@/components/common/types'
...@@ -522,22 +523,34 @@ import { formatRelativeTime } from '@/utils/format' ...@@ -522,22 +523,34 @@ import { formatRelativeTime } from '@/utils/format'
const { t } = useI18n() const { t } = useI18n()
const appStore = useAppStore() const appStore = useAppStore()
const authStore = useAuthStore()
// Table columns // Table columns
const columns = computed<Column[]>(() => [ const columns = computed<Column[]>(() => {
const cols: Column[] = [
{ key: 'select', label: '', sortable: false }, { key: 'select', label: '', sortable: false },
{ key: 'name', label: t('admin.accounts.columns.name'), sortable: true }, { key: 'name', label: t('admin.accounts.columns.name'), sortable: true },
{ key: 'platform_type', label: t('admin.accounts.columns.platformType'), sortable: false }, { key: 'platform_type', label: t('admin.accounts.columns.platformType'), sortable: false },
{ key: 'concurrency', label: t('admin.accounts.columns.concurrencyStatus'), sortable: false }, { key: 'concurrency', label: t('admin.accounts.columns.concurrencyStatus'), sortable: false },
{ key: 'status', label: t('admin.accounts.columns.status'), sortable: true }, { key: 'status', label: t('admin.accounts.columns.status'), sortable: true },
{ key: 'schedulable', label: t('admin.accounts.columns.schedulable'), sortable: true }, { key: 'schedulable', label: t('admin.accounts.columns.schedulable'), sortable: true },
{ key: 'today_stats', label: t('admin.accounts.columns.todayStats'), sortable: false }, { key: 'today_stats', label: t('admin.accounts.columns.todayStats'), sortable: false }
{ key: 'groups', label: t('admin.accounts.columns.groups'), 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: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
{ key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true }, { key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
{ key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true }, { key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
{ key: 'actions', label: t('admin.accounts.columns.actions'), sortable: false } { key: 'actions', label: t('admin.accounts.columns.actions'), sortable: false }
]) )
return cols
})
// Filter options // Filter options
const platformOptions = computed(() => [ const platformOptions = computed(() => [
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment