2026-05-29 17:30:26 +08:00
|
|
|
<template>
|
|
|
|
|
<div class="page-container">
|
|
|
|
|
<div class="page-header">
|
|
|
|
|
<div>
|
|
|
|
|
<h1>用户管理</h1>
|
|
|
|
|
<p>管理系统用户和权限</p>
|
|
|
|
|
</div>
|
2026-06-10 11:21:17 +08:00
|
|
|
<t-button theme="primary" @click="openUserModal()">+ 添加用户</t-button>
|
2026-05-29 17:30:26 +08:00
|
|
|
</div>
|
|
|
|
|
|
2026-06-10 11:21:17 +08:00
|
|
|
<t-loading :loading="state.loading" size="large">
|
|
|
|
|
<t-table :data="state.users" row-key="id" stripe>
|
|
|
|
|
<t-table-column colKey="username" title="用户名" />
|
|
|
|
|
<t-table-column colKey="email" title="邮箱" />
|
|
|
|
|
<t-table-column colKey="full_name" title="姓名">
|
|
|
|
|
<template #cell="{ row }">{{ row.full_name || '-' }}</template>
|
|
|
|
|
</t-table-column>
|
|
|
|
|
<t-table-column colKey="is_active" title="状态">
|
|
|
|
|
<template #cell="{ row }">
|
|
|
|
|
<t-tag :theme="row.is_active ? 'success' : 'danger'">
|
|
|
|
|
{{ row.is_active ? '正常' : '禁用' }}
|
|
|
|
|
</t-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</t-table-column>
|
|
|
|
|
<t-table-column colKey="roles" title="角色">
|
|
|
|
|
<template #cell="{ row }">
|
|
|
|
|
<t-tag v-for="role in row.roles" :key="role" theme="primary" variant="light" style="margin-right: 4px;">
|
|
|
|
|
{{ role }}
|
|
|
|
|
</t-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</t-table-column>
|
|
|
|
|
<t-table-column colKey="created_at" title="注册时间">
|
|
|
|
|
<template #cell="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
|
|
|
|
</t-table-column>
|
|
|
|
|
<t-table-column colKey="actions" title="操作">
|
|
|
|
|
<template #cell="{ row }">
|
|
|
|
|
<t-space :size="4">
|
|
|
|
|
<t-button theme="primary" size="small" @click="openUserModal(row)">编辑</t-button>
|
|
|
|
|
<t-button theme="warning" size="small" @click="resetPassword(row)">重置密码</t-button>
|
|
|
|
|
<t-button v-if="row.id !== storeUser?.id" theme="danger" size="small" @click="deleteUser(row)">删除</t-button>
|
|
|
|
|
</t-space>
|
|
|
|
|
</template>
|
|
|
|
|
</t-table-column>
|
|
|
|
|
</t-table>
|
|
|
|
|
<t-empty v-if="!state.loading && state.users.length === 0" description="暂无用户数据" />
|
|
|
|
|
</t-loading>
|
2026-05-29 17:30:26 +08:00
|
|
|
|
2026-06-10 11:21:17 +08:00
|
|
|
<t-dialog
|
|
|
|
|
v-model:visible="state.showUserModal"
|
|
|
|
|
:header="state.editingUser ? '编辑用户' : '添加用户'"
|
|
|
|
|
:close-on-overlay-click="true"
|
|
|
|
|
width="520px"
|
|
|
|
|
>
|
|
|
|
|
<t-form label-width="80px">
|
|
|
|
|
<t-form-item label="用户名">
|
|
|
|
|
<t-input v-model="state.userForm.username" :disabled="!!state.editingUser" />
|
|
|
|
|
</t-form-item>
|
|
|
|
|
<t-form-item label="邮箱">
|
|
|
|
|
<t-input v-model="state.userForm.email" type="email" />
|
|
|
|
|
</t-form-item>
|
|
|
|
|
<t-form-item v-if="!state.editingUser" label="密码">
|
|
|
|
|
<t-input v-model="state.userForm.password" type="password" />
|
|
|
|
|
</t-form-item>
|
|
|
|
|
<t-form-item label="姓名">
|
|
|
|
|
<t-input v-model="state.userForm.full_name" />
|
|
|
|
|
</t-form-item>
|
|
|
|
|
<t-form-item label="角色">
|
|
|
|
|
<div style="display: flex; flex-direction: column; gap: 8px;">
|
|
|
|
|
<label v-for="role in state.roles" :key="role.id" style="display: flex; align-items: center; gap: 6px; cursor: pointer;">
|
|
|
|
|
<input type="checkbox" :value="role.id" v-model="state.userForm.role_ids" />
|
|
|
|
|
{{ role.name }}
|
|
|
|
|
</label>
|
2026-05-29 17:30:26 +08:00
|
|
|
</div>
|
2026-06-10 11:21:17 +08:00
|
|
|
</t-form-item>
|
|
|
|
|
</t-form>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<t-button theme="default" @click="state.showUserModal = false">取消</t-button>
|
|
|
|
|
<t-button theme="primary" @click="saveUser">保存</t-button>
|
|
|
|
|
</template>
|
|
|
|
|
</t-dialog>
|
2026-05-29 17:30:26 +08:00
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { reactive, onMounted } from 'vue'
|
|
|
|
|
import { useRouter } from 'vue-router'
|
2026-06-10 11:21:17 +08:00
|
|
|
import { DialogPlugin } from 'tdesign-vue-next'
|
2026-05-29 17:30:26 +08:00
|
|
|
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[],
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-06-10 11:21:17 +08:00
|
|
|
function confirmDialog(header: string, body: string, theme: string, confirmText: string, cancelText: string): Promise<boolean> {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const dlg = DialogPlugin.confirm({
|
|
|
|
|
header,
|
|
|
|
|
body,
|
|
|
|
|
theme: theme as any,
|
|
|
|
|
confirmBtn: confirmText,
|
|
|
|
|
cancelBtn: cancelText,
|
|
|
|
|
onConfirm: () => { dlg.hide(); resolve(true) },
|
|
|
|
|
onCancel: () => { dlg.hide(); resolve(false) },
|
|
|
|
|
onClose: () => { resolve(false) },
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 17:30:26 +08:00
|
|
|
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) => {
|
2026-06-10 11:21:17 +08:00
|
|
|
if (!await confirmDialog('删除确认', `确定要删除用户 ${user.username} 吗?`, 'warning', '删除', '取消')) return
|
2026-05-29 17:30:26 +08:00
|
|
|
|
|
|
|
|
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>
|