From 43f2ad9edb7f8488955f68790c5e1ef0fcbbfe6a Mon Sep 17 00:00:00 2001 From: stefanfeng Date: Fri, 7 Aug 2026 17:07:21 +0800 Subject: [PATCH] feat: add takeover config UI to authorization management page - Added updateTakeoverConfig API function (PUT /api/avatar/{id}/authorizations/takeover) - Added expandable takeover config section per auth card with toggle switch, mode selector (immediate/delayed), and delay seconds input - Added takeover summary badge showing current config when not editing - Styles follow existing light theme with indigo (#6366F1) accent --- digital-avatar-app/src/api/index.ts | 9 + .../src/views/AuthorizationManage.vue | 248 +++++++++++++++++- 2 files changed, 256 insertions(+), 1 deletion(-) diff --git a/digital-avatar-app/src/api/index.ts b/digital-avatar-app/src/api/index.ts index 47acede..cd92827 100644 --- a/digital-avatar-app/src/api/index.ts +++ b/digital-avatar-app/src/api/index.ts @@ -299,4 +299,13 @@ export const getCurrentUser = () => export const logoutUser = () => request.post('/huihui/logout') +// 更新接管配置 +export const updateTakeoverConfig = (avatarId: string, data: { + authorizationId: string + takeoverEnabled: boolean + takeoverMode?: 'immediate' | 'delayed' + takeoverDelaySeconds?: number +}) => + request.put(`/avatar/${avatarId}/authorizations/takeover`, data) + export default request diff --git a/digital-avatar-app/src/views/AuthorizationManage.vue b/digital-avatar-app/src/views/AuthorizationManage.vue index d805da6..6f33ecb 100644 --- a/digital-avatar-app/src/views/AuthorizationManage.vue +++ b/digital-avatar-app/src/views/AuthorizationManage.vue @@ -30,6 +30,46 @@ {{ auth.status === 'active' ? '撤销' : '授权' }} + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ 已启用 + {{ auth.takeoverMode === 'immediate' ? '立即接管' : '延迟 ' + (auth.takeoverDelaySeconds || 30) + 's' }} +
+
@@ -47,7 +87,7 @@ import { ref, onMounted } from 'vue' import { useRouter } from 'vue-router' import { useAvatarStore } from '@/store/avatar' -import { getAuthorizationList, updateAuthorization } from '@/api' +import { getAuthorizationList, updateAuthorization, updateTakeoverConfig } from '@/api' import { pickAvatarId, unwrapListData } from '@/utils/avatar-page-data.js' const router = useRouter() @@ -60,8 +100,14 @@ const authList = ref>([]) +// 接管配置编辑状态(每行一个) +const editingTakeover = ref>({}) + // 从后端加载授权列表 const loadAuth = async () => { try { @@ -137,6 +183,38 @@ const goBack = () => { router.back() } +// 切换接管配置编辑状态 +const toggleTakeoverEdit = (auth: any) => { + editingTakeover.value[auth.id] = !editingTakeover.value[auth.id] +} + +// 保存接管配置 +const saveTakeoverConfig = async (auth: any) => { + const avatarId = pickAvatarId(avatarStore.currentAvatarId, avatarStore.avatars) + if (!avatarId) return + + const delay = auth.takeoverMode === 'delayed' ? (auth.takeoverDelaySeconds ?? 30) : undefined + + try { + await updateTakeoverConfig(avatarId, { + authorizationId: auth.id, + takeoverEnabled: auth.takeoverEnabled ?? false, + takeoverMode: auth.takeoverMode || 'immediate', + takeoverDelaySeconds: delay, + }) + editingTakeover.value[auth.id] = false + } catch (e: any) { + alert('保存接管配置失败:' + (e?.message || '请重试')) + } +} + +// 取消编辑接管配置 +const cancelTakeoverEdit = (auth: any) => { + // 重新加载以恢复原始值 + loadAuth() + editingTakeover.value[auth.id] = false +} + onMounted(() => { loadAuth() }) @@ -321,4 +399,172 @@ onMounted(() => { font-weight: 600; cursor: pointer; } + +/* 接管配置 */ +.takeover-section { + margin-top: 8px; + padding-top: 12px; + border-top: 1px solid #F3F4F6; + width: 100%; +} + +.takeover-toggle-btn { + background: none; + border: none; + color: #6366F1; + font-size: 12px; + font-weight: 500; + cursor: pointer; + padding: 4px 0; +} + +.takeover-form { + margin-top: 10px; + display: flex; + flex-direction: column; + gap: 10px; + background: #FAFBFC; + border-radius: 8px; + padding: 12px; +} + +.takeover-row { + display: flex; + align-items: center; + gap: 10px; +} + +.takeover-label { + font-size: 13px; + color: #374151; + font-weight: 500; + min-width: 72px; + flex-shrink: 0; +} + +/* 切换开关 */ +.toggle-switch { + position: relative; + display: inline-block; + width: 40px; + height: 22px; + flex-shrink: 0; +} + +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: absolute; + cursor: pointer; + inset: 0; + background: #D1D5DB; + border-radius: 22px; + transition: background 0.2s; +} + +.toggle-slider::before { + content: ''; + position: absolute; + height: 16px; + width: 16px; + left: 3px; + bottom: 3px; + background: white; + border-radius: 50%; + transition: transform 0.2s; +} + +.toggle-switch input:checked + .toggle-slider { + background: #6366F1; +} + +.toggle-switch input:checked + .toggle-slider::before { + transform: translateX(18px); +} + +.takeover-select { + flex: 1; + padding: 6px 10px; + border: 1px solid #E5E7EB; + border-radius: 6px; + font-size: 13px; + color: #374151; + background: white; + outline: none; +} + +.takeover-select:disabled { + background: #F3F4F6; + color: #9CA3AF; +} + +.takeover-input { + flex: 1; + padding: 6px 10px; + border: 1px solid #E5E7EB; + border-radius: 6px; + font-size: 13px; + color: #374151; + background: white; + outline: none; + width: 80px; +} + +.takeover-actions { + display: flex; + gap: 8px; + justify-content: flex-end; + margin-top: 4px; +} + +.takeover-cancel-btn { + padding: 6px 16px; + border-radius: 6px; + font-size: 12px; + font-weight: 500; + cursor: pointer; + border: 1px solid #E5E7EB; + background: white; + color: #6B7280; +} + +.takeover-save-btn { + padding: 6px 16px; + border-radius: 6px; + font-size: 12px; + font-weight: 500; + cursor: pointer; + border: none; + background: #6366F1; + color: white; +} + +/* 接管配置摘要 */ +.takeover-summary { + display: flex; + align-items: center; + gap: 8px; + margin-top: 6px; +} + +.takeover-badge { + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; +} + +.takeover-badge.enabled { + background: #EEF2FF; + color: #6366F1; +} + +.takeover-mode-text { + font-size: 12px; + color: #6B7280; +}