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
This commit is contained in:
stefanfeng
2026-08-07 17:07:21 +08:00
parent 8e365e63e1
commit 43f2ad9edb
2 changed files with 256 additions and 1 deletions
+9
View File
@@ -299,4 +299,13 @@ export const getCurrentUser = () =>
export const logoutUser = () => export const logoutUser = () =>
request.post('/huihui/logout') 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 export default request
@@ -30,6 +30,46 @@
{{ auth.status === 'active' ? '撤销' : '授权' }} {{ auth.status === 'active' ? '撤销' : '授权' }}
</button> </button>
</div> </div>
<!-- 接管配置 -->
<div class="takeover-section">
<button class="takeover-toggle-btn" @click="toggleTakeoverEdit(auth)">
{{ editingTakeover[auth.id] ? '收起配置' : '配置接管' }}
</button>
<div class="takeover-form" v-if="editingTakeover[auth.id]">
<div class="takeover-row">
<label class="takeover-label">启用接管</label>
<label class="toggle-switch">
<input type="checkbox" v-model="auth.takeoverEnabled" />
<span class="toggle-slider"></span>
</label>
</div>
<div class="takeover-row">
<label class="takeover-label">接管模式</label>
<select class="takeover-select" v-model="auth.takeoverMode" :disabled="!auth.takeoverEnabled">
<option value="immediate">立即接管</option>
<option value="delayed">延迟接管</option>
</select>
</div>
<div class="takeover-row" v-if="auth.takeoverMode === 'delayed' && auth.takeoverEnabled">
<label class="takeover-label">延迟时间(秒)</label>
<input
type="number"
class="takeover-input"
v-model.number="auth.takeoverDelaySeconds"
min="5"
placeholder="30"
/>
</div>
<div class="takeover-actions">
<button class="takeover-cancel-btn" @click="cancelTakeoverEdit(auth)">取消</button>
<button class="takeover-save-btn" @click="saveTakeoverConfig(auth)">保存</button>
</div>
</div>
<div class="takeover-summary" v-else-if="auth.takeoverEnabled">
<span class="takeover-badge enabled">已启用</span>
<span class="takeover-mode-text">{{ auth.takeoverMode === 'immediate' ? '立即接管' : '延迟 ' + (auth.takeoverDelaySeconds || 30) + 's' }}</span>
</div>
</div>
</div> </div>
</section> </section>
@@ -47,7 +87,7 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useAvatarStore } from '@/store/avatar' 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' import { pickAvatarId, unwrapListData } from '@/utils/avatar-page-data.js'
const router = useRouter() const router = useRouter()
@@ -60,8 +100,14 @@ const authList = ref<Array<{
targetName: string targetName: string
permissions: string[] permissions: string[]
status: 'active' | 'inactive' status: 'active' | 'inactive'
takeoverEnabled?: boolean
takeoverMode?: 'immediate' | 'delayed'
takeoverDelaySeconds?: number
}>>([]) }>>([])
// 接管配置编辑状态(每行一个)
const editingTakeover = ref<Record<string, boolean>>({})
// 从后端加载授权列表 // 从后端加载授权列表
const loadAuth = async () => { const loadAuth = async () => {
try { try {
@@ -137,6 +183,38 @@ const goBack = () => {
router.back() 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(() => { onMounted(() => {
loadAuth() loadAuth()
}) })
@@ -321,4 +399,172 @@ onMounted(() => {
font-weight: 600; font-weight: 600;
cursor: pointer; 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;
}
</style> </style>