ForcePilot/web/src/components/UserManagementComponent.vue
Wenjie Zhang 9ca01763ec refactor(settings): 将设置页面重构为模态框形式并优化样式
重构设置功能为模态框形式,移除原设置页面路由及相关导航链接
新增 SettingsModal 组件集中管理设置功能,优化移动端适配
调整 ModelProvidersComponent 样式,移除冗余按钮和样式
统一用户管理组件样式,优化颜色和布局
2025-12-19 14:41:23 +08:00

726 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="user-management">
<!-- 头部区域 -->
<div class="header-section">
<div class="header-content">
<h3 class="title">用户管理</h3>
<p class="description">管理系统用户请谨慎操作删除用户后该用户将无法登录系统</p>
</div>
<a-button type="primary" @click="showAddUserModal" class="add-btn">
<template #icon><PlusOutlined /></template>
添加用户
</a-button>
</div>
<!-- 主内容区域 -->
<div class="content-section">
<a-spin :spinning="userManagement.loading">
<div v-if="userManagement.error" class="error-message">
<a-alert type="error" :message="userManagement.error" show-icon />
</div>
<div class="cards-container">
<div v-if="userManagement.users.length === 0" class="empty-state">
<a-empty description="暂无用户数据" />
</div>
<div v-else class="user-cards-grid">
<div v-for="user in userManagement.users" :key="user.id" class="user-card">
<div class="card-header">
<div class="user-info">
<div class="user-avatar">
<img v-if="user.avatar" :src="user.avatar" :alt="user.username" class="avatar-img" />
<div v-else class="avatar-placeholder">
{{ user.username.charAt(0).toUpperCase() }}
</div>
</div>
<div class="user-basic-info">
<h4 class="username">{{ user.username }}</h4>
<div class="user-id">ID: {{ user.user_id || '-' }}</div>
</div>
</div>
<a-tag :color="getRoleColor(user.role)" class="role-tag">
{{ getRoleLabel(user.role) }}
</a-tag>
</div>
<div class="card-content">
<div class="info-item">
<span class="info-label">手机号:</span>
<span class="info-value phone-text">{{ user.phone_number || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">创建时间:</span>
<span class="info-value time-text">{{ formatTime(user.created_at) }}</span>
</div>
<div class="info-item">
<span class="info-label">最后登录:</span>
<span class="info-value time-text">{{ formatTime(user.last_login) }}</span>
</div>
</div>
<div class="card-actions">
<a-tooltip title="编辑用户">
<a-button type="text" size="small" @click="showEditUserModal(user)" class="action-btn">
<EditOutlined />
<span>编辑</span>
</a-button>
</a-tooltip>
<a-tooltip title="删除用户">
<a-button
type="text"
size="small"
danger
@click="confirmDeleteUser(user)"
:disabled="user.id === userStore.userId || (user.role === 'superadmin' && userStore.userRole !== 'superadmin')"
class="action-btn"
>
<DeleteOutlined />
<span>删除</span>
</a-button>
</a-tooltip>
</div>
</div>
</div>
</div>
</a-spin>
</div>
<!-- 用户表单模态框 -->
<a-modal
v-model:open="userManagement.modalVisible"
:title="userManagement.modalTitle"
@ok="handleUserFormSubmit"
:confirmLoading="userManagement.loading"
@cancel="userManagement.modalVisible = false"
:maskClosable="false"
width="480px"
class="user-modal"
>
<a-form layout="vertical" class="user-form">
<a-form-item label="用户名" required class="form-item">
<a-input
v-model:value="userManagement.form.username"
placeholder="请输入用户名2-20个字符"
size="large"
@blur="validateAndGenerateUserId"
:maxlength="20"
/>
<div v-if="userManagement.form.usernameError" class="error-text">
{{ userManagement.form.usernameError }}
</div>
</a-form-item>
<!-- 显示自动生成的用户ID -->
<a-form-item v-if="userManagement.form.generatedUserId || userManagement.editMode" label="用户ID" class="form-item">
<a-input
:value="userManagement.form.generatedUserId"
placeholder="自动生成"
size="large"
disabled
:addon-before="userManagement.editMode ? '已存在ID' : '登录ID'"
/>
<div v-if="!userManagement.editMode" class="help-text">此ID将用于登录根据用户名自动生成</div>
<div v-else class="help-text">编辑模式下不能修改用户ID</div>
</a-form-item>
<!-- 手机号字段 -->
<a-form-item label="手机号" class="form-item">
<a-input
v-model:value="userManagement.form.phoneNumber"
placeholder="请输入手机号(可选,可用于登录)"
size="large"
:maxlength="11"
/>
<div v-if="userManagement.form.phoneError" class="error-text">
{{ userManagement.form.phoneError }}
</div>
</a-form-item>
<template v-if="userManagement.editMode">
<div class="password-toggle">
<a-checkbox v-model:checked="userManagement.displayPasswordFields">
修改密码
</a-checkbox>
</div>
</template>
<template v-if="!userManagement.editMode || userManagement.displayPasswordFields">
<a-form-item label="密码" required class="form-item">
<a-input-password
v-model:value="userManagement.form.password"
placeholder="请输入密码"
size="large"
/>
</a-form-item>
<a-form-item label="确认密码" required class="form-item">
<a-input-password
v-model:value="userManagement.form.confirmPassword"
placeholder="请再次输入密码"
size="large"
/>
</a-form-item>
</template>
<a-form-item label="角色" class="form-item">
<a-select v-model:value="userManagement.form.role" size="large">
<a-select-option value="user">普通用户</a-select-option>
<a-select-option value="admin" v-if="userStore.isSuperAdmin">管理员</a-select-option>
<a-select-option value="superadmin" v-if="userStore.isSuperAdmin">超级管理员</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { reactive, onMounted, watch } from 'vue';
import { notification, Modal } from 'ant-design-vue';
import { useUserStore } from '@/stores/user';
import {
DeleteOutlined,
EditOutlined,
PlusOutlined
} from '@ant-design/icons-vue';
import { formatDateTime } from '@/utils/time';
const userStore = useUserStore();
// 用户管理相关状态
const userManagement = reactive({
loading: false,
users: [],
error: null,
modalVisible: false,
modalTitle: '添加用户',
editMode: false,
editUserId: null,
form: {
username: '',
generatedUserId: '', // 自动生成的user_id
phoneNumber: '', // 手机号
password: '',
confirmPassword: '',
role: 'user', // 默认角色
usernameError: '', // 用户名错误信息
phoneError: '' // 手机号错误信息
},
displayPasswordFields: true, // 编辑时是否显示密码字段
});
// 添加验证用户名并生成user_id的函数
const validateAndGenerateUserId = async () => {
const username = userManagement.form.username.trim();
// 清空之前的错误和生成的ID
userManagement.form.usernameError = '';
userManagement.form.generatedUserId = '';
if (!username) {
return;
}
// 在编辑模式下不需要重新生成user_id
if (userManagement.editMode) {
return;
}
try {
const result = await userStore.validateUsernameAndGenerateUserId(username);
userManagement.form.generatedUserId = result.user_id;
} catch (error) {
userManagement.form.usernameError = error.message || '用户名验证失败';
}
};
// 验证手机号格式
const validatePhoneNumber = (phone) => {
if (!phone) {
return true; // 手机号可选
}
// 中国大陆手机号格式验证
const phoneRegex = /^1[3-9]\d{9}$/;
return phoneRegex.test(phone);
};
// 监听密码字段显示状态变化
watch(() => userManagement.displayPasswordFields, (newVal) => {
// 当取消显示密码字段时,清空密码输入
if (!newVal) {
userManagement.form.password = '';
userManagement.form.confirmPassword = '';
}
});
// 监听手机号输入变化
watch(() => userManagement.form.phoneNumber, (newPhone) => {
userManagement.form.phoneError = '';
if (newPhone && !validatePhoneNumber(newPhone)) {
userManagement.form.phoneError = '请输入正确的手机号格式';
}
});
// 格式化时间显示
const formatTime = (timeStr) => formatDateTime(timeStr);
// 获取用户列表
const fetchUsers = async () => {
try {
userManagement.loading = true;
const users = await userStore.getUsers();
userManagement.users = users;
userManagement.error = null;
} catch (error) {
console.error('获取用户列表失败:', error);
userManagement.error = '获取用户列表失败';
} finally {
userManagement.loading = false;
}
};
// 打开添加用户模态框
const showAddUserModal = () => {
userManagement.modalTitle = '添加用户';
userManagement.editMode = false;
userManagement.editUserId = null;
userManagement.form = {
username: '',
generatedUserId: '',
phoneNumber: '',
password: '',
confirmPassword: '',
role: 'user', // 默认角色为普通用户
usernameError: '',
phoneError: ''
};
userManagement.displayPasswordFields = true;
userManagement.modalVisible = true;
};
// 打开编辑用户模态框
const showEditUserModal = (user) => {
userManagement.modalTitle = '编辑用户';
userManagement.editMode = true;
userManagement.editUserId = user.id;
userManagement.form = {
username: user.username,
generatedUserId: user.user_id || '', // 编辑模式显示现有的user_id
phoneNumber: user.phone_number || '',
password: '',
confirmPassword: '',
role: user.role,
usernameError: '',
phoneError: ''
};
userManagement.displayPasswordFields = false; // 默认不显示密码字段
userManagement.modalVisible = true;
};
// 处理用户表单提交
const handleUserFormSubmit = async () => {
try {
// 简单验证
if (!userManagement.form.username.trim()) {
notification.error({ message: '用户名不能为空' });
return;
}
// 验证用户名长度
if (userManagement.form.username.trim().length < 2 || userManagement.form.username.trim().length > 20) {
notification.error({ message: '用户名长度必须在 2-20 个字符之间' });
return;
}
// 验证手机号
if (userManagement.form.phoneNumber && !validatePhoneNumber(userManagement.form.phoneNumber)) {
notification.error({ message: '请输入正确的手机号格式' });
return;
}
if (userManagement.displayPasswordFields) {
if (!userManagement.form.password) {
notification.error({ message: '密码不能为空' });
return;
}
if (userManagement.form.password !== userManagement.form.confirmPassword) {
notification.error({ message: '两次输入的密码不一致' });
return;
}
}
userManagement.loading = true;
// 根据模式决定创建还是更新用户
if (userManagement.editMode) {
// 创建更新数据对象
const updateData = {
username: userManagement.form.username.trim(),
role: userManagement.form.role
};
// 添加手机号字段
if (userManagement.form.phoneNumber) {
updateData.phone_number = userManagement.form.phoneNumber;
}
// 如果显示了密码字段并且填写了密码,才更新密码
if (userManagement.displayPasswordFields && userManagement.form.password) {
updateData.password = userManagement.form.password;
}
await userStore.updateUser(userManagement.editUserId, updateData);
notification.success({ message: '用户更新成功' });
} else {
// 创建新用户
const createData = {
username: userManagement.form.username.trim(),
password: userManagement.form.password,
role: userManagement.form.role
};
// 添加手机号字段(如果填写了)
if (userManagement.form.phoneNumber) {
createData.phone_number = userManagement.form.phoneNumber;
}
await userStore.createUser(createData);
notification.success({ message: '用户创建成功' });
}
// 重新获取用户列表
await fetchUsers();
userManagement.modalVisible = false;
} catch (error) {
console.error('用户操作失败:', error);
notification.error({
message: '操作失败',
description: error.message || '请稍后重试'
});
} finally {
userManagement.loading = false;
}
};
// 删除用户
const confirmDeleteUser = (user) => {
// 自己不能删除自己
if (user.id === userStore.userId) {
notification.error({ message: '不能删除自己的账户' });
return;
}
// 确认对话框
Modal.confirm({
title: '确认删除用户',
content: `确定要删除用户 "${user.username}" 吗?此操作不可撤销。`,
okText: '删除',
okType: 'danger',
cancelText: '取消',
async onOk() {
try {
userManagement.loading = true;
await userStore.deleteUser(user.id);
notification.success({ message: '用户删除成功' });
// 重新获取用户列表
await fetchUsers();
} catch (error) {
console.error('删除用户失败:', error);
notification.error({
message: '删除失败',
description: error.message || '请稍后重试'
});
} finally {
userManagement.loading = false;
}
}
});
};
// 角色显示辅助函数
const getRoleLabel = (role) => {
switch (role) {
case 'superadmin': return '超级管理员';
case 'admin': return '管理员';
case 'user': return '普通用户';
default: return role;
}
};
// 角色标签颜色
const getRoleColor = (role) => {
switch (role) {
case 'superadmin': return 'red';
case 'admin': return 'blue';
case 'user': return 'green';
default: return 'default';
}
};
// 在组件挂载时获取用户列表
onMounted(() => {
fetchUsers();
});
</script>
<style lang="less" scoped>
.user-management {
margin-top: 20px;
min-height: 50vh;
.header-section {
display: flex;
justify-content: space-between;
align-items: flex-start;
.header-content {
flex: 1;
.description {
font-size: 14px;
color: var(--gray-600);
margin: 0;
line-height: 1.4;
margin-bottom: 16px;
}
}
}
.content-section {
overflow: hidden;
.error-message {
padding: 16px 24px;
}
.cards-container {
.empty-state {
padding: 60px 20px;
text-align: center;
}
.user-cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
// padding: 16px;
.user-card {
background: var(--gray-0);
border: 1px solid var(--gray-150);
border-radius: 8px;
padding: 16px;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border-color: var(--gray-200);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
.user-info {
display: flex;
align-items: center;
gap: 12px;
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--gray-50);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
color: var(--gray-600);
font-weight: 500;
font-size: 14px;
}
}
.user-basic-info {
.username {
margin: 0 0 2px 0;
font-size: 15px;
font-weight: 600;
color: var(--gray-900);
}
.user-id {
font-size: 12px;
color: var(--gray-600);
font-family: 'Monaco', 'Consolas', monospace;
}
}
}
.role-tag {
font-weight: 500;
border-radius: 4px;
padding: 2px 8px;
font-size: 12px;
}
}
.card-content {
margin-bottom: 12px;
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 2px 0;
border-bottom: 1px solid var(--gray-25);
&:last-child {
border-bottom: none;
}
.info-label {
font-size: 12px;
color: var(--gray-600);
font-weight: 500;
min-width: 70px;
}
.info-value {
font-size: 12px;
color: var(--gray-900);
text-align: right;
flex: 1;
&.time-text {
color: var(--gray-700);
}
&.phone-text {
font-family: 'Monaco', 'Consolas', monospace;
}
}
}
}
.card-actions {
display: flex;
justify-content: flex-end;
gap: 6px;
padding-top: 8px;
border-top: 1px solid var(--gray-25);
.action-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border-radius: 6px;
transition: all 0.2s ease;
font-size: 12px;
span {
font-size: 12px;
}
&:hover {
background: var(--gray-25);
}
&.ant-btn-dangerous:hover {
background: var(--gray-25);
border-color: var(--color-error-500);
color: var(--color-error-500);
}
}
}
}
}
}
}
.time-text {
font-size: 13px;
color: var(--gray-700);
}
.phone-text, .user-id-text {
font-size: 13px;
color: var(--gray-900);
font-family: 'Monaco', 'Consolas', monospace;
}
}
.user-modal {
:deep(.ant-modal-header) {
padding: 20px 24px;
border-bottom: 1px solid var(--gray-150);
.ant-modal-title {
font-size: 16px;
font-weight: 600;
color: var(--gray-900);
}
}
:deep(.ant-modal-body) {
padding: 24px;
}
.user-form {
.form-item {
margin-bottom: 20px;
:deep(.ant-form-item-label) {
padding-bottom: 4px;
label {
font-weight: 500;
color: var(--gray-900);
}
}
}
.error-text {
color: var(--color-error-500);
font-size: 12px;
margin-top: 4px;
line-height: 1.3;
}
.help-text {
color: var(--gray-600);
font-size: 12px;
margin-top: 4px;
line-height: 1.3;
}
.password-toggle {
margin-bottom: 16px;
:deep(.ant-checkbox-wrapper) {
font-weight: 500;
color: var(--gray-600);
}
}
}
}
</style>