加入分组查询、姓名查询,修复了页面闪烁问题,人脸质量等统一提示问题
This commit is contained in:
parent
499d5426c8
commit
0cf047ed03
|
|
@ -471,6 +471,15 @@ class FaceFeatureExtractor:
|
|||
processing_time) / self.stats['total_extractions']
|
||||
)
|
||||
|
||||
# Check if we detected faces but filtered them all out due to quality
|
||||
if not face_infos and boxes:
|
||||
return FeatureExtractionResult(
|
||||
success=False,
|
||||
faces=[],
|
||||
processing_time=processing_time,
|
||||
error_message="Face quality check failed (brightness/clarity/pose)"
|
||||
)
|
||||
|
||||
return FeatureExtractionResult(
|
||||
success=len(face_infos) > 0,
|
||||
faces=face_infos,
|
||||
|
|
|
|||
|
|
@ -33,8 +33,10 @@ public class UserController {
|
|||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
public Map<String, Object> listUsers() {
|
||||
List<User> users = userService.getAllUsers();
|
||||
public Map<String, Object> listUsers(
|
||||
@RequestParam(value = "name", required = false) String name,
|
||||
@RequestParam(value = "groupId", required = false) Long groupId) {
|
||||
List<User> users = userService.getUsers(name, groupId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 200);
|
||||
|
|
@ -94,4 +96,14 @@ public class UserController {
|
|||
result.put("msg", "批量删除成功");
|
||||
return result;
|
||||
}
|
||||
|
||||
@GetMapping("/count")
|
||||
public Map<String, Object> countUsers() {
|
||||
long count = userService.countUsers();
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 200);
|
||||
result.put("msg", "success");
|
||||
result.put("data", count);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
package com.bonuos.face.handler;
|
||||
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public Map<String, Object> handleRuntimeException(RuntimeException e) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 500);
|
||||
|
||||
// 提取核心错误信息,去除 "java.lang.RuntimeException: " 前缀
|
||||
String msg = e.getMessage();
|
||||
if (msg != null && msg.contains(": ")) {
|
||||
// 如果是包装过的异常,尝试提取更有意义的部分,但这里简单返回 message 即可,
|
||||
// 因为我们在 Service 层已经精心构造了 message
|
||||
}
|
||||
|
||||
result.put("msg", msg != null ? msg : "系统内部错误");
|
||||
return result;
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public Map<String, Object> handleException(Exception e) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 500);
|
||||
result.put("msg", "系统未知错误: " + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -17,11 +17,15 @@ public interface UserService {
|
|||
User addUser(String name, Long groupId, MultipartFile photo);
|
||||
|
||||
/**
|
||||
* 查询所有用户
|
||||
* 查询用户列表(支持过滤)
|
||||
*
|
||||
* @param name 姓名(模糊查询)
|
||||
* @param groupId 分组ID
|
||||
* @return 用户列表
|
||||
*/
|
||||
List<User> getAllUsers();
|
||||
List<User> getUsers(String name, Long groupId);
|
||||
|
||||
long countUsers();
|
||||
|
||||
/**
|
||||
* 根据ID查询用户
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.bonuos.face.service.impl;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.bonuos.face.entity.User;
|
||||
import com.bonuos.face.mapper.UserMapper;
|
||||
|
|
@ -64,6 +63,13 @@ public class UserServiceImpl implements UserService {
|
|||
List<Float> features = faceFeatureExtractorClient.extractFeature(dest);
|
||||
featureJson = convertFeaturesToJson(features);
|
||||
} catch (IOException e) {
|
||||
// 处理特征提取失败的异常
|
||||
if (e.getMessage().contains("No face detected") ||
|
||||
e.getMessage().contains("未检测到人脸") ||
|
||||
e.getMessage().contains("Face quality check failed") ||
|
||||
e.getMessage().toLowerCase().contains("feature extraction failed")) {
|
||||
throw new RuntimeException("请上传正确且清晰的人脸照片", e);
|
||||
}
|
||||
throw new RuntimeException("特征提取失败: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
|
|
@ -95,11 +101,28 @@ public class UserServiceImpl implements UserService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public List<User> getAllUsers() {
|
||||
public List<User> getUsers(String name, Long groupId) {
|
||||
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<User> queryWrapper = new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
|
||||
// 只返回活跃用户(status=1)
|
||||
return userMapper.selectList(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<User>()
|
||||
.eq("status", 1));
|
||||
queryWrapper.eq("status", 1);
|
||||
|
||||
if (name != null && !name.isEmpty()) {
|
||||
queryWrapper.like("name", name);
|
||||
}
|
||||
|
||||
if (groupId != null) {
|
||||
queryWrapper.eq("group_id", groupId);
|
||||
}
|
||||
|
||||
queryWrapper.orderByDesc("create_time");
|
||||
|
||||
return userMapper.selectList(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countUsers() {
|
||||
return userMapper
|
||||
.selectCount(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<User>().eq("status", 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -142,6 +165,13 @@ public class UserServiceImpl implements UserService {
|
|||
List<Float> features = faceFeatureExtractorClient.extractFeature(dest);
|
||||
user.setFeatureData(convertFeaturesToJson(features));
|
||||
} catch (IOException e) {
|
||||
// 处理特征提取失败的异常
|
||||
if (e.getMessage().contains("No face detected") ||
|
||||
e.getMessage().contains("未检测到人脸") ||
|
||||
e.getMessage().contains("Face quality check failed") ||
|
||||
e.getMessage().toLowerCase().contains("feature extraction failed")) {
|
||||
throw new RuntimeException("请上传正确且清晰的人脸照片", e);
|
||||
}
|
||||
throw new RuntimeException("照片保存或特征提取失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,8 @@ public class FaceFeatureExtractorClient {
|
|||
} else {
|
||||
throw new IOException("API call failed with status: " + response.getStatusCode());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Error calling Face Feature Extractor API", e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,455 +126,498 @@
|
|||
object-fit: cover;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<el-container style="height: 100vh;">
|
||||
<el-aside width="220px">
|
||||
<div
|
||||
style="height: 60px; display: flex; align-items: center; padding: 0 15px; background-color: #304156; border-bottom: 1px solid #1f2d3d;">
|
||||
<img src="company_icon.png"
|
||||
style="width: 32px; height: 32px; margin-right: 10px; border-radius: 50%;">
|
||||
<div style="color: #fff; font-size: 13px; font-weight: bold; line-height: 1.4;">
|
||||
<div>安徽博诺思</div>
|
||||
<div>信息科技有限公司</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-menu :default-active="activeMenu" @select="handleSelect">
|
||||
<el-menu-item index="overview">
|
||||
<el-icon>
|
||||
<Odometer />
|
||||
</el-icon>
|
||||
<span>系统概览</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="users">
|
||||
<el-icon>
|
||||
<User />
|
||||
</el-icon>
|
||||
<span>用户管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="groups">
|
||||
<el-icon><Office-Building /></el-icon>
|
||||
<span>分组管理</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header>
|
||||
<div class="logo">人脸识别智能管理系统</div>
|
||||
<div style="display: flex; align-items: center; gap: 15px;">
|
||||
<el-avatar :size="32"
|
||||
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"></el-avatar>
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span style="cursor: pointer; display: flex; align-items: center;">
|
||||
管理员 <el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main>
|
||||
|
||||
<!-- 仪表盘试图 -->
|
||||
<div v-if="activeMenu === 'overview'">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<template #header><span>👤 总用户数</span></template>
|
||||
<h2 style="margin: 0; color: #409EFF;">{{ users.length }}</h2>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<template #header><span>🏢 总分组数</span></template>
|
||||
<h2 style="margin: 0; color: #67C23A;">{{ groups.length }}</h2>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<body>
|
||||
<div id="app" v-cloak>
|
||||
<el-container style="height: 100vh;">
|
||||
<el-aside width="220px">
|
||||
<div
|
||||
style="height: 60px; display: flex; align-items: center; padding: 0 15px; background-color: #304156; border-bottom: 1px solid #1f2d3d;">
|
||||
<img src="company_icon.png"
|
||||
style="width: 32px; height: 32px; margin-right: 10px; border-radius: 50%;">
|
||||
<div style="color: #fff; font-size: 13px; font-weight: bold; line-height: 1.4;">
|
||||
<div>安徽博诺思</div>
|
||||
<div>信息科技有限公司</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-menu :default-active="activeMenu" @select="handleSelect">
|
||||
<el-menu-item index="overview">
|
||||
<el-icon>
|
||||
<Odometer />
|
||||
</el-icon>
|
||||
<span>系统概览</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="users">
|
||||
<el-icon>
|
||||
<User />
|
||||
</el-icon>
|
||||
<span>用户管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="groups">
|
||||
<el-icon><Office-Building /></el-icon>
|
||||
<span>分组管理</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header>
|
||||
<div class="logo">人脸识别智能管理系统</div>
|
||||
<div style="display: flex; align-items: center; gap: 15px;">
|
||||
<el-avatar :size="32"
|
||||
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"></el-avatar>
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span style="cursor: pointer; display: flex; align-items: center;">
|
||||
管理员 <el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main>
|
||||
|
||||
<!-- 用户管理视图 -->
|
||||
<div v-show="activeMenu === 'users'">
|
||||
<div class="page-header">
|
||||
<span class="page-title">用户列表</span>
|
||||
<div>
|
||||
<el-button @click="loadUsers" :icon="Refresh" circle></el-button>
|
||||
<el-button type="danger" :disabled="selectedUsers.length === 0"
|
||||
@click="handleBatchDelete">批量删除</el-button>
|
||||
<el-button type="primary" @click="openUserDialog()">+ 新增用户</el-button>
|
||||
<!-- 仪表盘试图 -->
|
||||
<div v-if="activeMenu === 'overview'">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<template #header><span>👤 总用户数</span></template>
|
||||
<h2 style="margin: 0; color: #409EFF;">{{ totalUserCount }}</h2>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<template #header><span>🏢 总分组数</span></template>
|
||||
<h2 style="margin: 0; color: #67C23A;">{{ groups.length }}</h2>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 用户管理视图 -->
|
||||
<div v-show="activeMenu === 'users'">
|
||||
<div class="page-header">
|
||||
<span class="page-title">用户列表</span>
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<el-select v-model="searchForm.groupId" placeholder="分组查询" clearable
|
||||
style="width: 150px" @change="loadUsers">
|
||||
<el-option v-for="g in groups" :key="g.id" :label="g.groupName"
|
||||
:value="g.id"></el-option>
|
||||
</el-select>
|
||||
<el-input v-model="searchForm.name" placeholder="姓名查询" style="width: 150px"
|
||||
clearable @clear="loadUsers" @keyup.enter="loadUsers">
|
||||
<template #append>
|
||||
<el-button :icon="Search" @click="loadUsers" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button @click="loadUsers" :icon="Refresh" circle></el-button>
|
||||
<el-button type="danger" :disabled="selectedUsers.length === 0"
|
||||
@click="handleBatchDelete">批量删除</el-button>
|
||||
<el-button type="primary" @click="openUserDialog()">+ 新增用户</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-container">
|
||||
<el-table :data="users" style="width: 100%" v-loading="loading"
|
||||
@selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55"></el-table-column>
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column label="头像" width="100">
|
||||
<template #default="scope">
|
||||
<img v-if="scope.row.photoUrl" :src="'/uploads/' + scope.row.photoUrl"
|
||||
class="small-avatar" onerror="this.style.display='none'">
|
||||
<el-tag v-else type="info" size="small">无</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="姓名"></el-table-column>
|
||||
<el-table-column label="所属分组">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.groupId" effect="plain">{{
|
||||
getGroupName(scope.row.groupId) }}</el-tag>
|
||||
<span v-else style="color: #999;">--</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="特征状态">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="hasFeature(scope.row)" type="success">已提取</el-tag>
|
||||
<el-tag v-else type="danger">未提取</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="openUserDialog(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger"
|
||||
@click="handleDeleteUser(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-container">
|
||||
<el-table :data="users" style="width: 100%" v-loading="loading"
|
||||
@selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55"></el-table-column>
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column label="头像" width="100">
|
||||
<template #default="scope">
|
||||
<img v-if="scope.row.photoUrl" :src="'/uploads/' + scope.row.photoUrl"
|
||||
class="small-avatar" onerror="this.style.display='none'">
|
||||
<el-tag v-else type="info" size="small">无</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="姓名"></el-table-column>
|
||||
<el-table-column label="所属分组">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.groupId" effect="plain">{{
|
||||
getGroupName(scope.row.groupId) }}</el-tag>
|
||||
<span v-else style="color: #999;">--</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="特征状态">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="hasFeature(scope.row)" type="success">已提取</el-tag>
|
||||
<el-tag v-else type="danger">未提取</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="openUserDialog(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger"
|
||||
@click="handleDeleteUser(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分组管理视图 -->
|
||||
<div v-show="activeMenu === 'groups'">
|
||||
<div class="page-header">
|
||||
<span class="page-title">分组列表</span>
|
||||
<div>
|
||||
<el-button @click="loadGroups" :icon="Refresh" circle></el-button>
|
||||
<el-button type="primary" @click="openGroupDialog()">+ 新建分组</el-button>
|
||||
<!-- 分组管理视图 -->
|
||||
<div v-show="activeMenu === 'groups'">
|
||||
<div class="page-header">
|
||||
<span class="page-title">分组列表</span>
|
||||
<div>
|
||||
<el-button @click="loadGroups" :icon="Refresh" circle></el-button>
|
||||
<el-button type="primary" @click="openGroupDialog()">+ 新建分组</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-container">
|
||||
<el-table :data="groups" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="groupName" label="分组名称"></el-table-column>
|
||||
<el-table-column prop="groupNo" label="分组编码"></el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间"></el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="danger"
|
||||
@click="handleDeleteGroup(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-container">
|
||||
<el-table :data="groups" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="groupName" label="分组名称"></el-table-column>
|
||||
<el-table-column prop="groupNo" label="分组编码"></el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间"></el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="danger"
|
||||
@click="handleDeleteGroup(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</el-main>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</el-container>
|
||||
|
||||
<!-- 用户编辑/新增弹窗 -->
|
||||
<el-dialog v-model="userDialog.visible" :title="userDialog.isEdit ? '编辑用户' : '新增用户'" width="500px">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="userForm.name" placeholder="请输入姓名"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="分组">
|
||||
<el-select v-model="userForm.groupId" placeholder="请选择分组" style="width: 100%">
|
||||
<el-option v-for="g in groups" :key="g.id" :label="g.groupName" :value="g.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="照片">
|
||||
<el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false"
|
||||
:on-change="handleFileChange">
|
||||
<img v-if="userForm.previewUrl" :src="userForm.previewUrl" class="avatar" />
|
||||
<el-icon v-else class="avatar-uploader-icon">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="userDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitUser" :loading="submitting">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 用户编辑/新增弹窗 -->
|
||||
<el-dialog v-model="userDialog.visible" :title="userDialog.isEdit ? '编辑用户' : '新增用户'" width="500px">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="userForm.name" placeholder="请输入姓名"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="分组">
|
||||
<el-select v-model="userForm.groupId" placeholder="请选择分组" style="width: 100%">
|
||||
<el-option v-for="g in groups" :key="g.id" :label="g.groupName" :value="g.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="照片">
|
||||
<el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false"
|
||||
:on-change="handleFileChange">
|
||||
<img v-if="userForm.previewUrl" :src="userForm.previewUrl" class="avatar" />
|
||||
<el-icon v-else class="avatar-uploader-icon">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="userDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitUser" :loading="submitting">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 分组弹窗 -->
|
||||
<el-dialog v-model="groupDialog.visible" title="新建分组" width="400px">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="分组名称">
|
||||
<el-input v-model="groupForm.groupName" placeholder="例如:研发部"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="groupDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitGroup" :loading="submitting">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 分组弹窗 -->
|
||||
<el-dialog v-model="groupDialog.visible" title="新建分组" width="400px">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="分组名称">
|
||||
<el-input v-model="groupForm.groupName" placeholder="例如:研发部"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="groupDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitGroup" :loading="submitting">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 引入 Vue 和 Element Plus -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<script src="https://unpkg.com/element-plus/dist/index.full.js"></script>
|
||||
<!-- 引入 Icons -->
|
||||
<script src="https://unpkg.com/@element-plus/icons-vue"></script>
|
||||
<!-- 引入 Vue 和 Element Plus -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<script src="https://unpkg.com/element-plus/dist/index.full.js"></script>
|
||||
<!-- 引入 Icons -->
|
||||
<script src="https://unpkg.com/@element-plus/icons-vue"></script>
|
||||
|
||||
<script>
|
||||
const { createApp, ref, reactive, onMounted, computed } = Vue;
|
||||
<script>
|
||||
const { createApp, ref, reactive, onMounted, computed } = Vue;
|
||||
|
||||
const app = createApp({
|
||||
setup() {
|
||||
// Auth Check
|
||||
const token = sessionStorage.getItem('admin_token');
|
||||
if (!token) {
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
|
||||
const activeMenu = ref('users');
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
const users = ref([]);
|
||||
const groups = ref([]);
|
||||
const groupMap = reactive({});
|
||||
|
||||
const handleCommand = (cmd) => {
|
||||
if (cmd === 'logout') {
|
||||
sessionStorage.removeItem('admin_token');
|
||||
const app = createApp({
|
||||
setup() {
|
||||
// Auth Check
|
||||
const token = sessionStorage.getItem('admin_token');
|
||||
if (!token) {
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
};
|
||||
|
||||
const selectedFile = ref(null);
|
||||
const selectedUsers = ref([]);
|
||||
const activeMenu = ref('users');
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
// Dialog States
|
||||
const userDialog = reactive({ visible: false, isEdit: false });
|
||||
const groupDialog = reactive({ visible: false });
|
||||
const users = ref([]);
|
||||
const groups = ref([]);
|
||||
const groupMap = reactive({});
|
||||
|
||||
// Forms
|
||||
const userForm = reactive({ id: null, name: '', groupId: null, previewUrl: '' });
|
||||
const groupForm = reactive({ groupName: '' });
|
||||
|
||||
// API Base
|
||||
const API_USER = '/api/users';
|
||||
const API_GROUP = '/api/groups';
|
||||
|
||||
const handleSelect = (key) => activeMenu.value = key;
|
||||
|
||||
// Load Data
|
||||
const loadGroups = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_GROUP}/list`).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
groups.value = res.data;
|
||||
res.data.forEach(g => groupMap[g.id] = g.groupName);
|
||||
const handleCommand = (cmd) => {
|
||||
if (cmd === 'logout') {
|
||||
sessionStorage.removeItem('admin_token');
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
} catch (e) { console.error(e); }
|
||||
};
|
||||
};
|
||||
|
||||
const loadUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await fetch(`${API_USER}/list`).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
users.value = res.data;
|
||||
}
|
||||
} catch (e) { ElementPlus.ElMessage.error('加载用户失败'); }
|
||||
loading.value = false;
|
||||
};
|
||||
const selectedFile = ref(null);
|
||||
const selectedUsers = ref([]);
|
||||
|
||||
const getGroupName = (id) => groupMap[id] || id;
|
||||
const hasFeature = (user) => user.featureData && user.featureData.length > 20;
|
||||
// Stats
|
||||
const totalUserCount = ref(0);
|
||||
|
||||
// User Operations
|
||||
const openUserDialog = (user) => {
|
||||
userDialog.visible = true;
|
||||
selectedFile.value = null;
|
||||
// Dialog States
|
||||
const userDialog = reactive({ visible: false, isEdit: false });
|
||||
const groupDialog = reactive({ visible: false });
|
||||
|
||||
if (user) {
|
||||
userDialog.isEdit = true;
|
||||
userForm.id = user.id;
|
||||
userForm.name = user.name;
|
||||
userForm.groupId = user.groupId;
|
||||
userForm.previewUrl = user.photoUrl ? `/uploads/${user.photoUrl}` : '';
|
||||
} else {
|
||||
userDialog.isEdit = false;
|
||||
userForm.id = null;
|
||||
userForm.name = '';
|
||||
userForm.groupId = null;
|
||||
userForm.previewUrl = '';
|
||||
}
|
||||
};
|
||||
// Forms
|
||||
const userForm = reactive({ id: null, name: '', groupId: null, previewUrl: '' });
|
||||
const groupForm = reactive({ groupName: '' });
|
||||
|
||||
const handleFileChange = (uploadFile) => {
|
||||
const file = uploadFile.raw;
|
||||
if (file) {
|
||||
selectedFile.value = file;
|
||||
userForm.previewUrl = URL.createObjectURL(file);
|
||||
}
|
||||
};
|
||||
// API Base
|
||||
const API_USER = '/api/users';
|
||||
const API_GROUP = '/api/groups';
|
||||
|
||||
const submitUser = async () => {
|
||||
if (!userForm.name) return ElementPlus.ElMessage.warning('请输入姓名');
|
||||
if (!userDialog.isEdit && !selectedFile.value) return ElementPlus.ElMessage.warning('请选择照片');
|
||||
const handleSelect = (key) => activeMenu.value = key;
|
||||
|
||||
submitting.value = true;
|
||||
const formData = new FormData();
|
||||
formData.append('name', userForm.name);
|
||||
if (userForm.groupId) formData.append('groupId', userForm.groupId);
|
||||
if (selectedFile.value) formData.append('photo', selectedFile.value);
|
||||
|
||||
try {
|
||||
const url = userDialog.isEdit ? `${API_USER}/update/${userForm.id}` : `${API_USER}/add`;
|
||||
const res = await fetch(url, { method: 'POST', body: formData }).then(r => r.json());
|
||||
|
||||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success(userDialog.isEdit ? '更新成功' : '注册成功');
|
||||
userDialog.visible = false;
|
||||
loadUsers();
|
||||
loadData(); // Refresh all
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg || '操作失败');
|
||||
}
|
||||
} catch (e) {
|
||||
ElementPlus.ElMessage.error('网络错误');
|
||||
}
|
||||
submitting.value = false;
|
||||
};
|
||||
|
||||
const handleDeleteUser = (user) => {
|
||||
ElementPlus.ElMessageBox.confirm(`确定删除用户 ${user.name} 吗?`, '提示', { type: 'warning' })
|
||||
.then(async () => {
|
||||
const res = await fetch(`${API_USER}/${user.id}`, { method: 'DELETE' }).then(r => r.json());
|
||||
// Load Data
|
||||
const loadGroups = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_GROUP}/list`).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
loadUsers();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg);
|
||||
groups.value = res.data;
|
||||
res.data.forEach(g => groupMap[g.id] = g.groupName);
|
||||
}
|
||||
}).catch(() => { });
|
||||
};
|
||||
} catch (e) { console.error(e); }
|
||||
};
|
||||
|
||||
const handleSelectionChange = (val) => {
|
||||
selectedUsers.value = val;
|
||||
};
|
||||
const searchForm = reactive({ name: '', groupId: null });
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedUsers.value.length === 0) return;
|
||||
const loadUserCount = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_USER}/count`).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
totalUserCount.value = res.data;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load user count", e);
|
||||
}
|
||||
};
|
||||
|
||||
ElementPlus.ElMessageBox.confirm(`确定删除选中的 ${selectedUsers.value.length} 名用户吗?`, '提示', { type: 'warning' })
|
||||
.then(async () => {
|
||||
const ids = selectedUsers.value.map(u => u.id);
|
||||
try {
|
||||
const response = await fetch(`${API_USER}/deleteBatch`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(ids)
|
||||
});
|
||||
const loadUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (searchForm.name) params.append('name', searchForm.name);
|
||||
if (searchForm.groupId) params.append('groupId', searchForm.groupId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status} - 后端可能未更新`);
|
||||
}
|
||||
const res = await fetch(`${API_USER}/list?${params.toString()}`).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
users.value = res.data;
|
||||
}
|
||||
} catch (e) { ElementPlus.ElMessage.error('加载用户失败'); }
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const res = await response.json();
|
||||
const getGroupName = (id) => groupMap[id] || id;
|
||||
const hasFeature = (user) => user.featureData && user.featureData.length > 20;
|
||||
|
||||
// User Operations
|
||||
const openUserDialog = (user) => {
|
||||
userDialog.visible = true;
|
||||
selectedFile.value = null;
|
||||
|
||||
if (user) {
|
||||
userDialog.isEdit = true;
|
||||
userForm.id = user.id;
|
||||
userForm.name = user.name;
|
||||
userForm.groupId = user.groupId;
|
||||
userForm.previewUrl = user.photoUrl ? `/uploads/${user.photoUrl}` : '';
|
||||
} else {
|
||||
userDialog.isEdit = false;
|
||||
userForm.id = null;
|
||||
userForm.name = '';
|
||||
userForm.groupId = null;
|
||||
userForm.previewUrl = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (uploadFile) => {
|
||||
const file = uploadFile.raw;
|
||||
if (file) {
|
||||
selectedFile.value = file;
|
||||
userForm.previewUrl = URL.createObjectURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const submitUser = async () => {
|
||||
if (!userForm.name) return ElementPlus.ElMessage.warning('请输入姓名');
|
||||
if (!userDialog.isEdit && !selectedFile.value) return ElementPlus.ElMessage.warning('请选择照片');
|
||||
|
||||
submitting.value = true;
|
||||
const formData = new FormData();
|
||||
formData.append('name', userForm.name);
|
||||
if (userForm.groupId) formData.append('groupId', userForm.groupId);
|
||||
if (selectedFile.value) formData.append('photo', selectedFile.value);
|
||||
|
||||
try {
|
||||
const url = userDialog.isEdit ? `${API_USER}/update/${userForm.id}` : `${API_USER}/add`;
|
||||
const res = await fetch(url, { method: 'POST', body: formData }).then(r => r.json());
|
||||
|
||||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success(userDialog.isEdit ? '更新成功' : '注册成功');
|
||||
userDialog.visible = false;
|
||||
loadUsers();
|
||||
loadUserCount();
|
||||
loadData(); // Refresh all
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg || '操作失败');
|
||||
}
|
||||
} catch (e) {
|
||||
ElementPlus.ElMessage.error('网络错误');
|
||||
}
|
||||
submitting.value = false;
|
||||
};
|
||||
|
||||
const handleDeleteUser = (user) => {
|
||||
ElementPlus.ElMessageBox.confirm(`确定删除用户 ${user.name} 吗?`, '提示', { type: 'warning' })
|
||||
.then(async () => {
|
||||
const res = await fetch(`${API_USER}/${user.id}`, { method: 'DELETE' }).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success('批量删除成功');
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
loadUsers();
|
||||
loadUserCount();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ElementPlus.ElMessage.error('删除失败: ' + e.message);
|
||||
}
|
||||
}).catch(() => { });
|
||||
};
|
||||
}).catch(() => { });
|
||||
};
|
||||
|
||||
// Group Operations
|
||||
const openGroupDialog = () => {
|
||||
groupDialog.visible = true;
|
||||
groupForm.groupName = '';
|
||||
};
|
||||
const handleSelectionChange = (val) => {
|
||||
selectedUsers.value = val;
|
||||
};
|
||||
|
||||
const submitGroup = async () => {
|
||||
if (!groupForm.groupName) return ElementPlus.ElMessage.warning('请输入分组名称');
|
||||
submitting.value = true;
|
||||
try {
|
||||
const res = await fetch(`${API_GROUP}/add`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ groupName: groupForm.groupName })
|
||||
}).then(r => r.json());
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedUsers.value.length === 0) return;
|
||||
|
||||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success('分组创建成功');
|
||||
groupDialog.visible = false;
|
||||
loadGroups();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg);
|
||||
}
|
||||
} catch (e) { ElementPlus.ElMessage.error('网络错误'); }
|
||||
submitting.value = false;
|
||||
};
|
||||
ElementPlus.ElMessageBox.confirm(`确定删除选中的 ${selectedUsers.value.length} 名用户吗?`, '提示', { type: 'warning' })
|
||||
.then(async () => {
|
||||
const ids = selectedUsers.value.map(u => u.id);
|
||||
try {
|
||||
const response = await fetch(`${API_USER}/deleteBatch`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(ids)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status} - 后端可能未更新`);
|
||||
}
|
||||
|
||||
const res = await response.json();
|
||||
|
||||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success('批量删除成功');
|
||||
loadUsers();
|
||||
loadUserCount();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ElementPlus.ElMessage.error('删除失败: ' + e.message);
|
||||
}
|
||||
}).catch(() => { });
|
||||
};
|
||||
|
||||
// Group Operations
|
||||
const openGroupDialog = () => {
|
||||
groupDialog.visible = true;
|
||||
groupForm.groupName = '';
|
||||
};
|
||||
|
||||
const submitGroup = async () => {
|
||||
if (!groupForm.groupName) return ElementPlus.ElMessage.warning('请输入分组名称');
|
||||
submitting.value = true;
|
||||
try {
|
||||
const res = await fetch(`${API_GROUP}/add`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ groupName: groupForm.groupName })
|
||||
}).then(r => r.json());
|
||||
|
||||
const handleDeleteGroup = (group) => {
|
||||
ElementPlus.ElMessageBox.confirm(`确定删除分组 ${group.groupName} 吗? 如果分组下有用户将无法删除。`, '提示', { type: 'warning' })
|
||||
.then(async () => {
|
||||
const res = await fetch(`${API_GROUP}/${group.id}`, { method: 'DELETE' }).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
ElementPlus.ElMessage.success('分组创建成功');
|
||||
groupDialog.visible = false;
|
||||
loadGroups();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error('删除失败: ' + res.msg);
|
||||
ElementPlus.ElMessage.error(res.msg);
|
||||
}
|
||||
}).catch(() => { });
|
||||
};
|
||||
} catch (e) { ElementPlus.ElMessage.error('网络错误'); }
|
||||
submitting.value = false;
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
await loadGroups();
|
||||
await loadUsers();
|
||||
};
|
||||
const handleDeleteGroup = (group) => {
|
||||
ElementPlus.ElMessageBox.confirm(`确定删除分组 ${group.groupName} 吗? 如果分组下有用户将无法删除。`, '提示', { type: 'warning' })
|
||||
.then(async () => {
|
||||
const res = await fetch(`${API_GROUP}/${group.id}`, { method: 'DELETE' }).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
loadGroups();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error('删除失败: ' + res.msg);
|
||||
}
|
||||
}).catch(() => { });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
const loadData = async () => {
|
||||
await loadGroups();
|
||||
await loadUsers();
|
||||
await loadUserCount();
|
||||
};
|
||||
|
||||
return {
|
||||
activeMenu, handleSelect,
|
||||
users, groups, loading, submitting,
|
||||
getGroupName, hasFeature,
|
||||
userDialog, userForm, openUserDialog, submitUser, handleDeleteUser,
|
||||
handleSelectionChange, handleBatchDelete, selectedUsers,
|
||||
groupDialog, groupForm, openGroupDialog, submitGroup, handleDeleteGroup,
|
||||
loadUsers, loadGroups,
|
||||
loadUsers, loadGroups,
|
||||
handleFileChange,
|
||||
handleCommand,
|
||||
Plus: ElementPlusIconsVue.Plus,
|
||||
Refresh: ElementPlusIconsVue.Refresh
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
return {
|
||||
activeMenu, handleSelect,
|
||||
users, groups, loading, submitting,
|
||||
getGroupName, hasFeature,
|
||||
userDialog, userForm, openUserDialog, submitUser, handleDeleteUser,
|
||||
handleSelectionChange, handleBatchDelete, selectedUsers,
|
||||
groupDialog, groupForm, openGroupDialog, submitGroup, handleDeleteGroup,
|
||||
loadUsers, loadGroups,
|
||||
handleFileChange,
|
||||
handleCommand,
|
||||
searchForm,
|
||||
totalUserCount,
|
||||
Plus: ElementPlusIconsVue.Plus,
|
||||
Refresh: ElementPlusIconsVue.Refresh,
|
||||
Search: ElementPlusIconsVue.Search
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Register Icons
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
});
|
||||
|
||||
// Register Icons
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(ElementPlus);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
app.use(ElementPlus);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -126,455 +126,498 @@
|
|||
object-fit: cover;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<el-container style="height: 100vh;">
|
||||
<el-aside width="220px">
|
||||
<div
|
||||
style="height: 60px; display: flex; align-items: center; padding: 0 15px; background-color: #304156; border-bottom: 1px solid #1f2d3d;">
|
||||
<img src="company_icon.png"
|
||||
style="width: 32px; height: 32px; margin-right: 10px; border-radius: 50%;">
|
||||
<div style="color: #fff; font-size: 13px; font-weight: bold; line-height: 1.4;">
|
||||
<div>安徽博诺思</div>
|
||||
<div>信息科技有限公司</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-menu :default-active="activeMenu" @select="handleSelect">
|
||||
<el-menu-item index="overview">
|
||||
<el-icon>
|
||||
<Odometer />
|
||||
</el-icon>
|
||||
<span>系统概览</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="users">
|
||||
<el-icon>
|
||||
<User />
|
||||
</el-icon>
|
||||
<span>用户管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="groups">
|
||||
<el-icon><Office-Building /></el-icon>
|
||||
<span>分组管理</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header>
|
||||
<div class="logo">人脸识别智能管理系统</div>
|
||||
<div style="display: flex; align-items: center; gap: 15px;">
|
||||
<el-avatar :size="32"
|
||||
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"></el-avatar>
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span style="cursor: pointer; display: flex; align-items: center;">
|
||||
管理员 <el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main>
|
||||
|
||||
<!-- 仪表盘试图 -->
|
||||
<div v-if="activeMenu === 'overview'">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<template #header><span>👤 总用户数</span></template>
|
||||
<h2 style="margin: 0; color: #409EFF;">{{ users.length }}</h2>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<template #header><span>🏢 总分组数</span></template>
|
||||
<h2 style="margin: 0; color: #67C23A;">{{ groups.length }}</h2>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<body>
|
||||
<div id="app" v-cloak>
|
||||
<el-container style="height: 100vh;">
|
||||
<el-aside width="220px">
|
||||
<div
|
||||
style="height: 60px; display: flex; align-items: center; padding: 0 15px; background-color: #304156; border-bottom: 1px solid #1f2d3d;">
|
||||
<img src="company_icon.png"
|
||||
style="width: 32px; height: 32px; margin-right: 10px; border-radius: 50%;">
|
||||
<div style="color: #fff; font-size: 13px; font-weight: bold; line-height: 1.4;">
|
||||
<div>安徽博诺思</div>
|
||||
<div>信息科技有限公司</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-menu :default-active="activeMenu" @select="handleSelect">
|
||||
<el-menu-item index="overview">
|
||||
<el-icon>
|
||||
<Odometer />
|
||||
</el-icon>
|
||||
<span>系统概览</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="users">
|
||||
<el-icon>
|
||||
<User />
|
||||
</el-icon>
|
||||
<span>用户管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="groups">
|
||||
<el-icon><Office-Building /></el-icon>
|
||||
<span>分组管理</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header>
|
||||
<div class="logo">人脸识别智能管理系统</div>
|
||||
<div style="display: flex; align-items: center; gap: 15px;">
|
||||
<el-avatar :size="32"
|
||||
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"></el-avatar>
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span style="cursor: pointer; display: flex; align-items: center;">
|
||||
管理员 <el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main>
|
||||
|
||||
<!-- 用户管理视图 -->
|
||||
<div v-show="activeMenu === 'users'">
|
||||
<div class="page-header">
|
||||
<span class="page-title">用户列表</span>
|
||||
<div>
|
||||
<el-button @click="loadUsers" :icon="Refresh" circle></el-button>
|
||||
<el-button type="danger" :disabled="selectedUsers.length === 0"
|
||||
@click="handleBatchDelete">批量删除</el-button>
|
||||
<el-button type="primary" @click="openUserDialog()">+ 新增用户</el-button>
|
||||
<!-- 仪表盘试图 -->
|
||||
<div v-if="activeMenu === 'overview'">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<template #header><span>👤 总用户数</span></template>
|
||||
<h2 style="margin: 0; color: #409EFF;">{{ totalUserCount }}</h2>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<template #header><span>🏢 总分组数</span></template>
|
||||
<h2 style="margin: 0; color: #67C23A;">{{ groups.length }}</h2>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 用户管理视图 -->
|
||||
<div v-show="activeMenu === 'users'">
|
||||
<div class="page-header">
|
||||
<span class="page-title">用户列表</span>
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<el-select v-model="searchForm.groupId" placeholder="分组查询" clearable
|
||||
style="width: 150px" @change="loadUsers">
|
||||
<el-option v-for="g in groups" :key="g.id" :label="g.groupName"
|
||||
:value="g.id"></el-option>
|
||||
</el-select>
|
||||
<el-input v-model="searchForm.name" placeholder="姓名查询" style="width: 150px"
|
||||
clearable @clear="loadUsers" @keyup.enter="loadUsers">
|
||||
<template #append>
|
||||
<el-button :icon="Search" @click="loadUsers" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button @click="loadUsers" :icon="Refresh" circle></el-button>
|
||||
<el-button type="danger" :disabled="selectedUsers.length === 0"
|
||||
@click="handleBatchDelete">批量删除</el-button>
|
||||
<el-button type="primary" @click="openUserDialog()">+ 新增用户</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-container">
|
||||
<el-table :data="users" style="width: 100%" v-loading="loading"
|
||||
@selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55"></el-table-column>
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column label="头像" width="100">
|
||||
<template #default="scope">
|
||||
<img v-if="scope.row.photoUrl" :src="'/uploads/' + scope.row.photoUrl"
|
||||
class="small-avatar" onerror="this.style.display='none'">
|
||||
<el-tag v-else type="info" size="small">无</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="姓名"></el-table-column>
|
||||
<el-table-column label="所属分组">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.groupId" effect="plain">{{
|
||||
getGroupName(scope.row.groupId) }}</el-tag>
|
||||
<span v-else style="color: #999;">--</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="特征状态">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="hasFeature(scope.row)" type="success">已提取</el-tag>
|
||||
<el-tag v-else type="danger">未提取</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="openUserDialog(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger"
|
||||
@click="handleDeleteUser(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-container">
|
||||
<el-table :data="users" style="width: 100%" v-loading="loading"
|
||||
@selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55"></el-table-column>
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column label="头像" width="100">
|
||||
<template #default="scope">
|
||||
<img v-if="scope.row.photoUrl" :src="'/uploads/' + scope.row.photoUrl"
|
||||
class="small-avatar" onerror="this.style.display='none'">
|
||||
<el-tag v-else type="info" size="small">无</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="姓名"></el-table-column>
|
||||
<el-table-column label="所属分组">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.groupId" effect="plain">{{
|
||||
getGroupName(scope.row.groupId) }}</el-tag>
|
||||
<span v-else style="color: #999;">--</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="特征状态">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="hasFeature(scope.row)" type="success">已提取</el-tag>
|
||||
<el-tag v-else type="danger">未提取</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="openUserDialog(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger"
|
||||
@click="handleDeleteUser(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分组管理视图 -->
|
||||
<div v-show="activeMenu === 'groups'">
|
||||
<div class="page-header">
|
||||
<span class="page-title">分组列表</span>
|
||||
<div>
|
||||
<el-button @click="loadGroups" :icon="Refresh" circle></el-button>
|
||||
<el-button type="primary" @click="openGroupDialog()">+ 新建分组</el-button>
|
||||
<!-- 分组管理视图 -->
|
||||
<div v-show="activeMenu === 'groups'">
|
||||
<div class="page-header">
|
||||
<span class="page-title">分组列表</span>
|
||||
<div>
|
||||
<el-button @click="loadGroups" :icon="Refresh" circle></el-button>
|
||||
<el-button type="primary" @click="openGroupDialog()">+ 新建分组</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-container">
|
||||
<el-table :data="groups" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="groupName" label="分组名称"></el-table-column>
|
||||
<el-table-column prop="groupNo" label="分组编码"></el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间"></el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="danger"
|
||||
@click="handleDeleteGroup(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-container">
|
||||
<el-table :data="groups" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="groupName" label="分组名称"></el-table-column>
|
||||
<el-table-column prop="groupNo" label="分组编码"></el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间"></el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="danger"
|
||||
@click="handleDeleteGroup(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</el-main>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</el-container>
|
||||
|
||||
<!-- 用户编辑/新增弹窗 -->
|
||||
<el-dialog v-model="userDialog.visible" :title="userDialog.isEdit ? '编辑用户' : '新增用户'" width="500px">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="userForm.name" placeholder="请输入姓名"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="分组">
|
||||
<el-select v-model="userForm.groupId" placeholder="请选择分组" style="width: 100%">
|
||||
<el-option v-for="g in groups" :key="g.id" :label="g.groupName" :value="g.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="照片">
|
||||
<el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false"
|
||||
:on-change="handleFileChange">
|
||||
<img v-if="userForm.previewUrl" :src="userForm.previewUrl" class="avatar" />
|
||||
<el-icon v-else class="avatar-uploader-icon">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="userDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitUser" :loading="submitting">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 用户编辑/新增弹窗 -->
|
||||
<el-dialog v-model="userDialog.visible" :title="userDialog.isEdit ? '编辑用户' : '新增用户'" width="500px">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="userForm.name" placeholder="请输入姓名"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="分组">
|
||||
<el-select v-model="userForm.groupId" placeholder="请选择分组" style="width: 100%">
|
||||
<el-option v-for="g in groups" :key="g.id" :label="g.groupName" :value="g.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="照片">
|
||||
<el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false"
|
||||
:on-change="handleFileChange">
|
||||
<img v-if="userForm.previewUrl" :src="userForm.previewUrl" class="avatar" />
|
||||
<el-icon v-else class="avatar-uploader-icon">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="userDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitUser" :loading="submitting">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 分组弹窗 -->
|
||||
<el-dialog v-model="groupDialog.visible" title="新建分组" width="400px">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="分组名称">
|
||||
<el-input v-model="groupForm.groupName" placeholder="例如:研发部"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="groupDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitGroup" :loading="submitting">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 分组弹窗 -->
|
||||
<el-dialog v-model="groupDialog.visible" title="新建分组" width="400px">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="分组名称">
|
||||
<el-input v-model="groupForm.groupName" placeholder="例如:研发部"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="groupDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitGroup" :loading="submitting">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 引入 Vue 和 Element Plus -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<script src="https://unpkg.com/element-plus/dist/index.full.js"></script>
|
||||
<!-- 引入 Icons -->
|
||||
<script src="https://unpkg.com/@element-plus/icons-vue"></script>
|
||||
<!-- 引入 Vue 和 Element Plus -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<script src="https://unpkg.com/element-plus/dist/index.full.js"></script>
|
||||
<!-- 引入 Icons -->
|
||||
<script src="https://unpkg.com/@element-plus/icons-vue"></script>
|
||||
|
||||
<script>
|
||||
const { createApp, ref, reactive, onMounted, computed } = Vue;
|
||||
<script>
|
||||
const { createApp, ref, reactive, onMounted, computed } = Vue;
|
||||
|
||||
const app = createApp({
|
||||
setup() {
|
||||
// Auth Check
|
||||
const token = sessionStorage.getItem('admin_token');
|
||||
if (!token) {
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
|
||||
const activeMenu = ref('users');
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
const users = ref([]);
|
||||
const groups = ref([]);
|
||||
const groupMap = reactive({});
|
||||
|
||||
const handleCommand = (cmd) => {
|
||||
if (cmd === 'logout') {
|
||||
sessionStorage.removeItem('admin_token');
|
||||
const app = createApp({
|
||||
setup() {
|
||||
// Auth Check
|
||||
const token = sessionStorage.getItem('admin_token');
|
||||
if (!token) {
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
};
|
||||
|
||||
const selectedFile = ref(null);
|
||||
const selectedUsers = ref([]);
|
||||
const activeMenu = ref('users');
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
// Dialog States
|
||||
const userDialog = reactive({ visible: false, isEdit: false });
|
||||
const groupDialog = reactive({ visible: false });
|
||||
const users = ref([]);
|
||||
const groups = ref([]);
|
||||
const groupMap = reactive({});
|
||||
|
||||
// Forms
|
||||
const userForm = reactive({ id: null, name: '', groupId: null, previewUrl: '' });
|
||||
const groupForm = reactive({ groupName: '' });
|
||||
|
||||
// API Base
|
||||
const API_USER = '/api/users';
|
||||
const API_GROUP = '/api/groups';
|
||||
|
||||
const handleSelect = (key) => activeMenu.value = key;
|
||||
|
||||
// Load Data
|
||||
const loadGroups = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_GROUP}/list`).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
groups.value = res.data;
|
||||
res.data.forEach(g => groupMap[g.id] = g.groupName);
|
||||
const handleCommand = (cmd) => {
|
||||
if (cmd === 'logout') {
|
||||
sessionStorage.removeItem('admin_token');
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
} catch (e) { console.error(e); }
|
||||
};
|
||||
};
|
||||
|
||||
const loadUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await fetch(`${API_USER}/list`).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
users.value = res.data;
|
||||
}
|
||||
} catch (e) { ElementPlus.ElMessage.error('加载用户失败'); }
|
||||
loading.value = false;
|
||||
};
|
||||
const selectedFile = ref(null);
|
||||
const selectedUsers = ref([]);
|
||||
|
||||
const getGroupName = (id) => groupMap[id] || id;
|
||||
const hasFeature = (user) => user.featureData && user.featureData.length > 20;
|
||||
// Stats
|
||||
const totalUserCount = ref(0);
|
||||
|
||||
// User Operations
|
||||
const openUserDialog = (user) => {
|
||||
userDialog.visible = true;
|
||||
selectedFile.value = null;
|
||||
// Dialog States
|
||||
const userDialog = reactive({ visible: false, isEdit: false });
|
||||
const groupDialog = reactive({ visible: false });
|
||||
|
||||
if (user) {
|
||||
userDialog.isEdit = true;
|
||||
userForm.id = user.id;
|
||||
userForm.name = user.name;
|
||||
userForm.groupId = user.groupId;
|
||||
userForm.previewUrl = user.photoUrl ? `/uploads/${user.photoUrl}` : '';
|
||||
} else {
|
||||
userDialog.isEdit = false;
|
||||
userForm.id = null;
|
||||
userForm.name = '';
|
||||
userForm.groupId = null;
|
||||
userForm.previewUrl = '';
|
||||
}
|
||||
};
|
||||
// Forms
|
||||
const userForm = reactive({ id: null, name: '', groupId: null, previewUrl: '' });
|
||||
const groupForm = reactive({ groupName: '' });
|
||||
|
||||
const handleFileChange = (uploadFile) => {
|
||||
const file = uploadFile.raw;
|
||||
if (file) {
|
||||
selectedFile.value = file;
|
||||
userForm.previewUrl = URL.createObjectURL(file);
|
||||
}
|
||||
};
|
||||
// API Base
|
||||
const API_USER = '/api/users';
|
||||
const API_GROUP = '/api/groups';
|
||||
|
||||
const submitUser = async () => {
|
||||
if (!userForm.name) return ElementPlus.ElMessage.warning('请输入姓名');
|
||||
if (!userDialog.isEdit && !selectedFile.value) return ElementPlus.ElMessage.warning('请选择照片');
|
||||
const handleSelect = (key) => activeMenu.value = key;
|
||||
|
||||
submitting.value = true;
|
||||
const formData = new FormData();
|
||||
formData.append('name', userForm.name);
|
||||
if (userForm.groupId) formData.append('groupId', userForm.groupId);
|
||||
if (selectedFile.value) formData.append('photo', selectedFile.value);
|
||||
|
||||
try {
|
||||
const url = userDialog.isEdit ? `${API_USER}/update/${userForm.id}` : `${API_USER}/add`;
|
||||
const res = await fetch(url, { method: 'POST', body: formData }).then(r => r.json());
|
||||
|
||||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success(userDialog.isEdit ? '更新成功' : '注册成功');
|
||||
userDialog.visible = false;
|
||||
loadUsers();
|
||||
loadData(); // Refresh all
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg || '操作失败');
|
||||
}
|
||||
} catch (e) {
|
||||
ElementPlus.ElMessage.error('网络错误');
|
||||
}
|
||||
submitting.value = false;
|
||||
};
|
||||
|
||||
const handleDeleteUser = (user) => {
|
||||
ElementPlus.ElMessageBox.confirm(`确定删除用户 ${user.name} 吗?`, '提示', { type: 'warning' })
|
||||
.then(async () => {
|
||||
const res = await fetch(`${API_USER}/${user.id}`, { method: 'DELETE' }).then(r => r.json());
|
||||
// Load Data
|
||||
const loadGroups = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_GROUP}/list`).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
loadUsers();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg);
|
||||
groups.value = res.data;
|
||||
res.data.forEach(g => groupMap[g.id] = g.groupName);
|
||||
}
|
||||
}).catch(() => { });
|
||||
};
|
||||
} catch (e) { console.error(e); }
|
||||
};
|
||||
|
||||
const handleSelectionChange = (val) => {
|
||||
selectedUsers.value = val;
|
||||
};
|
||||
const searchForm = reactive({ name: '', groupId: null });
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedUsers.value.length === 0) return;
|
||||
const loadUserCount = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_USER}/count`).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
totalUserCount.value = res.data;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load user count", e);
|
||||
}
|
||||
};
|
||||
|
||||
ElementPlus.ElMessageBox.confirm(`确定删除选中的 ${selectedUsers.value.length} 名用户吗?`, '提示', { type: 'warning' })
|
||||
.then(async () => {
|
||||
const ids = selectedUsers.value.map(u => u.id);
|
||||
try {
|
||||
const response = await fetch(`${API_USER}/deleteBatch`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(ids)
|
||||
});
|
||||
const loadUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (searchForm.name) params.append('name', searchForm.name);
|
||||
if (searchForm.groupId) params.append('groupId', searchForm.groupId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status} - 后端可能未更新`);
|
||||
}
|
||||
const res = await fetch(`${API_USER}/list?${params.toString()}`).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
users.value = res.data;
|
||||
}
|
||||
} catch (e) { ElementPlus.ElMessage.error('加载用户失败'); }
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const res = await response.json();
|
||||
const getGroupName = (id) => groupMap[id] || id;
|
||||
const hasFeature = (user) => user.featureData && user.featureData.length > 20;
|
||||
|
||||
// User Operations
|
||||
const openUserDialog = (user) => {
|
||||
userDialog.visible = true;
|
||||
selectedFile.value = null;
|
||||
|
||||
if (user) {
|
||||
userDialog.isEdit = true;
|
||||
userForm.id = user.id;
|
||||
userForm.name = user.name;
|
||||
userForm.groupId = user.groupId;
|
||||
userForm.previewUrl = user.photoUrl ? `/uploads/${user.photoUrl}` : '';
|
||||
} else {
|
||||
userDialog.isEdit = false;
|
||||
userForm.id = null;
|
||||
userForm.name = '';
|
||||
userForm.groupId = null;
|
||||
userForm.previewUrl = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (uploadFile) => {
|
||||
const file = uploadFile.raw;
|
||||
if (file) {
|
||||
selectedFile.value = file;
|
||||
userForm.previewUrl = URL.createObjectURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const submitUser = async () => {
|
||||
if (!userForm.name) return ElementPlus.ElMessage.warning('请输入姓名');
|
||||
if (!userDialog.isEdit && !selectedFile.value) return ElementPlus.ElMessage.warning('请选择照片');
|
||||
|
||||
submitting.value = true;
|
||||
const formData = new FormData();
|
||||
formData.append('name', userForm.name);
|
||||
if (userForm.groupId) formData.append('groupId', userForm.groupId);
|
||||
if (selectedFile.value) formData.append('photo', selectedFile.value);
|
||||
|
||||
try {
|
||||
const url = userDialog.isEdit ? `${API_USER}/update/${userForm.id}` : `${API_USER}/add`;
|
||||
const res = await fetch(url, { method: 'POST', body: formData }).then(r => r.json());
|
||||
|
||||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success(userDialog.isEdit ? '更新成功' : '注册成功');
|
||||
userDialog.visible = false;
|
||||
loadUsers();
|
||||
loadUserCount();
|
||||
loadData(); // Refresh all
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg || '操作失败');
|
||||
}
|
||||
} catch (e) {
|
||||
ElementPlus.ElMessage.error('网络错误');
|
||||
}
|
||||
submitting.value = false;
|
||||
};
|
||||
|
||||
const handleDeleteUser = (user) => {
|
||||
ElementPlus.ElMessageBox.confirm(`确定删除用户 ${user.name} 吗?`, '提示', { type: 'warning' })
|
||||
.then(async () => {
|
||||
const res = await fetch(`${API_USER}/${user.id}`, { method: 'DELETE' }).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success('批量删除成功');
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
loadUsers();
|
||||
loadUserCount();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ElementPlus.ElMessage.error('删除失败: ' + e.message);
|
||||
}
|
||||
}).catch(() => { });
|
||||
};
|
||||
}).catch(() => { });
|
||||
};
|
||||
|
||||
// Group Operations
|
||||
const openGroupDialog = () => {
|
||||
groupDialog.visible = true;
|
||||
groupForm.groupName = '';
|
||||
};
|
||||
const handleSelectionChange = (val) => {
|
||||
selectedUsers.value = val;
|
||||
};
|
||||
|
||||
const submitGroup = async () => {
|
||||
if (!groupForm.groupName) return ElementPlus.ElMessage.warning('请输入分组名称');
|
||||
submitting.value = true;
|
||||
try {
|
||||
const res = await fetch(`${API_GROUP}/add`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ groupName: groupForm.groupName })
|
||||
}).then(r => r.json());
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedUsers.value.length === 0) return;
|
||||
|
||||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success('分组创建成功');
|
||||
groupDialog.visible = false;
|
||||
loadGroups();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg);
|
||||
}
|
||||
} catch (e) { ElementPlus.ElMessage.error('网络错误'); }
|
||||
submitting.value = false;
|
||||
};
|
||||
ElementPlus.ElMessageBox.confirm(`确定删除选中的 ${selectedUsers.value.length} 名用户吗?`, '提示', { type: 'warning' })
|
||||
.then(async () => {
|
||||
const ids = selectedUsers.value.map(u => u.id);
|
||||
try {
|
||||
const response = await fetch(`${API_USER}/deleteBatch`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(ids)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status} - 后端可能未更新`);
|
||||
}
|
||||
|
||||
const res = await response.json();
|
||||
|
||||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success('批量删除成功');
|
||||
loadUsers();
|
||||
loadUserCount();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ElementPlus.ElMessage.error('删除失败: ' + e.message);
|
||||
}
|
||||
}).catch(() => { });
|
||||
};
|
||||
|
||||
// Group Operations
|
||||
const openGroupDialog = () => {
|
||||
groupDialog.visible = true;
|
||||
groupForm.groupName = '';
|
||||
};
|
||||
|
||||
const submitGroup = async () => {
|
||||
if (!groupForm.groupName) return ElementPlus.ElMessage.warning('请输入分组名称');
|
||||
submitting.value = true;
|
||||
try {
|
||||
const res = await fetch(`${API_GROUP}/add`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ groupName: groupForm.groupName })
|
||||
}).then(r => r.json());
|
||||
|
||||
const handleDeleteGroup = (group) => {
|
||||
ElementPlus.ElMessageBox.confirm(`确定删除分组 ${group.groupName} 吗? 如果分组下有用户将无法删除。`, '提示', { type: 'warning' })
|
||||
.then(async () => {
|
||||
const res = await fetch(`${API_GROUP}/${group.id}`, { method: 'DELETE' }).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
ElementPlus.ElMessage.success('分组创建成功');
|
||||
groupDialog.visible = false;
|
||||
loadGroups();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error('删除失败: ' + res.msg);
|
||||
ElementPlus.ElMessage.error(res.msg);
|
||||
}
|
||||
}).catch(() => { });
|
||||
};
|
||||
} catch (e) { ElementPlus.ElMessage.error('网络错误'); }
|
||||
submitting.value = false;
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
await loadGroups();
|
||||
await loadUsers();
|
||||
};
|
||||
const handleDeleteGroup = (group) => {
|
||||
ElementPlus.ElMessageBox.confirm(`确定删除分组 ${group.groupName} 吗? 如果分组下有用户将无法删除。`, '提示', { type: 'warning' })
|
||||
.then(async () => {
|
||||
const res = await fetch(`${API_GROUP}/${group.id}`, { method: 'DELETE' }).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
loadGroups();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error('删除失败: ' + res.msg);
|
||||
}
|
||||
}).catch(() => { });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
const loadData = async () => {
|
||||
await loadGroups();
|
||||
await loadUsers();
|
||||
await loadUserCount();
|
||||
};
|
||||
|
||||
return {
|
||||
activeMenu, handleSelect,
|
||||
users, groups, loading, submitting,
|
||||
getGroupName, hasFeature,
|
||||
userDialog, userForm, openUserDialog, submitUser, handleDeleteUser,
|
||||
handleSelectionChange, handleBatchDelete, selectedUsers,
|
||||
groupDialog, groupForm, openGroupDialog, submitGroup, handleDeleteGroup,
|
||||
loadUsers, loadGroups,
|
||||
loadUsers, loadGroups,
|
||||
handleFileChange,
|
||||
handleCommand,
|
||||
Plus: ElementPlusIconsVue.Plus,
|
||||
Refresh: ElementPlusIconsVue.Refresh
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
return {
|
||||
activeMenu, handleSelect,
|
||||
users, groups, loading, submitting,
|
||||
getGroupName, hasFeature,
|
||||
userDialog, userForm, openUserDialog, submitUser, handleDeleteUser,
|
||||
handleSelectionChange, handleBatchDelete, selectedUsers,
|
||||
groupDialog, groupForm, openGroupDialog, submitGroup, handleDeleteGroup,
|
||||
loadUsers, loadGroups,
|
||||
handleFileChange,
|
||||
handleCommand,
|
||||
searchForm,
|
||||
totalUserCount,
|
||||
Plus: ElementPlusIconsVue.Plus,
|
||||
Refresh: ElementPlusIconsVue.Refresh,
|
||||
Search: ElementPlusIconsVue.Search
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Register Icons
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
});
|
||||
|
||||
// Register Icons
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(ElementPlus);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
app.use(ElementPlus);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
Reference in New Issue