Implement comprehensive user management functionality with API integration

This commit is contained in:
wzclm 2025-02-20 21:41:57 +08:00
parent bb1acb50c1
commit fb5ad57dc1
7 changed files with 591 additions and 92 deletions

5
.env
View File

@ -19,4 +19,7 @@ VITE_BUILD_GZIP=false
VITE_BUILD_BROTLI=false
# 是否删除 console
VITE_DROP_CONSOLE=true
VITE_DROP_CONSOLE=true
# 后端 API 的基础 URL
VITE_API_BASE_URL=http://localhost:3000

View File

@ -0,0 +1,52 @@
import request from '@/utils/request'
/**
* 获取用户列表
* @returns {Promise} 返回用户列表数据
*/
export function getUserList() {
return request.get('/api/users')
}
/**
* 新增用户
* @param {Object} data - 用户数据
* @param {string} data.username - 用户名必选
* @param {string} data.password - 密码必选
* @param {string} data.role_id - 角色ID必选
* @param {string} [data.real_name] - 真实姓名可选
* @param {string} [data.email] - 邮箱可选
* @param {string} [data.phone] - 手机号可选
* @param {string} [data.status] - 状态可选
* @param {string} [data.expire_time] - 过期时间可选
* @returns {Promise} 返回创建结果
*/
export function createUser(data) {
return request.post('/api/users', data)
}
/**
* 编辑用户
* @param {string|number} id - 用户ID
* @param {Object} data - 用户数据
* @param {string} [data.real_name] - 真实姓名可选
* @param {string} [data.email] - 邮箱可选
* @param {string} [data.phone] - 手机号可选
* @param {string} [data.role_id] - 角色ID可选
* @param {string} [data.status] - 状态可选
* @param {string} [data.expire_time] - 过期时间可选
* @param {string} [data.password] - 新密码可选
* @returns {Promise} 返回更新结果
*/
export function updateUser(id, data) {
return request.put(`/api/users/${id}`, data)
}
/**
* 删除用户
* @param {string|number} id - 用户ID
* @returns {Promise} 返回删除结果
*/
export function deleteUser(id) {
return request.delete(`/api/users/${id}`)
}

View File

@ -1,11 +1,30 @@
import { defineStore } from 'pinia'
import { login as loginApi, logout as logoutApi } from '@/api/login'
// 安全的 JSON 解析函数
const safeJSONParse = (str, defaultValue = null) => {
if (!str || str === 'undefined' || str === 'null') {
return defaultValue
}
try {
return JSON.parse(str)
} catch (error) {
console.warn('JSON解析错误使用默认值:', error)
return defaultValue
}
}
export const useUserStore = defineStore('user', {
state: () => ({
token: localStorage.getItem('token'),
userInfo: JSON.parse(localStorage.getItem('userInfo')) || null
}),
state: () => {
// 从 localStorage 获取数据
const token = localStorage.getItem('token')
const userInfoStr = localStorage.getItem('userInfo')
return {
token: token || null,
userInfo: safeJSONParse(userInfoStr)
}
},
actions: {
// 登录

68
src/utils/format.js Normal file
View File

@ -0,0 +1,68 @@
/**
* 格式化日期时间
* @param {string|number|Date} time 需要格式化的时间
* @param {string} [format='YYYY-MM-DD HH:mm:ss'] 格式化的格式
* @returns {string} 格式化后的时间字符串
*/
export function formatDateTime(time, format = 'YYYY-MM-DD HH:mm:ss') {
if (!time) return '';
const date = new Date(time);
if (isNaN(date.getTime())) return '';
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds);
}
/**
* 格式化日期
* @param {string|number|Date} time 需要格式化的时间
* @returns {string} 格式化后的日期字符串 YYYY-MM-DD
*/
export function formatDate(time) {
return formatDateTime(time, 'YYYY-MM-DD');
}
/**
* 格式化时间为相对时间
* @param {string|number|Date} time 需要格式化的时间
* @returns {string} 相对时间描述
*/
export function formatRelativeTime(time) {
if (!time) return '';
const date = new Date(time);
if (isNaN(date.getTime())) return '';
const now = new Date();
const diff = now - date;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 30) {
return formatDateTime(time);
} else if (days > 0) {
return `${days}天前`;
} else if (hours > 0) {
return `${hours}小时前`;
} else if (minutes > 0) {
return `${minutes}分钟前`;
} else {
return '刚刚';
}
}

View File

@ -38,31 +38,9 @@ service.interceptors.response.use(
(response) => {
const res = response.data
// 模拟API响应
if (response.config.url.includes('/api/users/')) {
// 模拟登录接口
if (response.config.url.includes('/login')) {
return {
token: 'demo-token',
userInfo: {
id: 1,
username: 'admin',
role: '管理员',
email: 'admin@example.com',
status: true
}
}
}
// 模拟退出登录接口
if (response.config.url.includes('/logout')) {
return { message: '退出成功' }
}
}
// 如果响应成功
if (res.code === 200 || res.code === undefined) {
return res.data || res
if (res.success) {
return res
}
// 处理特定错误码
@ -74,7 +52,8 @@ service.interceptors.response.use(
ElMessage.error('没有权限访问该资源')
break
case 500:
ElMessage.error('服务器错误,请稍后重试')
console.error('服务器错误详情:', res)
ElMessage.error(res.message || '服务器错误,请稍后重试')
break
default:
ElMessage.error(res.message || '请求失败')
@ -83,7 +62,18 @@ service.interceptors.response.use(
return Promise.reject(new Error(res.message || '请求失败'))
},
(error) => {
console.error('响应错误:', error)
console.error('响应错误详情:', {
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data,
headers: error.response?.headers,
config: {
url: error.config?.url,
method: error.config?.method,
headers: error.config?.headers,
params: error.config?.params
}
})
// 处理网络错误
if (!error.response) {

View File

@ -4,38 +4,6 @@ import * as echarts from "echarts";
import { ElMessageBox, ElMessage } from "element-plus";
import { Plus } from '@element-plus/icons-vue';
interface AnalysisReport {
id: number;
title: string;
type: "species" | "environment"; // /
timeRange: {
//
start: string;
end: string;
};
dataSource: {
//
type: string;
points: string[]; //
}[];
analysis: {
summary: string; //
trends: {
//
indicator: string; //
trend: string; //
data: any[]; //
}[];
abnormal: {
//
type: string;
description: string;
level: string;
}[];
};
recommendations: string[]; //
}
//
const tableData = ref([
{

View File

@ -1,32 +1,260 @@
<script setup>
import { ref } from "vue";
import { ref, onMounted, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { getUserList, createUser, updateUser, deleteUser } from "@/api/user";
import { formatDateTime } from "@/utils/format";
const tableData = ref([
{
id: 1,
username: "admin",
role: "超级管理员",
email: "admin@example.com",
status: true,
createTime: "2024-03-20",
},
{
id: 2,
username: "manager",
role: "管理人员",
email: "manager@example.com",
status: true,
createTime: "2024-03-20",
},
]);
const tableData = ref([]);
const allData = ref([]); //
const loading = ref(false);
const total = ref(0);
//
const queryParams = ref({
username: '',
role: '',
status: ''
});
//
const filteredData = computed(() => {
return allData.value.filter(item => {
//
if (queryParams.value.username &&
!item.real_name.toLowerCase().includes(queryParams.value.username.toLowerCase())) {
return false;
}
//
if (queryParams.value.role &&
item.role.name !== queryParams.value.role) {
return false;
}
//
if (queryParams.value.status !== '' &&
item.status !== queryParams.value.status) {
return false;
}
return true;
}).sort((a, b) => a.id - b.id); // ID
});
//
const getList = async () => {
loading.value = true;
try {
const res = await getUserList();
if (res.success) {
allData.value = res.data.list; //
tableData.value = filteredData.value; //
total.value = filteredData.value.length;
} else {
ElMessage.error(res.message || '获取用户列表失败');
}
} catch (error) {
if (error.response) {
ElMessage.error(error.response.data.message || '获取用户列表失败');
} else if (error.request) {
ElMessage.error('网络错误,请检查网络连接');
} else {
ElMessage.error(error.message || '获取用户列表失败');
}
} finally {
loading.value = false;
}
};
//
const handleQuery = () => {
tableData.value = filteredData.value;
total.value = filteredData.value.length;
};
//
const resetQuery = () => {
queryParams.value = {
username: '',
role: '',
status: ''
};
tableData.value = allData.value;
total.value = allData.value.length;
};
// /
const dialogVisible = ref(false);
const dialogTitle = ref('新增用户');
const formData = ref({
username: '',
password: '',
role_id: undefined,
real_name: '',
email: '',
phone: '',
status: 1,
expire_time: undefined
});
//
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 30, message: '长度在 6 到 30 个字符', trigger: 'blur' },
{
pattern: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{6,30}$/,
message: '密码必须包含字母和数字',
trigger: 'blur'
}
],
role_id: [
{ required: true, message: '请选择角色', trigger: 'change' }
],
email: [
{
pattern: /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/,
message: '请输入正确的邮箱格式',
trigger: 'blur'
}
],
phone: [
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号格式',
trigger: 'blur'
}
]
};
const formRef = ref(null);
const isEdit = ref(false);
const editId = ref(null);
//
const handleAdd = () => {
dialogTitle.value = '新增用户';
isEdit.value = false;
editId.value = null;
formData.value = {
username: '',
password: '',
role_id: undefined,
real_name: '',
email: '',
phone: '',
status: 1,
expire_time: undefined
};
dialogVisible.value = true;
};
//
const handleEdit = (row) => {
console.log("编辑用户:", row);
dialogTitle.value = '编辑用户';
isEdit.value = true;
editId.value = row.id;
formData.value = {
real_name: row.real_name || '',
email: row.email || '',
phone: row.phone || '',
role_id: row.role.id.toString(),
status: row.status.toString(),
expire_time: row.expire_time || '',
password: '' //
};
dialogVisible.value = true;
};
const handleDelete = (row) => {
console.log("删除用户:", row);
//
const handleSubmit = async () => {
if (!formRef.value) return;
try {
await formRef.value.validate();
const submitData = { ...formData.value };
//
if (submitData.status) {
submitData.status = parseInt(submitData.status); //
}
if (submitData.role_id) {
submitData.role_id = parseInt(submitData.role_id); //
}
//
if (!submitData.expire_time) {
delete submitData.expire_time;
}
console.log('提交的数据:', submitData); //
if (isEdit.value) {
//
if (!submitData.password) {
delete submitData.password;
}
await updateUser(editId.value, submitData);
ElMessage.success('编辑成功');
} else {
await createUser(submitData);
ElMessage.success('添加成功');
}
dialogVisible.value = false;
getList(); //
} catch (error) {
console.error('表单提交错误:', error);
if (error.response) {
console.error('错误响应数据:', error.response.data);
ElMessage.error(error.response.data.message || '提交失败,请检查表单数据');
} else if (error.request) {
ElMessage.error('网络错误,请检查网络连接');
} else {
ElMessage.error(error.message || '操作失败');
}
}
};
//
const handleDelete = (row) => {
ElMessageBox.confirm(
'确认删除该用户吗?',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
await deleteUser(row.id);
ElMessage.success('删除成功');
getList(); //
} catch (error) {
ElMessage.error(error.message || '删除失败');
}
}).catch(() => {
//
});
};
//
const roleOptions = [
{ label: '超级管理员', value: '1' },
{ label: '管理人员', value: '2' },
{ label: '普通用户', value: '3' }
];
//
onMounted(() => {
getList();
});
</script>
<template>
@ -35,14 +263,50 @@ const handleDelete = (row) => {
<template #header>
<div class="card-header">
<span>用户管理</span>
<el-button type="primary">新增用户</el-button>
<el-button type="primary" @click="handleAdd">新增用户</el-button>
</div>
</template>
<el-table :data="tableData" style="width: 100%">
<!-- 搜索表单 -->
<el-form :model="queryParams" ref="queryForm" :inline="true" class="search-form">
<el-form-item label="用户名" prop="username">
<el-input
v-model="queryParams.username"
placeholder="请输入用户名"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="角色" prop="role">
<el-select v-model="queryParams.role" placeholder="请选择角色" clearable>
<el-option
v-for="item in roleOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option :value="1" label="启用" />
<el-option :value="0" label="禁用" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="用户名" width="120" />
<el-table-column prop="role" label="角色" width="120" />
<el-table-column prop="real_name" label="用户名" width="120" />
<el-table-column prop="role.name" label="角色" width="120" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
@ -51,19 +315,116 @@ const handleDelete = (row) => {
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column prop="updated_at" label="创建时间" width="180">
<template #default="{ row }">
{{ formatDateTime(row.updated_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)">
<el-button type="primary" link @click="handleEdit(row)">
编辑
</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">
<el-button type="danger" link @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
:size="'small'"
:total="total"
:default-current-page="1"
:default-page-size="10"
layout="total, prev, pager, next"
:disabled="true"
/>
</div>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="500px"
@close="dialogVisible = false"
destroy-on-close
>
<el-form
ref="formRef"
:model="formData"
:rules="isEdit ? {
...rules,
password: [] //
} : rules"
label-width="100px"
class="user-form"
>
<el-form-item label="用户名" prop="username" v-if="!isEdit">
<el-input v-model="formData.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item
:label="isEdit ? '新密码' : '密码'"
prop="password"
>
<el-input
v-model="formData.password"
type="password"
show-password
:placeholder="isEdit ? '不修改请留空' : '请输入密码'"
/>
</el-form-item>
<el-form-item label="角色" prop="role_id">
<el-select v-model="formData.role_id" placeholder="请选择角色" class="w-full">
<el-option
v-for="item in roleOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="真实姓名" prop="real_name">
<el-input
v-model="formData.real_name"
placeholder="请输入真实姓名"
maxlength="50"
show-word-limit
/>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="formData.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :value="'1'">启用</el-radio>
<el-radio :value="'0'">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="过期时间" prop="expire_time">
<el-date-picker
v-model="formData.expire_time"
type="datetime"
placeholder="请选择过期时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
class="w-full"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
@ -96,6 +457,25 @@ const handleDelete = (row) => {
}
}
.search-form {
margin-bottom: 20px;
.el-form-item {
margin-bottom: 16px;
:deep(.el-input),
:deep(.el-select) {
width: 240px;
}
}
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
:deep(.el-table) {
th.el-table__cell {
background-color: #fafafa;
@ -111,4 +491,23 @@ const handleDelete = (row) => {
.el-tag {
border-radius: 2px;
}
.user-form {
.el-form-item {
margin-bottom: 20px;
}
}
.dialog-footer {
text-align: right;
padding-top: 20px;
}
.w-full {
width: 100%;
}
:deep(.el-dialog__body) {
padding: 20px 40px;
}
</style>