Files
geMoldInsight/frontend/src/modules/users/UsersView.vue
T

260 lines
8.0 KiB
Vue
Raw Normal View History

2026-05-29 17:30:26 +08:00
<template>
<div class="page-container">
<div class="page-header">
<div>
<h1>用户管理</h1>
<p>管理系统用户和权限</p>
</div>
<button class="btn btn-primary" @click="openUserModal()">+ 添加用户</button>
</div>
<div v-if="state.loading" class="loading-state">
<div class="loading-spinner"></div>
<span>加载中...</span>
</div>
<div v-else class="table-container">
<table class="data-table">
<thead>
<tr>
<th>用户名</th>
<th>邮箱</th>
<th>姓名</th>
<th>状态</th>
<th>角色</th>
<th>注册时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in state.users" :key="user.id">
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>{{ user.full_name || '-' }}</td>
<td>
<span :class="['badge', user.is_active ? 'badge-success' : 'badge-error']">
{{ user.is_active ? '正常' : '禁用' }}
</span>
</td>
<td>
<span v-for="role in (user as any).roles" :key="role" class="badge badge-info" style="margin-right: 4px;">
{{ role }}
</span>
</td>
<td>{{ formatDateTime(user.created_at) }}</td>
<td>
<div class="action-buttons" style="display:flex; gap:4px;">
<button class="btn btn-sm" style="background:var(--primary-500);color:white;" @click="openUserModal(user)">编辑</button>
<button class="btn btn-sm" style="background:#eab308;color:white;" @click="resetPassword(user)">重置密码</button>
<button v-if="(user as any).id !== storeUser?.id" class="btn btn-sm" style="background:var(--error);color:white;" @click="deleteUser(user)">删除</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="state.showUserModal" class="modal-overlay" @click.self="state.showUserModal = false">
<div class="modal-content">
<div class="modal-header">
<h2>{{ state.editingUser ? '编辑用户' : '添加用户' }}</h2>
<button class="modal-close" @click="state.showUserModal = false">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">用户名</label>
<input v-model="state.userForm.username" type="text" class="form-input" :disabled="!!state.editingUser" />
</div>
<div class="form-group">
<label class="form-label">邮箱</label>
<input v-model="state.userForm.email" type="email" class="form-input" />
</div>
<div class="form-group" v-if="!state.editingUser">
<label class="form-label">密码</label>
<input v-model="state.userForm.password" type="password" class="form-input" />
</div>
<div class="form-group">
<label class="form-label">姓名</label>
<input v-model="state.userForm.full_name" type="text" class="form-input" />
</div>
<div class="form-group">
<label class="form-label">角色</label>
<div class="checkbox-group" style="flex-direction: column; gap: 8px;">
<label v-for="role in state.roles" :key="(role as any).id" class="checkbox-label">
<input type="checkbox" :value="(role as any).id" v-model="state.userForm.role_ids" class="checkbox-input" />
{{ (role as any).name }}
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="state.showUserModal = false">取消</button>
<button class="btn btn-primary" @click="saveUser">保存</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { apiRequest } from '@/shared/api'
import { useAppStore } from '@/stores/app'
import { addNotification } from '@/shared/notification'
import { formatDateTime } from '@/shared/utils'
const router = useRouter()
const store = useAppStore()
const storeUser = store.user
interface UserItem {
id: number
username: string
email: string
full_name: string | null
is_active: boolean
roles: string[]
created_at: string
}
interface RoleItem {
id: number
code: string
name: string
}
const state = reactive({
users: [] as UserItem[],
roles: [] as RoleItem[],
loading: true,
showUserModal: false,
editingUser: null as UserItem | null,
userForm: {
username: '',
email: '',
password: '',
full_name: '',
role_ids: [] as number[],
},
})
const loadUsers = async () => {
try {
state.users = await apiRequest<UserItem[]>('/api/auth/users')
} catch (e) {
const err = e as Error
addNotification(err.message || '加载用户列表失败', 'error')
} finally {
state.loading = false
}
}
const loadRoles = async () => {
try {
state.roles = await apiRequest<RoleItem[]>('/api/auth/roles')
} catch (e) {
const err = e as Error
addNotification(err.message || '加载角色列表失败', 'error')
}
}
const openUserModal = (user: UserItem | null = null) => {
state.editingUser = user
if (user) {
state.userForm = {
username: user.username,
email: user.email,
password: '',
full_name: user.full_name || '',
role_ids: user.roles
.map(r => {
const role = state.roles.find(role => role.code === r)
return role ? role.id : null
})
.filter(id => id !== null) as number[],
}
} else {
state.userForm = { username: '', email: '', password: '', full_name: '', role_ids: [] }
}
state.showUserModal = true
}
const saveUser = async () => {
if (!state.userForm.username || !state.userForm.email) {
addNotification('请填写用户名和邮箱', 'error')
return
}
if (!state.editingUser && !state.userForm.password) {
addNotification('请填写密码', 'error')
return
}
try {
if (state.editingUser) {
await apiRequest(`/api/auth/users/${(state.editingUser as UserItem).id}`, {
method: 'PUT',
body: JSON.stringify({
email: state.userForm.email,
full_name: state.userForm.full_name || null,
role_ids: state.userForm.role_ids,
}),
})
addNotification('用户更新成功', 'success')
} else {
await apiRequest('/api/auth/users', {
method: 'POST',
body: JSON.stringify(state.userForm),
})
addNotification('用户创建成功', 'success')
}
state.showUserModal = false
loadUsers()
} catch (e) {
const err = e as Error
addNotification(err.message || '保存用户失败', 'error')
}
}
const deleteUser = async (user: UserItem) => {
if (!confirm(`确定要删除用户 ${user.username} 吗?`)) return
try {
await apiRequest(`/api/auth/users/${user.id}`, { method: 'DELETE' })
addNotification('用户已删除', 'success')
loadUsers()
} catch (e) {
const err = e as Error
addNotification(err.message || '删除用户失败', 'error')
}
}
const resetPassword = async (user: UserItem) => {
const newPassword = prompt(`请输入 ${user.username} 的新密码:`)
if (!newPassword || newPassword.length < 6) {
addNotification('密码长度至少6位', 'error')
return
}
try {
await apiRequest(`/api/auth/users/${user.id}/reset-password`, {
method: 'PUT',
body: JSON.stringify(newPassword),
})
addNotification('密码已重置', 'success')
} catch (e) {
const err = e as Error
addNotification(err.message || '重置密码失败', 'error')
}
}
onMounted(async () => {
if (!store.user?.is_superuser) {
router.push('/')
return
}
await loadRoles()
loadUsers()
})
</script>