diff --git a/web/src/views/Settings.vue b/web/src/views/Settings.vue index 3f16861..94aa393 100755 --- a/web/src/views/Settings.vue +++ b/web/src/views/Settings.vue @@ -458,6 +458,65 @@ + + + + 用户管理 + + + + + {{ u.username }} + 本人 + + 创建于 {{ formatDate(u.createdAt) }} + + + + 重置密码 + + + + + + + 加载中… + {{ userLoadError }} + + + + + + + {{ creatingUser ? '创建中…' : '添加用户' }} + + + {{ userMutationError }} + + + + + 重置 {{ resetTarget.username }} 的密码 + 该用户的所有会话将被强制下线。 + + 新密码 (≥8 位) + + + {{ resetError }} + + 取消 + + {{ resettingPw ? '保存中…' : '确认重置' }} + + + + + @@ -469,6 +528,7 @@ import AvatarUpload from '../components/AvatarUpload.vue'; import CustomAvatarRow from '../components/CustomAvatarRow.vue'; import QRCode from 'qrcode'; import { usePlayerStore } from '../stores/player.js'; +import { useSession } from '../composables/useSession.js'; const store = usePlayerStore(); @@ -841,11 +901,113 @@ async function updateProfile(botId: string, key: keyof ProfileConfig, value: boo } } +// --- User Management --- +const session = useSession(); + +interface UserListEntry { id: string; username: string; createdAt: number } +const userList = ref([]); +const userLoadError = ref(''); +const userMutationError = ref(''); +const newUser = reactive({ username: '', password: '' }); +const creatingUser = ref(false); +const resetTarget = ref(null); +const resetPassword = ref(''); +const resetError = ref(''); +const resettingPw = ref(false); + +async function loadUsers() { + userLoadError.value = ''; + try { + const res = await fetch('/api/users'); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const body = await res.json(); + userList.value = body.users ?? []; + } catch (e) { + userLoadError.value = (e as Error).message; + } +} + +async function onCreateUser() { + userMutationError.value = ''; + creatingUser.value = true; + try { + const res = await fetch('/api/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: newUser.username, password: newUser.password }), + }); + if (!res.ok) { + const b = await res.json().catch(() => ({})); + throw new Error(b.error ?? `HTTP ${res.status}`); + } + newUser.username = ''; + newUser.password = ''; + await loadUsers(); + } catch (e) { + userMutationError.value = (e as Error).message; + } finally { + creatingUser.value = false; + } +} + +async function onDeleteUser(u: UserListEntry) { + if (!confirm(`确认删除用户 ${u.username}?`)) return; + userMutationError.value = ''; + try { + const res = await fetch(`/api/users/${u.id}`, { method: 'DELETE' }); + if (!res.ok && res.status !== 204) { + const b = await res.json().catch(() => ({})); + throw new Error(b.error ?? `HTTP ${res.status}`); + } + await loadUsers(); + } catch (e) { + userMutationError.value = (e as Error).message; + } +} + +function openResetPassword(u: UserListEntry) { + resetTarget.value = u; + resetPassword.value = ''; + resetError.value = ''; +} + +async function onConfirmReset() { + if (!resetTarget.value) return; + if (resetPassword.value.length < 8) { + resetError.value = '密码至少 8 位'; + return; + } + resettingPw.value = true; + resetError.value = ''; + try { + const res = await fetch(`/api/users/${resetTarget.value.id}/reset-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ newPassword: resetPassword.value }), + }); + if (!res.ok && res.status !== 204) { + const b = await res.json().catch(() => ({})); + throw new Error(b.error ?? `HTTP ${res.status}`); + } + resetTarget.value = null; + } catch (e) { + resetError.value = (e as Error).message; + } finally { + resettingPw.value = false; + } +} + +function formatDate(ms: number): string { + const d = new Date(ms); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; +} + onMounted(() => { store.fetchBots(); // Refresh bot status on page visit checkAuthStatus(); loadQuality(); loadIdleTimeout(); + loadUsers(); }); onUnmounted(() => { @@ -1467,4 +1629,27 @@ onUnmounted(() => { } } } + +// --- User Management --- +.user-list { display: flex; flex-direction: column; gap: 8px; } +.user-item { + display: flex; align-items: center; justify-content: space-between; + padding: 12px; background: var(--bg-secondary); border-radius: var(--radius-sm); +} +.user-info { display: flex; flex-direction: column; gap: 4px; } +.user-name { font-weight: 500; color: var(--text-primary); display: flex; align-items: center; gap: 8px; } +.user-self-badge { + font-size: 11px; padding: 2px 6px; border-radius: 4px; + background: var(--color-primary); color: #fff; +} +.user-created { font-size: 12px; color: var(--text-secondary); } +.user-actions { display: flex; gap: 8px; } +.user-add-form { + display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; +} +.user-add-form .input { flex: 1; min-width: 140px; } +.user-empty, .user-error { font-size: 12px; color: var(--text-secondary); padding: 8px 0; } +.user-error { color: #e26a6a; } +.modal-hint { color: var(--text-secondary); font-size: 12px; margin: 0 0 8px; } +.form-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px; }
{{ userMutationError }}
该用户的所有会话将被强制下线。
{{ resetError }}