"git@web.lueluesay.top:chenxi/sub2api.git" did not exist on "8e55ee0e2ca9c5fd00e7afa5ded757bea43d2667"
Commit 942c3e15 authored by song's avatar song
Browse files

Merge branch 'main' into feature/antigravity_auth_image

parents caa8c47b c328b741
...@@ -600,8 +600,3 @@ formatters: ...@@ -600,8 +600,3 @@ formatters:
replacement: 'any' replacement: 'any'
- pattern: 'a[b:len(a)]' - pattern: 'a[b:len(a)]'
replacement: 'a[b:]' replacement: 'a[b:]'
exclusions:
paths:
- internal/pkg/antigravity/claude_types.go
- internal/pkg/antigravity/gemini_types.go
- internal/pkg/antigravity/stream_transformer.go
\ No newline at end of file
...@@ -135,18 +135,18 @@ func (p *StreamingProcessor) emitMessageStart(v1Resp *V1InternalResponse) []byte ...@@ -135,18 +135,18 @@ func (p *StreamingProcessor) emitMessageStart(v1Resp *V1InternalResponse) []byte
responseID = "msg_" + generateRandomID() responseID = "msg_" + generateRandomID()
} }
message := map[string]interface{}{ message := map[string]any{
"id": responseID, "id": responseID,
"type": "message", "type": "message",
"role": "assistant", "role": "assistant",
"content": []interface{}{}, "content": []any{},
"model": p.originalModel, "model": p.originalModel,
"stop_reason": nil, "stop_reason": nil,
"stop_sequence": nil, "stop_sequence": nil,
"usage": usage, "usage": usage,
} }
event := map[string]interface{}{ event := map[string]any{
"type": "message_start", "type": "message_start",
"message": message, "message": message,
} }
...@@ -205,14 +205,14 @@ func (p *StreamingProcessor) processThinking(text, signature string) []byte { ...@@ -205,14 +205,14 @@ func (p *StreamingProcessor) processThinking(text, signature string) []byte {
// 开始或继续 thinking 块 // 开始或继续 thinking 块
if p.blockType != BlockTypeThinking { if p.blockType != BlockTypeThinking {
_, _ = result.Write(p.startBlock(BlockTypeThinking, map[string]interface{}{ _, _ = result.Write(p.startBlock(BlockTypeThinking, map[string]any{
"type": "thinking", "type": "thinking",
"thinking": "", "thinking": "",
})) }))
} }
if text != "" { if text != "" {
_, _ = result.Write(p.emitDelta("thinking_delta", map[string]interface{}{ _, _ = result.Write(p.emitDelta("thinking_delta", map[string]any{
"thinking": text, "thinking": text,
})) }))
} }
...@@ -246,11 +246,11 @@ func (p *StreamingProcessor) processText(text, signature string) []byte { ...@@ -246,11 +246,11 @@ func (p *StreamingProcessor) processText(text, signature string) []byte {
// 非空 text 带签名 - 特殊处理 // 非空 text 带签名 - 特殊处理
if signature != "" { if signature != "" {
_, _ = result.Write(p.startBlock(BlockTypeText, map[string]interface{}{ _, _ = result.Write(p.startBlock(BlockTypeText, map[string]any{
"type": "text", "type": "text",
"text": "", "text": "",
})) }))
_, _ = result.Write(p.emitDelta("text_delta", map[string]interface{}{ _, _ = result.Write(p.emitDelta("text_delta", map[string]any{
"text": text, "text": text,
})) }))
_, _ = result.Write(p.endBlock()) _, _ = result.Write(p.endBlock())
...@@ -260,13 +260,13 @@ func (p *StreamingProcessor) processText(text, signature string) []byte { ...@@ -260,13 +260,13 @@ func (p *StreamingProcessor) processText(text, signature string) []byte {
// 普通 text (无签名) // 普通 text (无签名)
if p.blockType != BlockTypeText { if p.blockType != BlockTypeText {
_, _ = result.Write(p.startBlock(BlockTypeText, map[string]interface{}{ _, _ = result.Write(p.startBlock(BlockTypeText, map[string]any{
"type": "text", "type": "text",
"text": "", "text": "",
})) }))
} }
_, _ = result.Write(p.emitDelta("text_delta", map[string]interface{}{ _, _ = result.Write(p.emitDelta("text_delta", map[string]any{
"text": text, "text": text,
})) }))
...@@ -284,11 +284,11 @@ func (p *StreamingProcessor) processFunctionCall(fc *GeminiFunctionCall, signatu ...@@ -284,11 +284,11 @@ func (p *StreamingProcessor) processFunctionCall(fc *GeminiFunctionCall, signatu
toolID = fmt.Sprintf("%s-%s", fc.Name, generateRandomID()) toolID = fmt.Sprintf("%s-%s", fc.Name, generateRandomID())
} }
toolUse := map[string]interface{}{ toolUse := map[string]any{
"type": "tool_use", "type": "tool_use",
"id": toolID, "id": toolID,
"name": fc.Name, "name": fc.Name,
"input": map[string]interface{}{}, // 必须为空,参数通过 delta 发送 "input": map[string]any{},
} }
if signature != "" { if signature != "" {
...@@ -300,7 +300,7 @@ func (p *StreamingProcessor) processFunctionCall(fc *GeminiFunctionCall, signatu ...@@ -300,7 +300,7 @@ func (p *StreamingProcessor) processFunctionCall(fc *GeminiFunctionCall, signatu
// 发送 input_json_delta // 发送 input_json_delta
if fc.Args != nil { if fc.Args != nil {
argsJSON, _ := json.Marshal(fc.Args) argsJSON, _ := json.Marshal(fc.Args)
_, _ = result.Write(p.emitDelta("input_json_delta", map[string]interface{}{ _, _ = result.Write(p.emitDelta("input_json_delta", map[string]any{
"partial_json": string(argsJSON), "partial_json": string(argsJSON),
})) }))
} }
...@@ -311,14 +311,14 @@ func (p *StreamingProcessor) processFunctionCall(fc *GeminiFunctionCall, signatu ...@@ -311,14 +311,14 @@ func (p *StreamingProcessor) processFunctionCall(fc *GeminiFunctionCall, signatu
} }
// startBlock 开始新的内容块 // startBlock 开始新的内容块
func (p *StreamingProcessor) startBlock(blockType BlockType, contentBlock map[string]interface{}) []byte { func (p *StreamingProcessor) startBlock(blockType BlockType, contentBlock map[string]any) []byte {
var result bytes.Buffer var result bytes.Buffer
if p.blockType != BlockTypeNone { if p.blockType != BlockTypeNone {
_, _ = result.Write(p.endBlock()) _, _ = result.Write(p.endBlock())
} }
event := map[string]interface{}{ event := map[string]any{
"type": "content_block_start", "type": "content_block_start",
"index": p.blockIndex, "index": p.blockIndex,
"content_block": contentBlock, "content_block": contentBlock,
...@@ -340,13 +340,13 @@ func (p *StreamingProcessor) endBlock() []byte { ...@@ -340,13 +340,13 @@ func (p *StreamingProcessor) endBlock() []byte {
// Thinking 块结束时发送暂存的签名 // Thinking 块结束时发送暂存的签名
if p.blockType == BlockTypeThinking && p.pendingSignature != "" { if p.blockType == BlockTypeThinking && p.pendingSignature != "" {
_, _ = result.Write(p.emitDelta("signature_delta", map[string]interface{}{ _, _ = result.Write(p.emitDelta("signature_delta", map[string]any{
"signature": p.pendingSignature, "signature": p.pendingSignature,
})) }))
p.pendingSignature = "" p.pendingSignature = ""
} }
event := map[string]interface{}{ event := map[string]any{
"type": "content_block_stop", "type": "content_block_stop",
"index": p.blockIndex, "index": p.blockIndex,
} }
...@@ -360,15 +360,15 @@ func (p *StreamingProcessor) endBlock() []byte { ...@@ -360,15 +360,15 @@ func (p *StreamingProcessor) endBlock() []byte {
} }
// emitDelta 发送 delta 事件 // emitDelta 发送 delta 事件
func (p *StreamingProcessor) emitDelta(deltaType string, deltaContent map[string]interface{}) []byte { func (p *StreamingProcessor) emitDelta(deltaType string, deltaContent map[string]any) []byte {
delta := map[string]interface{}{ delta := map[string]any{
"type": deltaType, "type": deltaType,
} }
for k, v := range deltaContent { for k, v := range deltaContent {
delta[k] = v delta[k] = v
} }
event := map[string]interface{}{ event := map[string]any{
"type": "content_block_delta", "type": "content_block_delta",
"index": p.blockIndex, "index": p.blockIndex,
"delta": delta, "delta": delta,
...@@ -381,14 +381,14 @@ func (p *StreamingProcessor) emitDelta(deltaType string, deltaContent map[string ...@@ -381,14 +381,14 @@ func (p *StreamingProcessor) emitDelta(deltaType string, deltaContent map[string
func (p *StreamingProcessor) emitEmptyThinkingWithSignature(signature string) []byte { func (p *StreamingProcessor) emitEmptyThinkingWithSignature(signature string) []byte {
var result bytes.Buffer var result bytes.Buffer
_, _ = result.Write(p.startBlock(BlockTypeThinking, map[string]interface{}{ _, _ = result.Write(p.startBlock(BlockTypeThinking, map[string]any{
"type": "thinking", "type": "thinking",
"thinking": "", "thinking": "",
})) }))
_, _ = result.Write(p.emitDelta("thinking_delta", map[string]interface{}{ _, _ = result.Write(p.emitDelta("thinking_delta", map[string]any{
"thinking": "", "thinking": "",
})) }))
_, _ = result.Write(p.emitDelta("signature_delta", map[string]interface{}{ _, _ = result.Write(p.emitDelta("signature_delta", map[string]any{
"signature": signature, "signature": signature,
})) }))
_, _ = result.Write(p.endBlock()) _, _ = result.Write(p.endBlock())
...@@ -422,9 +422,9 @@ func (p *StreamingProcessor) emitFinish(finishReason string) []byte { ...@@ -422,9 +422,9 @@ func (p *StreamingProcessor) emitFinish(finishReason string) []byte {
OutputTokens: p.outputTokens, OutputTokens: p.outputTokens,
} }
deltaEvent := map[string]interface{}{ deltaEvent := map[string]any{
"type": "message_delta", "type": "message_delta",
"delta": map[string]interface{}{ "delta": map[string]any{
"stop_reason": stopReason, "stop_reason": stopReason,
"stop_sequence": nil, "stop_sequence": nil,
}, },
...@@ -434,7 +434,7 @@ func (p *StreamingProcessor) emitFinish(finishReason string) []byte { ...@@ -434,7 +434,7 @@ func (p *StreamingProcessor) emitFinish(finishReason string) []byte {
_, _ = result.Write(p.formatSSE("message_delta", deltaEvent)) _, _ = result.Write(p.formatSSE("message_delta", deltaEvent))
if !p.messageStopSent { if !p.messageStopSent {
stopEvent := map[string]interface{}{ stopEvent := map[string]any{
"type": "message_stop", "type": "message_stop",
} }
_, _ = result.Write(p.formatSSE("message_stop", stopEvent)) _, _ = result.Write(p.formatSSE("message_stop", stopEvent))
...@@ -445,7 +445,7 @@ func (p *StreamingProcessor) emitFinish(finishReason string) []byte { ...@@ -445,7 +445,7 @@ func (p *StreamingProcessor) emitFinish(finishReason string) []byte {
} }
// formatSSE 格式化 SSE 事件 // formatSSE 格式化 SSE 事件
func (p *StreamingProcessor) formatSSE(eventType string, data interface{}) []byte { func (p *StreamingProcessor) formatSSE(eventType string, data any) []byte {
jsonData, err := json.Marshal(data) jsonData, err := json.Marshal(data)
if err != nil { if err != nil {
return nil return nil
......
...@@ -186,7 +186,7 @@ func (r *accountRepository) BatchUpdateLastUsed(ctx context.Context, updates map ...@@ -186,7 +186,7 @@ func (r *accountRepository) BatchUpdateLastUsed(ctx context.Context, updates map
ids = append(ids, id) ids = append(ids, id)
} }
caseSql += " END WHERE id IN ?" caseSql += " END WHERE id IN ? AND deleted_at IS NULL"
args = append(args, ids) args = append(args, ids)
return r.db.WithContext(ctx).Exec(caseSql, args...).Error return r.db.WithContext(ctx).Exec(caseSql, args...).Error
......
...@@ -119,6 +119,7 @@ func (r *proxyRepository) CountAccountsByProxyID(ctx context.Context, proxyID in ...@@ -119,6 +119,7 @@ func (r *proxyRepository) CountAccountsByProxyID(ctx context.Context, proxyID in
var count int64 var count int64
err := r.db.WithContext(ctx).Table("accounts"). err := r.db.WithContext(ctx).Table("accounts").
Where("proxy_id = ?", proxyID). Where("proxy_id = ?", proxyID).
Where("deleted_at IS NULL").
Count(&count).Error Count(&count).Error
return count, err return count, err
} }
...@@ -134,6 +135,7 @@ func (r *proxyRepository) GetAccountCountsForProxies(ctx context.Context) (map[i ...@@ -134,6 +135,7 @@ func (r *proxyRepository) GetAccountCountsForProxies(ctx context.Context) (map[i
Table("accounts"). Table("accounts").
Select("proxy_id, COUNT(*) as count"). Select("proxy_id, COUNT(*) as count").
Where("proxy_id IS NOT NULL"). Where("proxy_id IS NOT NULL").
Where("deleted_at IS NULL").
Group("proxy_id"). Group("proxy_id").
Scan(&results).Error Scan(&results).Error
if err != nil { if err != nil {
......
...@@ -182,6 +182,7 @@ func (r *usageLogRepository) GetDashboardStats(ctx context.Context) (*DashboardS ...@@ -182,6 +182,7 @@ func (r *usageLogRepository) GetDashboardStats(ctx context.Context) (*DashboardS
COUNT(CASE WHEN rate_limited_at IS NOT NULL AND rate_limit_reset_at > ? THEN 1 END) as ratelimit_accounts, COUNT(CASE WHEN rate_limited_at IS NOT NULL AND rate_limit_reset_at > ? THEN 1 END) as ratelimit_accounts,
COUNT(CASE WHEN overload_until IS NOT NULL AND overload_until > ? THEN 1 END) as overload_accounts COUNT(CASE WHEN overload_until IS NOT NULL AND overload_until > ? THEN 1 END) as overload_accounts
FROM accounts FROM accounts
WHERE deleted_at IS NULL
`, service.StatusActive, service.StatusError, now, now).Scan(&accountStats).Error; err != nil { `, service.StatusActive, service.StatusError, now, now).Scan(&accountStats).Error; err != nil {
return nil, err return nil, err
} }
......
...@@ -56,7 +56,9 @@ func (m *mockAccountRepoForGemini) ListWithFilters(ctx context.Context, params p ...@@ -56,7 +56,9 @@ func (m *mockAccountRepoForGemini) ListWithFilters(ctx context.Context, params p
func (m *mockAccountRepoForGemini) ListByGroup(ctx context.Context, groupID int64) ([]Account, error) { func (m *mockAccountRepoForGemini) ListByGroup(ctx context.Context, groupID int64) ([]Account, error) {
return nil, nil return nil, nil
} }
func (m *mockAccountRepoForGemini) ListActive(ctx context.Context) ([]Account, error) { return nil, nil } func (m *mockAccountRepoForGemini) ListActive(ctx context.Context) ([]Account, error) {
return nil, nil
}
func (m *mockAccountRepoForGemini) ListByPlatform(ctx context.Context, platform string) ([]Account, error) { func (m *mockAccountRepoForGemini) ListByPlatform(ctx context.Context, platform string) ([]Account, error) {
return nil, nil return nil, nil
} }
......
This diff is collapsed.
...@@ -14,13 +14,17 @@ ...@@ -14,13 +14,17 @@
"@vueuse/core": "^10.7.0", "@vueuse/core": "^10.7.0",
"axios": "^1.6.2", "axios": "^1.6.2",
"chart.js": "^4.4.1", "chart.js": "^4.4.1",
"driver.js": "^1.4.0",
"file-saver": "^2.0.5",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.0", "vue": "^3.4.0",
"vue-chartjs": "^5.3.0", "vue-chartjs": "^5.3.0",
"vue-i18n": "^9.14.5", "vue-i18n": "^9.14.5",
"vue-router": "^4.2.5" "vue-router": "^4.2.5",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@types/file-saver": "^2.0.7",
"@types/node": "^20.10.5", "@types/node": "^20.10.5",
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.3",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
......
This diff is collapsed.
import { DriveStep } from 'driver.js'
/**
* 管理员完整引导流程
* 交互式引导:指引用户实际操作
* @param t 国际化函数
* @param isSimpleMode 是否为简易模式(简易模式下会过滤分组相关步骤)
*/
export const getAdminSteps = (t: (key: string) => string, isSimpleMode = false): DriveStep[] => {
const allSteps: DriveStep[] = [
// ========== 欢迎介绍 ==========
{
popover: {
title: t('onboarding.admin.welcome.title'),
description: t('onboarding.admin.welcome.description'),
align: 'center',
nextBtnText: t('onboarding.admin.welcome.nextBtn'),
prevBtnText: t('onboarding.admin.welcome.prevBtn')
}
},
// ========== 第一部分:创建分组 ==========
{
element: '#sidebar-group-manage',
popover: {
title: t('onboarding.admin.groupManage.title'),
description: t('onboarding.admin.groupManage.description'),
side: 'right',
align: 'center',
showButtons: ['close'],
}
},
{
element: '[data-tour="groups-create-btn"]',
popover: {
title: t('onboarding.admin.createGroup.title'),
description: t('onboarding.admin.createGroup.description'),
side: 'bottom',
align: 'end',
showButtons: ['close']
}
},
{
element: '[data-tour="group-form-name"]',
popover: {
title: t('onboarding.admin.groupName.title'),
description: t('onboarding.admin.groupName.description'),
side: 'right',
align: 'start',
showButtons: ['next', 'previous']
}
},
{
element: '[data-tour="group-form-platform"]',
popover: {
title: t('onboarding.admin.groupPlatform.title'),
description: t('onboarding.admin.groupPlatform.description'),
side: 'right',
align: 'start',
showButtons: ['next', 'previous']
}
},
{
element: '[data-tour="group-form-multiplier"]',
popover: {
title: t('onboarding.admin.groupMultiplier.title'),
description: t('onboarding.admin.groupMultiplier.description'),
side: 'right',
align: 'start',
showButtons: ['next', 'previous']
}
},
{
element: '[data-tour="group-form-exclusive"]',
popover: {
title: t('onboarding.admin.groupExclusive.title'),
description: t('onboarding.admin.groupExclusive.description'),
side: 'top',
align: 'start',
showButtons: ['next', 'previous']
}
},
{
element: '[data-tour="group-form-submit"]',
popover: {
title: t('onboarding.admin.groupSubmit.title'),
description: t('onboarding.admin.groupSubmit.description'),
side: 'left',
align: 'center',
showButtons: ['close']
}
},
// ========== 第二部分:创建账号授权 ==========
{
element: '#sidebar-channel-manage',
popover: {
title: t('onboarding.admin.accountManage.title'),
description: t('onboarding.admin.accountManage.description'),
side: 'right',
align: 'center',
showButtons: ['close']
}
},
{
element: '[data-tour="accounts-create-btn"]',
popover: {
title: t('onboarding.admin.createAccount.title'),
description: t('onboarding.admin.createAccount.description'),
side: 'bottom',
align: 'end',
showButtons: ['close']
}
},
{
element: '[data-tour="account-form-name"]',
popover: {
title: t('onboarding.admin.accountName.title'),
description: t('onboarding.admin.accountName.description'),
side: 'right',
align: 'start',
showButtons: ['next', 'previous']
}
},
{
element: '[data-tour="account-form-platform"]',
popover: {
title: t('onboarding.admin.accountPlatform.title'),
description: t('onboarding.admin.accountPlatform.description'),
side: 'right',
align: 'start',
showButtons: ['next', 'previous']
}
},
{
element: '[data-tour="account-form-type"]',
popover: {
title: t('onboarding.admin.accountType.title'),
description: t('onboarding.admin.accountType.description'),
side: 'right',
align: 'start',
showButtons: ['next', 'previous']
}
},
{
element: '[data-tour="account-form-priority"]',
popover: {
title: t('onboarding.admin.accountPriority.title'),
description: t('onboarding.admin.accountPriority.description'),
side: 'top',
align: 'start',
showButtons: ['next', 'previous']
}
},
{
element: '[data-tour="account-form-groups"]',
popover: {
title: t('onboarding.admin.accountGroups.title'),
description: t('onboarding.admin.accountGroups.description'),
side: 'top',
align: 'center',
showButtons: ['next', 'previous']
}
},
{
element: '[data-tour="account-form-submit"]',
popover: {
title: t('onboarding.admin.accountSubmit.title'),
description: t('onboarding.admin.accountSubmit.description'),
side: 'left',
align: 'center',
showButtons: ['close']
}
},
// ========== 第三部分:创建API密钥 ==========
{
element: '[data-tour="sidebar-my-keys"]',
popover: {
title: t('onboarding.admin.keyManage.title'),
description: t('onboarding.admin.keyManage.description'),
side: 'right',
align: 'center',
showButtons: ['close']
}
},
{
element: '[data-tour="keys-create-btn"]',
popover: {
title: t('onboarding.admin.createKey.title'),
description: t('onboarding.admin.createKey.description'),
side: 'bottom',
align: 'end',
showButtons: ['close']
}
},
{
element: '[data-tour="key-form-name"]',
popover: {
title: t('onboarding.admin.keyName.title'),
description: t('onboarding.admin.keyName.description'),
side: 'right',
align: 'start',
showButtons: ['next', 'previous']
}
},
{
element: '[data-tour="key-form-group"]',
popover: {
title: t('onboarding.admin.keyGroup.title'),
description: t('onboarding.admin.keyGroup.description'),
side: 'right',
align: 'start',
showButtons: ['next', 'previous']
}
},
{
element: '[data-tour="key-form-submit"]',
popover: {
title: t('onboarding.admin.keySubmit.title'),
description: t('onboarding.admin.keySubmit.description'),
side: 'left',
align: 'center',
showButtons: ['close']
}
}
]
// 简易模式下过滤分组相关步骤
if (isSimpleMode) {
return allSteps.filter(step => {
const element = step.element as string | undefined
// 过滤掉分组管理和账号分组选择相关步骤
return !element || (
!element.includes('sidebar-group-manage') &&
!element.includes('groups-create-btn') &&
!element.includes('group-form-') &&
!element.includes('account-form-groups')
)
})
}
return allSteps
}
/**
* 普通用户引导流程
*/
export const getUserSteps = (t: (key: string) => string): DriveStep[] => [
{
popover: {
title: t('onboarding.user.welcome.title'),
description: t('onboarding.user.welcome.description'),
align: 'center',
nextBtnText: t('onboarding.user.welcome.nextBtn'),
prevBtnText: t('onboarding.user.welcome.prevBtn')
}
},
{
element: '[data-tour="sidebar-my-keys"]',
popover: {
title: t('onboarding.user.keyManage.title'),
description: t('onboarding.user.keyManage.description'),
side: 'right',
align: 'center',
showButtons: ['close']
}
},
{
element: '[data-tour="keys-create-btn"]',
popover: {
title: t('onboarding.user.createKey.title'),
description: t('onboarding.user.createKey.description'),
side: 'bottom',
align: 'end',
showButtons: ['close']
}
},
{
element: '[data-tour="key-form-name"]',
popover: {
title: t('onboarding.user.keyName.title'),
description: t('onboarding.user.keyName.description'),
side: 'right',
align: 'start',
showButtons: ['next', 'previous']
}
},
{
element: '[data-tour="key-form-group"]',
popover: {
title: t('onboarding.user.keyGroup.title'),
description: t('onboarding.user.keyGroup.description'),
side: 'right',
align: 'start',
showButtons: ['next', 'previous']
}
},
{
element: '[data-tour="key-form-submit"]',
popover: {
title: t('onboarding.user.keySubmit.title'),
description: t('onboarding.user.keySubmit.description'),
side: 'left',
align: 'center',
showButtons: ['close']
}
}
]
...@@ -362,6 +362,10 @@ const resetState = () => { ...@@ -362,6 +362,10 @@ const resetState = () => {
} }
const handleClose = () => { const handleClose = () => {
// 防止在连接测试进行中关闭对话框
if (status.value === 'connecting') {
return
}
closeEventSource() closeEventSource()
emit('close') emit('close')
} }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<BaseDialog <BaseDialog
:show="show" :show="show"
:title="t('admin.accounts.createAccount')" :title="t('admin.accounts.createAccount')"
width="wide" width="normal"
@close="handleClose" @close="handleClose"
> >
<!-- Step Indicator for OAuth accounts --> <!-- Step Indicator for OAuth accounts -->
...@@ -53,13 +53,14 @@ ...@@ -53,13 +53,14 @@
required required
class="input" class="input"
:placeholder="t('admin.accounts.enterAccountName')" :placeholder="t('admin.accounts.enterAccountName')"
data-tour="account-form-name"
/> />
</div> </div>
<!-- Platform Selection - Segmented Control Style --> <!-- Platform Selection - Segmented Control Style -->
<div> <div>
<label class="input-label">{{ t('admin.accounts.platform') }}</label> <label class="input-label">{{ t('admin.accounts.platform') }}</label>
<div class="mt-2 flex rounded-lg bg-gray-100 p-1 dark:bg-dark-700"> <div class="mt-2 flex rounded-lg bg-gray-100 p-1 dark:bg-dark-700" data-tour="account-form-platform">
<button <button
type="button" type="button"
@click="form.platform = 'anthropic'" @click="form.platform = 'anthropic'"
...@@ -166,7 +167,7 @@ ...@@ -166,7 +167,7 @@
<!-- Account Type Selection (Anthropic) --> <!-- Account Type Selection (Anthropic) -->
<div v-if="form.platform === 'anthropic'"> <div v-if="form.platform === 'anthropic'">
<label class="input-label">{{ t('admin.accounts.accountType') }}</label> <label class="input-label">{{ t('admin.accounts.accountType') }}</label>
<div class="mt-2 grid grid-cols-2 gap-3"> <div class="mt-2 grid grid-cols-2 gap-3" data-tour="account-form-type">
<button <button
type="button" type="button"
@click="accountCategory = 'oauth-based'" @click="accountCategory = 'oauth-based'"
...@@ -256,7 +257,7 @@ ...@@ -256,7 +257,7 @@
<!-- Account Type Selection (OpenAI) --> <!-- Account Type Selection (OpenAI) -->
<div v-if="form.platform === 'openai'"> <div v-if="form.platform === 'openai'">
<label class="input-label">{{ t('admin.accounts.accountType') }}</label> <label class="input-label">{{ t('admin.accounts.accountType') }}</label>
<div class="mt-2 grid grid-cols-2 gap-3"> <div class="mt-2 grid grid-cols-2 gap-3" data-tour="account-form-type">
<button <button
type="button" type="button"
@click="accountCategory = 'oauth-based'" @click="accountCategory = 'oauth-based'"
...@@ -338,7 +339,7 @@ ...@@ -338,7 +339,7 @@
<!-- Account Type Selection (Gemini) --> <!-- Account Type Selection (Gemini) -->
<div v-if="form.platform === 'gemini'"> <div v-if="form.platform === 'gemini'">
<label class="input-label">{{ t('admin.accounts.accountType') }}</label> <label class="input-label">{{ t('admin.accounts.accountType') }}</label>
<div class="mt-2 grid grid-cols-2 gap-3"> <div class="mt-2 grid grid-cols-2 gap-3" data-tour="account-form-type">
<button <button
type="button" type="button"
@click="accountCategory = 'oauth-based'" @click="accountCategory = 'oauth-based'"
...@@ -1014,7 +1015,13 @@ ...@@ -1014,7 +1015,13 @@
</div> </div>
<div> <div>
<label class="input-label">{{ t('admin.accounts.priority') }}</label> <label class="input-label">{{ t('admin.accounts.priority') }}</label>
<input v-model.number="form.priority" type="number" min="1" class="input" /> <input
v-model.number="form.priority"
type="number"
min="1"
class="input"
data-tour="account-form-priority"
/>
<p class="input-hint">{{ t('admin.accounts.priorityHint') }}</p> <p class="input-hint">{{ t('admin.accounts.priorityHint') }}</p>
</div> </div>
</div> </div>
...@@ -1056,6 +1063,7 @@ ...@@ -1056,6 +1063,7 @@
:groups="groups" :groups="groups"
:platform="form.platform" :platform="form.platform"
:mixed-scheduling="mixedScheduling" :mixed-scheduling="mixedScheduling"
data-tour="account-form-groups"
/> />
</form> </form>
...@@ -1091,6 +1099,7 @@ ...@@ -1091,6 +1099,7 @@
form="create-account-form" form="create-account-form"
:disabled="submitting" :disabled="submitting"
class="btn btn-primary" class="btn btn-primary"
data-tour="account-form-submit"
> >
<svg <svg
v-if="submitting" v-if="submitting"
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<BaseDialog <BaseDialog
:show="show" :show="show"
:title="t('admin.accounts.editAccount')" :title="t('admin.accounts.editAccount')"
width="wide" width="normal"
@close="handleClose" @close="handleClose"
> >
<form <form
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
> >
<div> <div>
<label class="input-label">{{ t('common.name') }}</label> <label class="input-label">{{ t('common.name') }}</label>
<input v-model="form.name" type="text" required class="input" /> <input v-model="form.name" type="text" required class="input" data-tour="edit-account-form-name" />
</div> </div>
<!-- API Key fields (only for apikey type) --> <!-- API Key fields (only for apikey type) -->
...@@ -457,7 +457,13 @@ ...@@ -457,7 +457,13 @@
</div> </div>
<div> <div>
<label class="input-label">{{ t('admin.accounts.priority') }}</label> <label class="input-label">{{ t('admin.accounts.priority') }}</label>
<input v-model.number="form.priority" type="number" min="1" class="input" /> <input
v-model.number="form.priority"
type="number"
min="1"
class="input"
data-tour="account-form-priority"
/>
</div> </div>
</div> </div>
...@@ -504,6 +510,7 @@ ...@@ -504,6 +510,7 @@
:groups="groups" :groups="groups"
:platform="account?.platform" :platform="account?.platform"
:mixed-scheduling="mixedScheduling" :mixed-scheduling="mixedScheduling"
data-tour="account-form-groups"
/> />
</form> </form>
...@@ -518,6 +525,7 @@ ...@@ -518,6 +525,7 @@
form="edit-account-form" form="edit-account-form"
:disabled="submitting" :disabled="submitting"
class="btn btn-primary" class="btn btn-primary"
data-tour="account-form-submit"
> >
<svg <svg
v-if="submitting" v-if="submitting"
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<BaseDialog <BaseDialog
:show="show" :show="show"
:title="t('admin.accounts.reAuthorizeAccount')" :title="t('admin.accounts.reAuthorizeAccount')"
width="wide" width="normal"
@close="handleClose" @close="handleClose"
> >
<div v-if="account" class="space-y-4"> <div v-if="account" class="space-y-4">
......
...@@ -151,6 +151,10 @@ watch( ...@@ -151,6 +151,10 @@ watch(
) )
const handleClose = () => { const handleClose = () => {
// 防止在同步进行中关闭对话框
if (syncing.value) {
return
}
emit('close') emit('close')
} }
......
<template> <template>
<Teleport to="body"> <Teleport to="body">
<Transition name="modal">
<div <div
v-if="show" v-if="show"
class="modal-overlay" class="modal-overlay"
aria-labelledby="modal-title" :aria-labelledby="dialogId"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
@click.self="handleClose" @click.self="handleClose"
> >
<!-- Modal panel --> <!-- Modal panel -->
<div :class="['modal-content', widthClasses]" @click.stop> <div ref="dialogRef" :class="['modal-content', widthClasses]" @click.stop>
<!-- Header --> <!-- Header -->
<div class="modal-header"> <div class="modal-header">
<h3 id="modal-title" class="modal-title"> <h3 :id="dialogId" class="modal-title">
{{ title }} {{ title }}
</h3> </h3>
<button <button
...@@ -43,11 +44,20 @@ ...@@ -43,11 +44,20 @@
</div> </div>
</div> </div>
</div> </div>
</Transition>
</Teleport> </Teleport>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, watch, onMounted, onUnmounted } from 'vue' import { computed, watch, onMounted, onUnmounted, ref, nextTick } from 'vue'
// 生成唯一ID以避免多个对话框时ID冲突
let dialogIdCounter = 0
const dialogId = `modal-title-${++dialogIdCounter}`
// 焦点管理
const dialogRef = ref<HTMLElement | null>(null)
let previousActiveElement: HTMLElement | null = null
type DialogWidth = 'narrow' | 'normal' | 'wide' | 'extra-wide' | 'full' type DialogWidth = 'narrow' | 'normal' | 'wide' | 'extra-wide' | 'full'
...@@ -72,12 +82,15 @@ const props = withDefaults(defineProps<Props>(), { ...@@ -72,12 +82,15 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<Emits>() const emit = defineEmits<Emits>()
const widthClasses = computed(() => { const widthClasses = computed(() => {
// Width guidance: narrow=confirm/short prompts, normal=standard forms,
// wide=multi-section forms or rich content, extra-wide=analytics/tables,
// full=full-screen or very dense layouts.
const widths: Record<DialogWidth, string> = { const widths: Record<DialogWidth, string> = {
narrow: 'max-w-md', narrow: 'max-w-md',
normal: 'max-w-lg', normal: 'max-w-lg',
wide: 'max-w-4xl', wide: 'w-full sm:max-w-2xl md:max-w-3xl lg:max-w-4xl',
'extra-wide': 'max-w-6xl', 'extra-wide': 'w-full sm:max-w-3xl md:max-w-4xl lg:max-w-5xl xl:max-w-6xl',
full: 'max-w-7xl' full: 'w-full sm:max-w-4xl md:max-w-5xl lg:max-w-6xl xl:max-w-7xl'
} }
return widths[props.width] return widths[props.width]
}) })
...@@ -94,14 +107,31 @@ const handleEscape = (event: KeyboardEvent) => { ...@@ -94,14 +107,31 @@ const handleEscape = (event: KeyboardEvent) => {
} }
} }
// Prevent body scroll when modal is open // Prevent body scroll when modal is open and manage focus
watch( watch(
() => props.show, () => props.show,
(isOpen) => { async (isOpen) => {
if (isOpen) { if (isOpen) {
document.body.style.overflow = 'hidden' // 保存当前焦点元素
previousActiveElement = document.activeElement as HTMLElement
// 使用CSS类而不是直接操作style,更易于管理多个对话框
document.body.classList.add('modal-open')
// 等待DOM更新后设置焦点到对话框
await nextTick()
if (dialogRef.value) {
const firstFocusable = dialogRef.value.querySelector<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
firstFocusable?.focus()
}
} else { } else {
document.body.style.overflow = '' document.body.classList.remove('modal-open')
// 恢复之前的焦点
if (previousActiveElement && typeof previousActiveElement.focus === 'function') {
previousActiveElement.focus()
}
previousActiveElement = null
} }
}, },
{ immediate: true } { immediate: true }
...@@ -113,6 +143,7 @@ onMounted(() => { ...@@ -113,6 +143,7 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('keydown', handleEscape) document.removeEventListener('keydown', handleEscape)
document.body.style.overflow = '' // 确保组件卸载时移除滚动锁定
document.body.classList.remove('modal-open')
}) })
</script> </script>
<template>
<BaseDialog :show="show" :title="t('usage.exporting')" width="narrow" @close="handleCancel">
<div class="space-y-4">
<div class="text-sm text-gray-600 dark:text-gray-400">
{{ t('usage.exportingProgress') }}
</div>
<div class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300">
<span>{{ t('usage.exportedCount', { current, total }) }}</span>
<span class="font-medium text-gray-900 dark:text-white">{{ normalizedProgress }}%</span>
</div>
<div class="h-2 w-full rounded-full bg-gray-200 dark:bg-dark-700">
<div
role="progressbar"
:aria-valuenow="normalizedProgress"
aria-valuemin="0"
aria-valuemax="100"
:aria-label="`${t('usage.exportingProgress')}: ${normalizedProgress}%`"
class="h-2 rounded-full bg-primary-600 transition-all"
:style="{ width: `${normalizedProgress}%` }"
></div>
</div>
<div v-if="estimatedTime" class="text-xs text-gray-500 dark:text-gray-400" aria-live="polite" aria-atomic="true">
{{ t('usage.estimatedTime', { time: estimatedTime }) }}
</div>
</div>
<template #footer>
<button
@click="handleCancel"
type="button"
class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:border-dark-600 dark:bg-dark-700 dark:text-gray-200 dark:hover:bg-dark-600 dark:focus:ring-offset-dark-800"
>
{{ t('usage.cancelExport') }}
</button>
</template>
</BaseDialog>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseDialog from './BaseDialog.vue'
interface Props {
show: boolean
progress: number
current: number
total: number
estimatedTime: string
}
interface Emits {
(e: 'cancel'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const { t } = useI18n()
const normalizedProgress = computed(() => {
const value = Number.isFinite(props.progress) ? props.progress : 0
return Math.min(100, Math.max(0, Math.round(value)))
})
const handleCancel = () => {
emit('cancel')
}
</script>
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