This commit is contained in:
胡圣锋 2025-11-20 21:24:10 +08:00
commit 2e550daa71
28 changed files with 2488 additions and 180 deletions

View File

@ -1,6 +1,7 @@
package com.ddnai.web.controller.psychology;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
@ -12,6 +13,8 @@ import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.ddnai.common.annotation.Anonymous;
import com.ddnai.common.annotation.Log;
import com.ddnai.common.core.controller.BaseController;
import com.ddnai.common.core.domain.AjaxResult;
@ -20,6 +23,7 @@ import com.ddnai.common.core.page.TableDataInfo;
import com.ddnai.common.enums.BusinessType;
import com.ddnai.common.utils.SecurityUtils;
import com.ddnai.common.utils.StringUtils;
import com.ddnai.common.utils.poi.ExcelUtil;
import com.ddnai.system.domain.psychology.PsyUserProfile;
import com.ddnai.system.service.ISysDeptService;
import com.ddnai.system.service.ISysPostService;
@ -235,4 +239,45 @@ public class PsyUserProfileController extends BaseController
{
return toAjax(profileService.deleteProfileByIds(profileIds));
}
// ==================== 导入导出功能 ====================
/**
* 导出用户档案列表
*/
@PreAuthorize("@ss.hasPermi('psychology:profile:export')")
@Log(title = "用户档案", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, PsyUserProfile profile)
{
List<PsyUserProfile> list = profileService.selectProfileList(profile);
ExcelUtil<PsyUserProfile> util = new ExcelUtil<PsyUserProfile>(PsyUserProfile.class);
util.exportExcel(response, list, "用户档案数据");
}
/**
* 下载用户档案导入模板
*/
@Anonymous
@GetMapping("/importTemplate")
public void importTemplate(HttpServletResponse response)
{
ExcelUtil<PsyUserProfile> util = new ExcelUtil<PsyUserProfile>(PsyUserProfile.class);
util.importTemplateExcel(response, "用户档案数据");
}
/**
* 导入用户档案数据
*/
@PreAuthorize("@ss.hasPermi('psychology:profile:import')")
@Log(title = "用户档案", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception
{
ExcelUtil<PsyUserProfile> util = new ExcelUtil<PsyUserProfile>(PsyUserProfile.class);
List<PsyUserProfile> profileList = util.importExcel(file.getInputStream());
String operName = getUsername();
String message = profileService.importProfile(profileList, updateSupport, operName);
return success(message);
}
}

View File

@ -178,33 +178,22 @@ public class SysLoginController
String infoNumberTrimmed = infoNumber.trim();
SysUser user = null;
// 优先通过信息编号查找用户档案然后通过userId查找用户
// 严格模式必须通过信息编号在用户档案中查找
// 只有在用户档案中存在的信息编号才能登录
com.ddnai.system.domain.psychology.PsyUserProfile profile = userProfileService.selectProfileByInfoNumber(infoNumberTrimmed);
if (profile != null && profile.getUserId() != null)
if (profile == null)
{
return AjaxResult.error("信息编号不存在,请先创建用户档案");
}
if (profile.getUserId() == null)
{
return AjaxResult.error("用户档案异常,未关联系统用户");
}
// 通过档案中的userId查找用户
user = userService.selectUserById(profile.getUserId());
}
// 如果通过信息编号找不到则使用原来的逻辑兼容旧版本
if (user == null)
{
// 支持两种方式1. 通过user_name查找 2. 如果是数字通过user_id查找
user = userService.selectUserByUserName(infoNumberTrimmed);
// 如果通过user_name找不到且输入的是数字尝试通过user_id查找
if (user == null && StringUtils.isNumeric(infoNumberTrimmed))
{
try
{
Long userId = Long.parseLong(infoNumberTrimmed);
user = userService.selectUserById(userId);
}
catch (NumberFormatException e)
{
// 数字格式错误忽略
}
}
}
// 检查系统有效时间系统管理员不受限制
try

View File

@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.ddnai.common.annotation.Anonymous;
import com.ddnai.common.annotation.Log;
import com.ddnai.common.core.controller.BaseController;
import com.ddnai.common.core.domain.AjaxResult;
@ -87,7 +88,8 @@ public class SysUserController extends BaseController
return success(message);
}
@PostMapping("/importTemplate")
@Anonymous
@GetMapping("/importTemplate")
public void importTemplate(HttpServletResponse response)
{
ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);

View File

@ -21,7 +21,7 @@ public class SysUser extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 用户ID */
/** 用户ID - 只在导出时显示,导入时不需要,通过信息编号自动设置 */
@Excel(name = "用户序号", type = Type.EXPORT, cellType = ColumnType.NUMERIC, prompt = "用户编号")
private Long userId;
@ -92,6 +92,44 @@ public class SysUser extends BaseEntity
/** 角色ID */
private Long roleId;
// ==================== 心理档案相关字段 ====================
/** 信息编号 */
@Excel(name = "信息编号")
private String infoNumber;
/** 档案类型 */
@Excel(name = "档案类型", readConverterExp = "standard=标准,child=儿童,adult=成人,senior=老年")
private String profileType;
/** 身份证号 */
@Excel(name = "身份证号")
private String idCard;
/** 生日 */
@Excel(name = "生日", width = 30, dateFormat = "yyyy-MM-dd")
private Date birthday;
/** 学历 */
@Excel(name = "学历")
private String education;
/** 职业 */
@Excel(name = "职业")
private String occupation;
/** 地址 */
@Excel(name = "地址")
private String address;
/** 紧急联系人 */
@Excel(name = "紧急联系人")
private String emergencyContact;
/** 紧急联系电话 */
@Excel(name = "紧急联系电话")
private String emergencyPhone;
public SysUser()
{
@ -310,6 +348,98 @@ public class SysUser extends BaseEntity
this.roleId = roleId;
}
// ==================== 心理档案字段的 Getter/Setter ====================
public String getInfoNumber()
{
return infoNumber;
}
public void setInfoNumber(String infoNumber)
{
this.infoNumber = infoNumber;
}
public String getProfileType()
{
return profileType;
}
public void setProfileType(String profileType)
{
this.profileType = profileType;
}
public String getIdCard()
{
return idCard;
}
public void setIdCard(String idCard)
{
this.idCard = idCard;
}
public Date getBirthday()
{
return birthday;
}
public void setBirthday(Date birthday)
{
this.birthday = birthday;
}
public String getEducation()
{
return education;
}
public void setEducation(String education)
{
this.education = education;
}
public String getOccupation()
{
return occupation;
}
public void setOccupation(String occupation)
{
this.occupation = occupation;
}
public String getAddress()
{
return address;
}
public void setAddress(String address)
{
this.address = address;
}
public String getEmergencyContact()
{
return emergencyContact;
}
public void setEmergencyContact(String emergencyContact)
{
this.emergencyContact = emergencyContact;
}
public String getEmergencyPhone()
{
return emergencyPhone;
}
public void setEmergencyPhone(String emergencyPhone)
{
this.emergencyPhone = emergencyPhone;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)

View File

@ -29,8 +29,10 @@ import com.ddnai.framework.manager.AsyncManager;
import com.ddnai.framework.manager.factory.AsyncFactory;
import com.ddnai.framework.security.context.AuthenticationContextHolder;
import com.ddnai.common.core.domain.entity.SysUser;
import com.ddnai.common.core.domain.entity.SysRole;
import com.ddnai.system.service.ISysConfigService;
import com.ddnai.system.service.ISysUserService;
import com.ddnai.system.service.psychology.IPsyUserProfileService;
/**
* 登录校验方法
@ -42,6 +44,9 @@ public class SysLoginService
{
private static final Logger log = LoggerFactory.getLogger(SysLoginService.class);
@Autowired
private IPsyUserProfileService userProfileService;
@Autowired
private TokenService tokenService;
@ -100,6 +105,36 @@ public class SysLoginService
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 检查学员用户是否有档案
SysUser user = loginUser.getUser();
if (user != null && user.getRoles() != null)
{
boolean isStudent = false;
for (SysRole role : user.getRoles())
{
if (role != null && ("student".equalsIgnoreCase(role.getRoleKey()) ||
(role.getRoleName() != null && role.getRoleName().contains("学员"))))
{
isStudent = true;
break;
}
}
// 如果是学员角色必须有档案才能登录
if (isStudent)
{
com.ddnai.system.domain.psychology.PsyUserProfile profile =
userProfileService.selectProfileByUserId(user.getUserId());
if (profile == null || StringUtils.isEmpty(profile.getInfoNumber()))
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, "学员账号必须有用户档案才能登录"));
throw new ServiceException("该账号未创建用户档案,请联系管理员创建档案后再登录");
}
log.info("学员登录验证通过,用户: {}, 信息编号: {}", username, profile.getInfoNumber());
}
}
recordLoginInfo(loginUser.getUserId());
// 生成token
return tokenService.createToken(loginUser);

View File

@ -1,5 +1,6 @@
package com.ddnai.system.domain.psychology;
import com.ddnai.common.annotation.Excel;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
@ -17,44 +18,54 @@ public class PsyUserProfile extends BaseEntity
/** 档案ID */
private Long profileId;
/** 用户ID关联sys_user */
/** 用户ID关联sys_user - 不在Excel模板中显示通过信息编号自动设置 */
private Long userId;
/** 档案类型standard标准 child儿童 adult成人 senior老年 */
@Excel(name = "档案类型", readConverterExp = "standard=标准,child=儿童,adult=成人,senior=老年")
private String profileType;
/** 头像 */
private String avatar;
/** 身份证号 */
@Excel(name = "身份证号")
private String idCard;
/** 档案数据JSON格式 */
private String profileData;
/** 姓名 */
@Excel(name = "姓名")
private String userName;
/** 电话 */
@Excel(name = "电话")
private String phone;
/** 生日 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "生日", width = 30, dateFormat = "yyyy-MM-dd")
private java.util.Date birthday;
/** 学历 */
@Excel(name = "学历")
private String education;
/** 职业 */
@Excel(name = "职业")
private String occupation;
/** 地址 */
@Excel(name = "地址")
private String address;
/** 紧急联系人 */
@Excel(name = "紧急联系人")
private String emergencyContact;
/** 紧急联系电话 */
@Excel(name = "紧急联系电话")
private String emergencyPhone;
/** 病史 */
@ -70,6 +81,7 @@ public class PsyUserProfile extends BaseEntity
private String deptName;
/** 信息编号 */
@Excel(name = "信息编号")
private String infoNumber;
public Long getProfileId()

View File

@ -517,6 +517,20 @@ public class SysUserServiceImpl implements ISysUserService
{
try
{
// 如果有信息编号使用信息编号作为登录账号
if (StringUtils.isNotEmpty(user.getInfoNumber()))
{
user.setUserName(user.getInfoNumber()); // 登录账号 = 信息编号
}
// 如果没有用户名跳过
if (StringUtils.isEmpty(user.getUserName()))
{
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、用户名或信息编号不能为空");
continue;
}
// 验证是否存在这个用户
SysUser u = userMapper.selectUserByUserName(user.getUserName());
if (StringUtils.isNull(u))

View File

@ -314,7 +314,37 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
@Override
public int deleteProfileByIds(Long[] profileIds)
{
return profileMapper.deleteProfileByIds(profileIds);
// 先获取所有要删除的档案记录对应的用户ID
java.util.List<Long> userIdsToDelete = new java.util.ArrayList<>();
for (Long profileId : profileIds)
{
PsyUserProfile profile = profileMapper.selectProfileById(profileId);
if (profile != null && profile.getUserId() != null)
{
userIdsToDelete.add(profile.getUserId());
}
}
// 删除档案
int result = profileMapper.deleteProfileByIds(profileIds);
// 删除对应的用户
if (!userIdsToDelete.isEmpty())
{
Long[] userIds = userIdsToDelete.toArray(new Long[0]);
try
{
userService.deleteUserByIds(userIds);
log.info("删除档案同时删除了对应的用户用户ID: {}", userIdsToDelete);
}
catch (Exception e)
{
log.error("删除用户失败用户ID: {}", userIdsToDelete, e);
// 即使删除用户失败档案已经删除也返回成功
}
}
return result;
}
private void validateInfoNumberUnique(String infoNumber, Long profileId)
@ -336,16 +366,22 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
{
throw new ServiceException("信息编号不能为空");
}
SysUser user = new SysUser();
// 使用信息编号作为登录账号用户名
String loginAccount = profile.getInfoNumber();
user.setUserName(loginAccount);
// 不设置userId让数据库自动生成
// userId和信息编号不需要保持一致信息编号用于登录
user.setUserName(loginAccount); // 登录账号 = 信息编号
user.setNickName(StringUtils.isNotEmpty(profile.getUserName()) ? profile.getUserName() : loginAccount);
user.setPhonenumber(profile.getPhone());
user.setDeptId(profile.getDeptId());
user.setStatus("0");
user.setPassword(SecurityUtils.encryptPassword(resolveInitPassword()));
user.setCreateBy(SecurityUtils.getUsername());
user.setRemark("由用户档案自动创建");
user.setRemark("由用户档案自动创建(登录账号:" + loginAccount + "");
Long studentRoleId = resolveStudentRoleId();
if (studentRoleId != null)
{
@ -355,11 +391,16 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
{
log.warn("未找到学生角色,自动创建的用户将没有角色,请检查角色配置");
}
if (!userService.checkUserNameUnique(user))
{
throw new ServiceException("登录账号 '" + loginAccount + "' 已存在,请更换信息编号");
}
log.info("自动创建用户,登录账号: {}", loginAccount);
userService.insertUser(user);
log.info("用户创建成功用户ID: {}, 登录账号: {}", user.getUserId(), user.getUserName());
return user;
}
@ -418,5 +459,86 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
}
return null;
}
/**
* 导入用户档案数据
*
* @param profileList 用户档案数据列表
* @param isUpdateSupport 是否更新支持如果已存在则进行更新数据
* @param operName 操作用户
* @return 结果
*/
@Override
public String importProfile(List<PsyUserProfile> profileList, Boolean isUpdateSupport, String operName)
{
if (StringUtils.isNull(profileList) || profileList.size() == 0)
{
throw new ServiceException("导入用户档案数据不能为空!");
}
int successNum = 0;
int failureNum = 0;
StringBuilder successMsg = new StringBuilder();
StringBuilder failureMsg = new StringBuilder();
for (PsyUserProfile profile : profileList)
{
try
{
// 设置创建者
profile.setCreateBy(operName);
// 验证信息编号
if (StringUtils.isEmpty(profile.getInfoNumber()))
{
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、档案信息编号为空");
continue;
}
// 根据信息编号查询是否存在
PsyUserProfile existProfile = profileMapper.selectProfileByInfoNumber(profile.getInfoNumber());
if (StringUtils.isNull(existProfile))
{
// 新增档案
this.insertProfile(profile);
successNum++;
successMsg.append("<br/>").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 导入成功");
}
else if (isUpdateSupport)
{
// 更新档案
profile.setProfileId(existProfile.getProfileId());
profile.setUserId(existProfile.getUserId());
profile.setUpdateBy(operName);
this.updateProfile(profile);
successNum++;
successMsg.append("<br/>").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 更新成功");
}
else
{
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 已存在");
}
}
catch (Exception e)
{
failureNum++;
String msg = "<br/>" + failureNum + "、信息编号 " + profile.getInfoNumber() + " 导入失败:";
failureMsg.append(msg).append(e.getMessage());
log.error(msg, e);
}
}
if (failureNum > 0)
{
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
throw new ServiceException(failureMsg.toString());
}
else
{
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
}
return successMsg.toString();
}
}

View File

@ -65,5 +65,15 @@ public interface IPsyUserProfileService
* @return 结果
*/
public int deleteProfileByIds(Long[] profileIds);
/**
* 导入用户档案数据
*
* @param profileList 用户档案数据列表
* @param isUpdateSupport 是否更新支持如果已存在则进行更新数据
* @param operName 操作用户
* @return 结果
*/
public String importProfile(List<PsyUserProfile> profileList, Boolean isUpdateSupport, String operName);
}

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
@ -152,7 +152,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<insert id="insertUser" parameterType="SysUser" useGeneratedKeys="true" keyProperty="userId">
insert into sys_user(
<if test="userId != null and userId != 0">user_id,</if>
<if test="deptId != null and deptId != 0">dept_id,</if>
<if test="userName != null and userName != ''">user_name,</if>
<if test="nickName != null and nickName != ''">nick_name,</if>
@ -167,7 +166,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="remark != null and remark != ''">remark,</if>
create_time
)values(
<if test="userId != null and userId != ''">#{userId},</if>
<if test="deptId != null and deptId != ''">#{deptId},</if>
<if test="userName != null and userName != ''">#{userName},</if>
<if test="nickName != null and nickName != ''">#{nickName},</if>

View File

@ -21,8 +21,10 @@ router.beforeEach((to, from, next) => {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
/* has token*/
if (to.path === '/login' || to.path === '/admin-login') {
// 已登录,统一跳回首页
next({ path: '/' })
// 已登录,根据角色跳转到相应页面
const userRoles = store.getters.roles || []
const isStudentRole = userRoles.some(role => role === 'student' || role.includes('学员'))
next({ path: isStudentRole ? '/student/tests' : '/' })
NProgress.done()
} else if (isWhiteList(to.path)) {
next()
@ -39,21 +41,22 @@ router.beforeEach((to, from, next) => {
const isStudentRole = userRoles.some(role => role === 'student' || role.includes('学员'))
if (isStudentRole) {
const allowPaths = [
'/',
'/index'
]
// 学员访问首页时重定向到测试题列表
if (to.path === '/' || to.path === '/index') {
next({ path: '/student/tests', replace: true })
return
}
const allowPrefixes = [
'/student',
'/psychology/assessment/taking',
'/psychology/questionnaire',
'/psychology/assessment/report'
]
const isAllowed = allowPaths.includes(to.path) || allowPrefixes.some(prefix => to.path.startsWith(prefix))
const isAllowed = allowPrefixes.some(prefix => to.path.startsWith(prefix))
if (isAllowed) {
next()
} else {
next({ path: '/' })
next({ path: '/student/tests' })
}
} else {
// 管理员角色,生成路由
@ -77,21 +80,22 @@ router.beforeEach((to, from, next) => {
const isStudentRole = userRoles.some(role => role === 'student' || role.includes('学员'))
if (isStudentRole) {
const allowPaths = [
'/',
'/index'
]
// 学员访问首页时重定向到测试题列表
if (to.path === '/' || to.path === '/index') {
next({ path: '/student/tests', replace: true })
return
}
const allowPrefixes = [
'/student',
'/psychology/assessment/taking',
'/psychology/questionnaire',
'/psychology/assessment/report'
]
const isAllowed = allowPaths.includes(to.path) || allowPrefixes.some(prefix => to.path.startsWith(prefix))
const isAllowed = allowPrefixes.some(prefix => to.path.startsWith(prefix))
if (isAllowed) {
next()
} else {
next({ path: '/' })
next({ path: '/student/tests' })
}
} else {
next()

View File

@ -106,6 +106,7 @@ export const constantRoutes = [
{
path: '/student/tests',
component: () => import('@/views/student/tests'),
name: 'StudentTests',
hidden: true,
meta: { title: '心理测试题' }
},

View File

@ -16,6 +16,7 @@ const getters = {
permission_routes: state => state.permission.routes,
topbarRouters: state => state.permission.topbarRouters,
defaultRoutes: state => state.permission.defaultRoutes,
sidebarRouters: state => state.permission.sidebarRouters
sidebarRouters: state => state.permission.sidebarRouters,
infoNumber: state => state.user.infoNumber
}
export default getters

View File

@ -1,7 +1,7 @@
import router from '@/router'
import { MessageBox, } from 'element-ui'
import { login, logout, getInfo, studentLogin } from '@/api/login'
import { getToken, setToken, removeToken, getRole, setRole } from '@/utils/auth'
import { getToken, setToken, removeToken, getRole, setRole, getInfoNumber, setInfoNumber } from '@/utils/auth'
import { isHttp, isEmpty } from "@/utils/validate"
import defAva from '@/assets/images/profile.jpg'
@ -22,7 +22,8 @@ const user = {
nickName: '',
avatar: '',
roles: initRoles(),
permissions: []
permissions: [],
infoNumber: getInfoNumber() || '' // 学员信息编号,从 Cookie 中恢复
},
mutations: {
@ -58,6 +59,11 @@ const user = {
},
SET_PERMISSIONS: (state, permissions) => {
state.permissions = permissions
},
SET_INFO_NUMBER: (state, infoNumber) => {
state.infoNumber = infoNumber
// 持久化到 Cookie
setInfoNumber(infoNumber)
}
},
@ -132,6 +138,7 @@ const user = {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
commit('SET_PERMISSIONS', [])
commit('SET_INFO_NUMBER', '')
removeToken()
resolve()
}).catch(error => {
@ -145,6 +152,7 @@ const user = {
return new Promise(resolve => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
commit('SET_INFO_NUMBER', '')
removeToken()
resolve()
})
@ -165,6 +173,8 @@ const user = {
// 设置学员token
setToken(res.token)
commit('SET_TOKEN', res.token)
// 保存信息编号到 store
commit('SET_INFO_NUMBER', infoNumber)
// 学员登录成功后需要调用GetInfo获取用户信息因为学员也是系统用户只是角色不同
// 这里不设置角色等GetInfo获取后再设置
resolve()

View File

@ -2,6 +2,7 @@ import Cookies from 'js-cookie'
const TokenKey = 'Admin-Token'
const RoleKey = 'User-Role'
const InfoNumberKey = 'Info-Number'
export function getToken() {
return Cookies.get(TokenKey)
@ -14,6 +15,7 @@ export function setToken(token) {
export function removeToken() {
Cookies.remove(TokenKey)
Cookies.remove(RoleKey)
Cookies.remove(InfoNumberKey)
}
// 获取用户角色
@ -29,3 +31,17 @@ export function setRole(role) {
return Cookies.remove(RoleKey)
}
}
// 获取信息编号
export function getInfoNumber() {
return Cookies.get(InfoNumberKey)
}
// 设置信息编号
export function setInfoNumber(infoNumber) {
if (infoNumber) {
return Cookies.set(InfoNumberKey, infoNumber)
} else {
return Cookies.remove(InfoNumberKey)
}
}

View File

@ -74,9 +74,6 @@
<span v-if="!loading"> </span>
<span v-else> 中...</span>
</el-button>
<div style="float: right;" v-if="register">
<router-link class="link-type" :to="'/register'">立即注册</router-link>
</div>
</el-form-item>
<el-form-item style="width:100%; text-align: center; margin-top: 10px;">
<a class="student-login-link" @click="switchToStudent">
@ -250,7 +247,8 @@ export default {
this.$store.dispatch("StudentLogin", loginData).then(() => {
// GetInfo
this.$store.dispatch("GetInfo").then(() => {
const targetPath = this.resolveRedirect("/")
// redirect
const targetPath = this.resolveRedirect("/student/tests")
this.$router.push(targetPath).catch(()=>{})
}).catch(() => {
this.loading = false

View File

@ -145,7 +145,10 @@ export default {
this.assessmentId = this.$route.query.assessmentId;
if (!this.assessmentId) {
this.$modal.msgError("测评ID不能为空");
this.$router.push('/psychology/assessment');
//
const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
this.$router.push(isStudent ? '/student/tests' : '/psychology/assessment');
return;
}
this.loadAssessment();
@ -162,7 +165,10 @@ export default {
const assessment = assessmentRes.data;
if (!assessment) {
this.$modal.msgError("测评不存在");
this.$router.push('/psychology/assessment');
//
const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
this.$router.push(isStudent ? '/student/tests' : '/psychology/assessment');
return;
}
@ -172,7 +178,10 @@ export default {
//
if (this.itemList.length === 0) {
this.$modal.msgWarning("该量表暂无题目,请联系管理员添加题目");
this.$router.push('/psychology/assessment');
//
const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
this.$router.push(isStudent ? '/student/tests' : '/psychology/assessment');
return;
}
@ -309,14 +318,20 @@ export default {
this.$modal.confirm('确定要暂停测评吗?您可以稍后继续完成。').then(() => {
pauseAssessment(this.assessmentId).then(() => {
this.$modal.msgSuccess("测评已暂停");
this.$router.push('/psychology/assessment');
//
const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
this.$router.push(isStudent ? '/student/tests' : '/psychology/assessment');
});
});
},
/** 退出 */
handleExit() {
this.$modal.confirm('确定要退出测评吗?已答题目将会保存。').then(() => {
this.$router.push('/psychology/assessment');
//
const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
this.$router.push(isStudent ? '/student/tests' : '/psychology/assessment');
});
},
/** 提交测评 */
@ -331,7 +346,10 @@ export default {
this.$modal.confirm('确定要提交测评吗?提交后将不能修改。').then(() => {
submitAssessment(this.assessmentId).then(response => {
this.$modal.msgSuccess(response.msg || "测评已提交,报告已生成");
this.$router.push('/psychology/assessment');
//
const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
this.$router.push(isStudent ? '/student/tests' : '/psychology/assessment');
}).catch(error => {
this.$modal.msgError(error.msg || "提交失败,请重试");
});
@ -461,7 +479,10 @@ export default {
submitAssessment(this.assessmentId).then(response => {
this.loading = false;
this.$modal.msgSuccess(response.msg || "测评已提交,报告已生成");
this.$router.push('/psychology/assessment');
//
const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
this.$router.push(isStudent ? '/student/tests' : '/psychology/assessment');
}).catch(error => {
this.loading = false;
this.$modal.msgError(error.msg || "提交失败,请重试");

View File

@ -9,14 +9,7 @@
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="用户ID" prop="userId">
<el-input
v-model="queryParams.userId"
placeholder="请输入用户ID"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<!-- 用户ID已隐藏只使用信息编号 -->
<el-form-item label="手机号码" prop="phone">
<el-input
v-model="queryParams.phone"
@ -39,14 +32,6 @@
<el-option label="老年" value="senior" />
</el-select>
</el-form-item>
<el-form-item label="身份证号" prop="idCard">
<el-input
v-model="queryParams.idCard"
placeholder="请输入身份证号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="信息编号" prop="infoNumber">
<el-input
v-model="queryParams.infoNumber"
@ -57,7 +42,6 @@
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
@ -72,16 +56,6 @@
v-hasPermi="['psychology:profile:add']"
>新增档案</el-button>
</el-col>
<el-col :span="1.8">
<el-button
type="primary"
plain
icon="el-icon-user"
size="mini"
@click="handleAddUser"
v-hasPermi="['psychology:profile:add']"
>创建用户</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
@ -104,13 +78,33 @@
v-hasPermi="['psychology:profile:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
plain
icon="el-icon-upload2"
size="mini"
@click="handleImport"
v-hasPermi="['psychology:profile:import']"
>导入</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['psychology:profile:export']"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table ref="profileTable" v-loading="loading" :data="profileList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" prop="profileId" width="80" />
<el-table-column label="用户ID" align="center" prop="userId" width="100" />
<!-- 用户ID列已隐藏只显示信息编号 -->
<el-table-column label="信息编号" align="center" prop="infoNumber" width="120" />
<el-table-column label="档案类型" align="center" prop="profileType" width="120">
<template slot-scope="scope">
@ -123,7 +117,6 @@
</el-table-column>
<el-table-column label="姓名" align="center" prop="userName" width="100" />
<el-table-column label="电话" align="center" prop="phone" width="120" />
<el-table-column label="身份证号" align="center" prop="idCard" width="180" />
<el-table-column label="生日" align="center" prop="birthday" width="120">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.birthday, '{y}-{m}-{d}') }}</span>
@ -147,13 +140,6 @@
@click="handleUpdate(scope.row)"
v-hasPermi="['psychology:profile:edit']"
>档案</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-user"
@click="handleEditUser(scope.row)"
v-hasPermi="['psychology:profile:edit']"
>用户</el-button>
<el-button
size="mini"
type="text"
@ -177,21 +163,6 @@
<el-dialog :title="title" :visible.sync="open" width="900px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-row>
<el-col :span="12">
<el-form-item prop="userId">
<template slot="label">
用户ID
<span style="color:#909399;font-size:12px;">留空自动创建</span>
</template>
<el-input-number
v-model="form.userId"
:min="1"
:disabled="form.profileId != undefined"
:controls="false"
placeholder="不填将自动创建用户"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="档案类型" prop="profileType">
<el-select v-model="form.profileType" placeholder="请选择档案类型">
@ -202,6 +173,9 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<!-- 用户ID已隐藏会通过信息编号自动填充 -->
</el-col>
</el-row>
<el-row>
<el-col :span="12">
@ -374,12 +348,32 @@
<el-button @click="cancelUser"> </el-button>
</div>
</el-dialog>
<!-- 用户档案导入对话框 -->
<el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
<el-upload ref="upload" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" :action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip text-center" slot="tip">
<div class="el-upload__tip" slot="tip">
<el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户档案数据
</div>
<span>仅允许导入xlsxlsx格式文件</span>
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板</el-link>
</div>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitFileForm"> </el-button>
<el-button @click="upload.open = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listProfile, getProfile, getProfileByUserId, delProfile, addProfile, updateProfile, getUserInfo, addUserInProfile, getUserInfoById, updateUserInProfile, delUserInProfile } from "@/api/psychology/profile"
import { deptTreeSelect } from "@/api/system/user"
import { getToken } from "@/utils/auth"
import Treeselect from "@riophae/vue-treeselect"
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
@ -484,6 +478,21 @@ export default {
trigger: "blur"
}
]
},
//
upload: {
//
open: false,
//
title: "",
//
isUploading: false,
//
updateSupport: 0,
//
headers: { Authorization: "Bearer " + getToken() },
//
url: process.env.VUE_APP_BASE_API + "/psychology/profile/importData"
}
}
},
@ -545,7 +554,8 @@ export default {
//
handleInfoNumberInput(value) {
//
this.form.infoNumber = value.replace(/\D/g, '')
const numericValue = value.replace(/\D/g, '')
this.form.infoNumber = numericValue
},
//
handleUserNameInput(value) {
@ -852,6 +862,42 @@ export default {
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
},
/** 导出按钮操作 */
handleExport() {
this.download('psychology/profile/export', {
...this.queryParams
}, `profile_${new Date().getTime()}.xlsx`)
},
/** 导入按钮操作 */
handleImport() {
this.upload.title = "用户档案导入"
this.upload.open = true
},
/** 下载模板操作 */
importTemplate() {
window.location.href = process.env.VUE_APP_BASE_API + '/psychology/profile/importTemplate'
},
//
handleFileUploadProgress(event, file, fileList) {
this.upload.isUploading = true
},
//
handleFileSuccess(response, file, fileList) {
this.upload.open = false
this.upload.isUploading = false
this.$refs.upload.clearFiles()
this.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true })
this.getList()
},
//
submitFileForm() {
const file = this.$refs.upload.uploadFiles
if (!file || file.length === 0 || !file[0].name.toLowerCase().endsWith('.xls') && !file[0].name.toLowerCase().endsWith('.xlsx')) {
this.$modal.msgError('请选择后缀为 "xls"或"xlsx"的文件。')
return
}
this.$refs.upload.submit()
}
}
}

View File

@ -106,7 +106,10 @@ export default {
startAnswerDirectly() {
if (!this.form.questionnaireId) {
this.$modal.msgError("问卷ID不能为空");
this.$router.push('/psychology/scale');
//
const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
this.$router.push(isStudent ? '/student/tests' : '/psychology/scale');
return;
}
@ -131,7 +134,10 @@ export default {
},
/** 返回 */
handleBack() {
this.$router.push('/psychology/scale');
//
const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
this.$router.push(isStudent ? '/student/tests' : '/psychology/scale');
}
}
};

View File

@ -216,7 +216,10 @@ export default {
this.answerId = this.$route.query.answerId;
if (!this.answerId) {
this.$modal.msgError("答题ID不能为空");
this.$router.push('/psychology/scale');
//
const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
this.$router.push(isStudent ? '/student/tests' : '/psychology/scale');
return;
}
this.loadAnswer();
@ -232,7 +235,10 @@ export default {
const answer = answerRes.data;
if (!answer) {
this.$modal.msgError("答题记录不存在");
this.$router.push('/psychology/scale');
//
const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
this.$router.push(isStudent ? '/student/tests' : '/psychology/scale');
return;
}
@ -256,7 +262,10 @@ export default {
if (this.itemList.length === 0) {
this.$modal.msgWarning("该问卷暂无题目,请联系管理员添加题目");
this.$router.push('/psychology/scale');
//
const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
this.$router.push(isStudent ? '/student/tests' : '/psychology/scale');
return;
}
@ -449,7 +458,10 @@ export default {
/** 退出 */
handleExit() {
this.$modal.confirm('确定要退出答题吗?已答题目将会保存。').then(() => {
this.$router.push('/psychology/scale');
//
const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
this.$router.push(isStudent ? '/student/tests' : '/psychology/scale');
});
},
/** 提交问卷 */
@ -480,7 +492,10 @@ export default {
submitQuestionnaireAnswer(this.answerId).then(response => {
this.loading = false;
this.$modal.msgSuccess(response.msg || "问卷已提交");
this.$router.push('/psychology/scale');
//
const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
this.$router.push(isStudent ? '/student/tests' : '/psychology/scale');
}).catch(error => {
this.loading = false;
this.$modal.msgError(error.msg || "提交失败,请重试");

View File

@ -72,6 +72,34 @@
<div v-if="reportForm.pdfPath">
<el-button type="primary" icon="el-icon-download" @click="handleDownloadPDF">下载PDF报告</el-button>
</div>
<!-- AI分析 -->
<el-divider content-position="left">AI智能分析</el-divider>
<div class="ai-analysis-section">
<el-button
type="primary"
icon="el-icon-magic-stick"
@click="handleAIAnalysis"
:loading="aiLoading"
:disabled="!reportForm.reportContent"
>
{{ aiLoading ? '分析中...' : 'AI分析' }}
</el-button>
<div v-if="aiError" class="ai-error">
<el-alert
:title="aiError"
type="error"
:closable="false"
show-icon>
</el-alert>
</div>
<div v-if="aiResult" class="ai-result">
<h3 class="ai-result-title">
<i class="el-icon-magic-stick"></i> AI分析结果
</h3>
<div class="ai-result-content" v-html="aiResult"></div>
</div>
</div>
</el-card>
<!-- 报告编辑对话框 -->
@ -88,23 +116,10 @@
</el-select>
</el-form-item>
<el-form-item label="报告摘要" prop="summary">
<el-input
v-model="editForm.summary"
type="textarea"
:rows="4"
placeholder="请输入报告摘要"
/>
<Editor v-model="editForm.summary" :min-height="150" />
</el-form-item>
<el-form-item label="报告内容" prop="reportContent">
<el-input
v-model="editForm.reportContent"
type="textarea"
:rows="15"
placeholder="请输入报告内容支持HTML格式"
/>
<div style="margin-top: 10px; color: #909399; font-size: 12px;">
<i class="el-icon-info"></i> 提示报告内容支持HTML格式可以包含标题段落列表等格式
</div>
<Editor v-model="editForm.reportContent" :min-height="400" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
@ -119,9 +134,12 @@
import { getReport, getReportByAssessmentId, updateReportWithType } from "@/api/psychology/report";
import { getQuestionnaireRankList } from "@/api/psychology/questionnaireAnswer";
import request from '@/utils/request';
import axios from 'axios';
import Editor from "@/components/Editor";
export default {
name: "ReportDetail",
components: { Editor },
data() {
return {
loading: true,
@ -129,6 +147,10 @@ export default {
sourceType: null,
rankList: [],
rankLoading: false,
// AI
aiLoading: false,
aiResult: '',
aiError: '',
//
editOpen: false,
editForm: {},
@ -291,6 +313,129 @@ export default {
if (this.$refs["editForm"]) {
this.$refs["editForm"].resetFields();
}
},
/** AI分析 */
async handleAIAnalysis() {
if (!this.reportForm.reportContent) {
this.$modal.msgWarning("报告内容为空,无法进行分析");
return;
}
this.aiLoading = true;
this.aiError = '';
this.aiResult = '';
// Ollama API
const OLLAMA_URL = 'http://192.168.0.106:11434/api/generate';
const MODEL = 'deepseek-r1:32b';
//
const SYSTEM_PROMPT = [
'你是专业心理测评报告分析师,请根据用户提供的报告内容进行深度分析。要求:',
'1. 提取报告的核心信息和关键指标;',
'2. 分析测评结果的含义和可能的影响;',
'3. 提供专业、客观、易懂的分析解读500-800字',
'4. 使用结构化的格式输出,包含:核心结论、详细分析、建议、总体结论四个部分;',
'5. 仅输出分析结果,不添加额外建议、问候语或思考过程;',
'6. 使用HTML格式输出使用<h3>标签作为小标题,<p>标签作为段落。'
].join('\n');
//
const reportContent = this.reportForm.reportContent || '';
const reportTitle = this.reportForm.reportTitle || '心理测评报告';
const reportType = this.reportForm.reportType || '标准报告';
// HTML
const textContent = reportContent.replace(/<[^>]*>/g, '').substring(0, 3000);
const prompt = `${SYSTEM_PROMPT}\n\n重要请直接输出结果不要包含任何思考过程、<think>标签或<think>标签。\n\n报告标题${reportTitle}\n报告类型${reportType}\n报告内容${textContent}`;
try {
const { data } = await axios.post(OLLAMA_URL, {
model: MODEL,
prompt: prompt,
temperature: 0.2,
num_predict: 1000,
stream: false
}, {
timeout: 60000 // 60
});
let rawResponse = data?.response ?? '无法解析模型输出';
//
rawResponse = rawResponse
.replace(/<think>[\s\S]*?<\/think>/gi, '')
.replace(/<think>[\s\S]*?<\/redacted_reasoning>/gi, '')
.replace(/<think[\s\S]*?>/gi, '')
.replace(/<redacted_reasoning[\s\S]*?>/gi, '')
// Markdown
.replace(/```html\s*/gi, '')
.replace(/```\s*/g, '')
.replace(/```[a-z]*\s*/gi, '')
.trim();
if (!rawResponse || rawResponse === '无法解析模型输出') {
this.aiError = '模型返回结果为空,请稍后重试';
return;
}
// HTML
this.aiResult = this.formatAIResult(rawResponse);
} catch (err) {
console.error('AI分析失败:', err);
if (err.code === 'ECONNABORTED' || err.message.includes('timeout')) {
this.aiError = '请求超时请检查Ollama服务是否正常运行';
} else if (err.response) {
this.aiError = err.response.data?.error || err.message || 'AI分析失败请稍后重试';
} else if (err.request) {
this.aiError = '无法连接到Ollama服务请检查服务地址是否正确当前' + OLLAMA_URL + '';
} else {
this.aiError = err.message || 'AI分析失败请稍后重试';
}
} finally {
this.aiLoading = false;
}
},
/** 格式化AI分析结果 */
formatAIResult(text) {
// Markdown```html```
let html = text
.replace(/```html\s*/gi, '')
.replace(/```\s*/g, '')
.replace(/```[a-z]*\s*/gi, '')
.trim();
// HTML
if (html.includes('<h3>') || html.includes('<p>') || html.includes('<div>')) {
//
html = html.replace(/```html\s*/gi, '').replace(/```\s*/g, '');
return html;
}
// """"""
html = html.replace(/^(\d+[\.、]?\s*[^\n]+)$/gm, '<h3>$1</h3>');
html = html.replace(/^([^\n]*(?:结论|分析|建议|总结|概述)[^\n]*)$/gm, '<h3>$1</h3>');
// <p>
html = html.split('\n\n').map(para => {
para = para.trim();
if (!para) return '';
if (para.startsWith('<h3>')) return para;
return '<p>' + para.replace(/\n/g, '<br>') + '</p>';
}).join('');
//
html = html.replace(/(<p>.*?<\/p>)/g, (match) => {
if (match.includes('<h3>')) return match.replace(/<p>|<\/p>/g, '');
return match;
});
//
html = html.replace(/```html\s*/gi, '').replace(/```\s*/g, '');
return html;
}
}
};
@ -345,5 +490,69 @@ export default {
font-weight: bold;
margin: 10px 0;
}
/* AI分析区域样式 */
.ai-analysis-section {
margin-top: 20px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
.ai-error {
margin-top: 15px;
}
.ai-result {
margin-top: 20px;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.ai-result-title {
font-size: 18px;
font-weight: bold;
color: #409eff;
margin: 0 0 15px 0;
padding-bottom: 10px;
border-bottom: 2px solid #e4e7ed;
display: flex;
align-items: center;
gap: 8px;
}
.ai-result-title i {
font-size: 20px;
}
.ai-result-content {
line-height: 1.8;
color: #333;
}
.ai-result-content >>> h3 {
font-size: 16px;
font-weight: bold;
color: #409eff;
margin: 20px 0 10px 0;
padding-left: 10px;
border-left: 3px solid #409eff;
}
.ai-result-content >>> p {
margin: 12px 0;
text-align: justify;
color: #606266;
}
.ai-result-content >>> p:first-of-type {
margin-top: 0;
}
.ai-result-content >>> br {
line-height: 1.8;
}
</style>

View File

@ -147,23 +147,10 @@
</el-select>
</el-form-item>
<el-form-item label="报告摘要" prop="summary">
<el-input
v-model="editForm.summary"
type="textarea"
:rows="4"
placeholder="请输入报告摘要"
/>
<Editor v-model="editForm.summary" :min-height="150" />
</el-form-item>
<el-form-item label="报告内容" prop="reportContent">
<el-input
v-model="editForm.reportContent"
type="textarea"
:rows="15"
placeholder="请输入报告内容支持HTML格式"
/>
<div style="margin-top: 10px; color: #909399; font-size: 12px;">
<i class="el-icon-info"></i> 提示报告内容支持HTML格式可以包含标题段落列表等格式
</div>
<Editor v-model="editForm.reportContent" :min-height="400" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
@ -197,9 +184,11 @@
import { listReport, getReport, delReport, exportReport, updateReportWithType } from "@/api/psychology/report";
import { loadSasReportData } from "@/services/report/ReportDataMapper";
import SASReportGenerator from "@/services/report/SASReportGenerator";
import Editor from "@/components/Editor";
export default {
name: "Report",
components: { Editor },
data() {
return {
//

View File

@ -72,11 +72,14 @@ export default {
return {
loading: false,
testList: [],
searchText: "",
studentNo: ""
searchText: ""
}
},
computed: {
// - store 使 infoNumber
studentNo() {
return this.$store.getters.infoNumber || this.$store.getters.name || "未知"
},
//
filteredTestList() {
if (!this.searchText) {
@ -88,12 +91,23 @@ export default {
})
}
},
watch: {
//
'$route'(to, from) {
//
if (to.path === '/student/tests') {
this.loadTestList()
}
}
},
created() {
//
this.studentNo = this.$store.getters.name || "未知"
//
this.loadTestList()
},
//
activated() {
this.loadTestList()
},
methods: {
//
loadTestList() {
@ -104,9 +118,19 @@ export default {
includeQuestionnaire: true //
}).then(response => {
//
this.testList = (response.rows || []).filter(scale => {
let filteredList = (response.rows || []).filter(scale => {
return scale.itemCount && scale.itemCount > 0
})
//
this.testList = filteredList.sort((a, b) => {
// 使 updateTime使 createTime
const timeA = a.updateTime || a.createTime || ''
const timeB = b.updateTime || b.createTime || ''
//
return timeB.localeCompare(timeA)
})
this.loading = false
}).catch(error => {
console.error("loadTestList, 加载测试题列表失败:", error)

View File

@ -96,32 +96,10 @@
</el-row>
<!-- 添加或修改用户配置对话框 -->
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-row>
<el-col :span="12">
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="30" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="归属部门" prop="deptId">
<treeselect v-model="form.deptId" :options="enabledDeptOptions" :show-count="true" placeholder="请选择归属部门" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12" v-if="false">
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
<el-dialog :title="title" :visible.sync="open" width="900px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<!-- 基本信息 -->
<el-divider content-position="left">基本信息</el-divider>
<el-row>
<el-col :span="12">
<el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName">
@ -136,7 +114,31 @@
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="用户性别">
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="form.nickName" placeholder="请输入用户昵称/姓名" maxlength="30" @input="handleNickNameInput" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="信息编号" prop="infoNumber">
<el-input v-model="form.infoNumber" placeholder="请输入信息编号(仅数字)" @input="handleInfoNumberInput" maxlength="20" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="性别" prop="sex">
<el-select v-model="form.sex" placeholder="请选择性别">
<el-option v-for="dict in dict.type.sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
</el-select>
@ -150,6 +152,78 @@
</el-form-item>
</el-col>
</el-row>
<!-- 心理档案信息 -->
<el-divider content-position="left">心理档案信息</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="档案类型" prop="profileType">
<el-select v-model="form.profileType" placeholder="请选择档案类型">
<el-option label="标准" value="standard" />
<el-option label="儿童" value="child" />
<el-option label="成人" value="adult" />
<el-option label="老年" value="senior" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="form.idCard" placeholder="请输入身份证号" maxlength="18" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="生日" prop="birthday">
<el-date-picker
v-model="form.birthday"
type="date"
placeholder="选择生日"
value-format="yyyy-MM-dd"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学历" prop="education">
<el-input v-model="form.education" placeholder="请输入学历" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="职业" prop="occupation">
<el-input v-model="form.occupation" placeholder="请输入职业" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="归属部门" prop="deptId">
<treeselect v-model="form.deptId" :options="enabledDeptOptions" :show-count="true" placeholder="请选择归属部门" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="地址" prop="address">
<el-input v-model="form.address" type="textarea" :rows="2" placeholder="请输入地址" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="紧急联系人" prop="emergencyContact">
<el-input v-model="form.emergencyContact" placeholder="请输入紧急联系人" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="紧急联系电话" prop="emergencyPhone">
<el-input v-model="form.emergencyPhone" placeholder="请输入紧急联系电话" maxlength="11" />
</el-form-item>
</el-col>
</el-row>
<!-- 系统权限 -->
<el-divider content-position="left">系统权限</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="岗位">
@ -401,10 +475,29 @@ export default {
status: "0",
remark: undefined,
postIds: [],
roleIds: []
roleIds: [],
//
infoNumber: undefined,
profileType: 'standard',
idCard: undefined,
birthday: undefined,
education: undefined,
occupation: undefined,
address: undefined,
emergencyContact: undefined,
emergencyPhone: undefined
}
this.resetForm("form")
},
//
handleInfoNumberInput(value) {
const numericValue = value.replace(/\D/g, '')
this.form.infoNumber = numericValue
},
//
handleNickNameInput(value) {
this.form.nickName = value.replace(/[^\u4e00-\u9fa5]/g, '')
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1

229
使用指南-总览.md Normal file
View File

@ -0,0 +1,229 @@
# 心理健康测评系统 - 使用指南总览
## 📚 文档导航
本系统为不同身份的用户提供了详细的使用指南,请根据您的身份选择对应的指南文档。
---
## 👥 用户身份说明
### 1. 系统管理员
**适用对象**
- 系统管理员
- 拥有完整系统管理权限的用户
**主要职责**
- 管理量表、用户、权限
- 配置预警规则
- 查看和管理测评报告
- 管理二维码
- 系统设置和维护
**对应指南**:👉 [系统管理员使用指南](./使用指南-系统管理员.md)
---
### 2. 普通用户/测评者
**适用对象**
- 已获得量表权限的注册用户
- 需要进行心理测评的用户
- 查看测评报告的用户
**主要功能**
- 进行心理量表测评
- 查看测评报告
- 查看历史记录
- 使用二维码扫描
- 管理个人档案
**对应指南**:👉 [普通用户使用指南](./使用指南-普通用户.md)
---
### 3. 注册用户
**适用对象**
- 新注册的用户
- 等待权限分配的用户
**主要流程**
- 注册账号
- 首次登录
- 等待权限分配
- 开始使用系统
**对应指南**:👉 [注册用户使用指南](./使用指南-注册用户.md)
---
## 📖 快速开始
### 我是系统管理员
1. 使用默认账号登录(用户名:`admin`,密码:`admin123`
2. 阅读 [系统管理员使用指南](./使用指南-系统管理员.md)
3. 开始配置系统和管理用户
### 我是新用户
1. 在登录页面点击"注册"
2. 填写注册信息完成注册
3. 阅读 [注册用户使用指南](./使用指南-注册用户.md)
4. 等待管理员分配权限
5. 获得权限后,阅读 [普通用户使用指南](./使用指南-普通用户.md)
6. 开始进行测评
### 我是已有权限的用户
1. 使用账号登录系统
2. 阅读 [普通用户使用指南](./使用指南-普通用户.md)
3. 开始进行测评
---
## 🎯 功能模块说明
### 量表管理
- **管理员**:创建、导入、配置量表
- **普通用户**:选择量表进行测评
### 测评管理
- **管理员**:查看和管理所有测评记录
- **普通用户**:查看自己的测评记录
### 报告管理
- **管理员**:查看、编辑、导出所有报告
- **普通用户**:查看自己的测评报告
### 权限管理
- **管理员**:为用户分配量表权限
- **普通用户**:查看自己的权限范围
### 问卷管理
- **管理员**:创建自定义问卷、主观题评分
- **普通用户**:参与问卷答题
### 预警管理
- **管理员**:配置预警规则、查看预警信息
- **普通用户**:不涉及
### 二维码管理
- **管理员**:生成和管理二维码
- **普通用户**:扫描二维码进行测评或查看报告
---
## 📋 文档结构
```
使用指南总览.md ← 您当前所在的位置
├── 使用指南-系统管理员.md ← 管理员完整使用指南
├── 使用指南-普通用户.md ← 普通用户完整使用指南
└── 使用指南-注册用户.md ← 注册用户指南
```
---
## 🔍 常见问题快速查找
### 关于注册
- 如何注册账号? → [注册用户使用指南](./使用指南-注册用户.md#注册流程)
- 注册后无法登录? → [注册用户使用指南](./使用指南-注册用户.md#常见问题)
### 关于测评
- 如何开始测评? → [普通用户使用指南](./使用指南-普通用户.md#开始测评)
- 如何答题? → [普通用户使用指南](./使用指南-普通用户.md#答题操作)
- 可以暂停测评吗? → [普通用户使用指南](./使用指南-普通用户.md#暂停和继续)
### 关于报告
- 如何查看报告? → [普通用户使用指南](./使用指南-普通用户.md#查看报告)
- 报告什么时候生成? → [普通用户使用指南](./使用指南-普通用户.md#常见问题)
### 关于权限
- 看不到量表怎么办? → [普通用户使用指南](./使用指南-普通用户.md#常见问题)
- 如何分配权限? → [系统管理员使用指南](./使用指南-系统管理员.md#权限管理)
### 关于量表
- 如何导入新量表? → [系统管理员使用指南](./使用指南-系统管理员.md#量表管理)
- 如何配置量表? → [系统管理员使用指南](./使用指南-系统管理员.md#配置量表)
---
## 📞 获取帮助
### 技术支持
如遇到问题,可以:
1. **查看文档**:阅读对应的使用指南
2. **查看常见问题**:每个指南都包含常见问题部分
3. **联系管理员**:联系系统管理员获取帮助
4. **查看系统日志**:管理员可以查看系统日志排查问题
### 文档更新
- 文档会随着系统更新而更新
- 建议定期查看最新版本
- 如有疑问,请联系技术支持
---
## ✅ 使用检查清单
### 新用户检查清单
- [ ] 已完成账号注册
- [ ] 已成功登录系统
- [ ] 已阅读注册用户使用指南
- [ ] 已联系管理员申请权限
- [ ] 已获得量表权限
- [ ] 已阅读普通用户使用指南
- [ ] 已准备好进行首次测评
### 管理员检查清单
- [ ] 已修改默认密码
- [ ] 已阅读系统管理员使用指南
- [ ] 已配置系统基础设置
- [ ] 已导入或创建量表
- [ ] 已为用户分配权限
- [ ] 已配置预警规则(如需要)
- [ ] 已测试系统功能
---
## 🎓 学习路径建议
### 对于新用户
1. **第一步**:注册账号 → [注册用户使用指南](./使用指南-注册用户.md)
2. **第二步**:等待权限分配
3. **第三步**:学习使用系统 → [普通用户使用指南](./使用指南-普通用户.md)
4. **第四步**:进行首次测评
5. **第五步**:查看测评报告
### 对于管理员
1. **第一步**:登录系统并修改密码
2. **第二步**:阅读管理员指南 → [系统管理员使用指南](./使用指南-系统管理员.md)
3. **第三步**:配置系统基础设置
4. **第四步**:导入或创建量表
5. **第五步**:为用户分配权限
6. **第六步**:测试系统功能
7. **第七步**:开始日常管理
---
## 📝 文档说明
- **最后更新**2025-01-XX
- **文档版本**v1.0
- **适用系统版本**:心理健康测评系统 v1.0
---
**祝您使用愉快!** 🎉

View File

@ -0,0 +1,375 @@
# 心理健康测评系统 - 普通用户使用指南
## 📋 目录
1. [系统概述](#系统概述)
2. [注册账号](#注册账号)
3. [登录系统](#登录系统)
4. [开始测评](#开始测评)
5. [答题操作](#答题操作)
6. [查看报告](#查看报告)
7. [查看历史记录](#查看历史记录)
8. [使用二维码](#使用二维码)
9. [用户档案](#用户档案)
10. [常见问题](#常见问题)
---
## 系统概述
心理健康测评系统是一个专业的心理测评平台,您可以通过该系统进行各种心理量表测评,查看测评报告,了解自己的心理健康状况。
### 主要功能
- **心理测评**:进行各种心理量表测评
- **查看报告**:查看详细的测评报告和结果解释
- **历史记录**:查看历史测评记录
- **二维码扫描**:通过扫码快速开始测评
- **用户档案**:管理个人档案信息
---
## 注册账号
### 注册步骤
1. 打开浏览器,访问系统地址(如:`http://localhost:80`
2. 在登录页面,点击 **"注册"** 链接
3. 填写注册信息:
- **用户名**:用于登录的用户名(必填,唯一)
- **密码**登录密码必填建议8位以上
- **确认密码**:再次输入密码(必填)
- **手机号**:手机号码(可选)
- **邮箱**:邮箱地址(可选)
4. 输入验证码(如显示)
5. 点击 **"注册"** 按钮
6. 注册成功后,系统会提示注册成功
### 注册注意事项
- 用户名不能重复,如果提示用户名已存在,请更换用户名
- 密码建议包含字母和数字,提高安全性
- 注册后需要等待管理员分配量表权限才能进行测评
- 如果忘记密码,请联系管理员重置
---
## 登录系统
### 登录步骤
1. 打开浏览器,访问系统地址
2. 在登录页面输入:
- **用户名**:您的用户名
- **密码**:您的密码
- **验证码**:输入显示的验证码(如显示)
3. 点击 **"登录"** 按钮
4. 登录成功后进入系统首页
### 登录问题
- **忘记密码**:请联系管理员重置密码
- **账号被锁定**:联系管理员解锁账号
- **验证码看不清**:点击验证码图片刷新
---
## 开始测评
### 方式一:通过菜单开始
1. 登录系统后,点击左侧菜单 **"心理测评管理"** → **"测评管理"**
2. 点击 **"开始测评"** 按钮
3. 在量表选择页面,选择要进行的量表
4. 填写被测评人信息:
- **姓名**:被测评人姓名
- **性别**:男/女
- **年龄**:年龄
- **其他信息**:根据需要填写
5. 点击 **"开始测评"** 按钮
6. 进入答题页面
### 方式二:通过二维码扫描
1. 使用手机扫描管理员提供的测评二维码
2. 如果未登录,系统会提示先登录
3. 登录后自动跳转到测评开始页面
4. 填写被测评人信息
5. 开始答题
### 量表选择说明
- **权限限制**:您只能看到管理员为您分配权限的量表
- **量表信息**:每个量表显示名称、类型、题目数量、预计时间
- **无权限提示**:如果没有可用的量表,系统会提示联系管理员
---
## 答题操作
### 答题界面
答题页面包含以下元素:
- **量表信息**:显示量表名称和进度
- **题目区域**:显示当前题目和选项
- **进度条**:显示答题进度
- **导航按钮**:上一题、下一题、提交
### 答题步骤
1. **阅读题目**:仔细阅读题目内容
2. **选择答案**
- **单选题**:选择一个选项
- **多选题**:可以选择多个选项
3. **保存答案**:选择答案后,系统自动保存
4. **继续答题**
- 点击 **"下一题"** 继续
- 或点击 **"上一题"** 返回修改
5. **完成答题**
- 答完所有题目后,点击 **"提交"** 按钮
- 确认提交后,系统生成测评报告
### 答题技巧
- **认真阅读**:仔细阅读每个题目,理解题意
- **如实回答**:根据自己的真实情况回答,不要猜测
- **不要跳过**:尽量回答所有题目,确保结果准确
- **可以修改**:在提交前可以随时返回修改答案
- **注意时间**:某些量表有时间限制,注意答题时间
### 暂停和继续
如果无法一次性完成测评:
1. **暂停测评**
- 点击 **"暂停"** 按钮
- 系统保存当前进度
2. **继续测评**
- 下次登录后,进入 **"测评管理"**
- 找到状态为"进行中"的测评
- 点击 **"继续测评"** 按钮
- 系统自动跳转到上次的答题位置
---
## 查看报告
### 查看方式
#### 方式一:通过菜单查看
1. 登录系统后,点击 **"心理测评管理"** → **"测评报告"**
2. 在报告列表中,找到要查看的报告
3. 点击 **"查看"** 按钮
4. 查看报告详细内容
#### 方式二:通过测评记录查看
1. 进入 **"测评管理"**
2. 找到已完成的测评记录
3. 点击 **"查看报告"** 按钮
4. 查看报告
#### 方式三:通过二维码扫描
1. 扫描管理员提供的报告查看二维码
2. 如果未登录,系统会提示先登录
3. 登录后自动跳转到报告详情页面
### 报告内容
测评报告通常包含以下内容:
- **基本信息**
- 被测评人信息
- 测评时间
- 量表名称
- **测评结果**
- 总分
- 各因子得分(如有)
- 分数解释
- **结果分析**
- 结果等级(如:正常、轻度、中度、重度)
- 详细解释
- 因子分析(如有)
- **建议指导**
- 针对性的建议
- 改善方法
- 注意事项
### 报告说明
- **报告生成**:提交测评后,系统自动生成报告
- **报告准确性**:报告结果基于您的答题情况,请如实回答
- **仅供参考**:报告结果仅供参考,不能替代专业诊断
- **隐私保护**:您的测评数据受到隐私保护
---
## 查看历史记录
### 查看测评记录
1. 进入 **"心理测评管理"** → **"测评管理"**
2. 查看所有测评记录:
- **进行中**:尚未完成的测评
- **已完成**:已完成的测评
- **已暂停**:暂停的测评
3. 可以按条件搜索:
- 量表名称
- 被测评人
- 测评状态
- 创建时间
### 查看报告记录
1. 进入 **"心理测评管理"** → **"测评报告"**
2. 查看所有报告记录
3. 可以按条件搜索:
- 报告标题
- 量表名称
- 创建时间
### 对比分析
- 可以查看同一被测评人的多次测评记录
- 对比不同时间的测评结果
- 了解心理变化趋势
---
## 使用二维码
### 扫描测评二维码
1. 使用手机扫描管理员提供的测评二维码
2. 如果未登录,系统会提示先登录
3. 登录后自动跳转到测评开始页面
4. 填写被测评人信息
5. 开始答题
### 扫描报告二维码
1. 使用手机扫描管理员提供的报告查看二维码
2. 如果未登录,系统会提示先登录
3. 登录后自动跳转到报告详情页面
4. 查看报告内容
### 二维码使用说明
- **扫码工具**:可以使用微信、支付宝等应用的扫一扫功能
- **网络要求**:需要网络连接才能访问
- **权限要求**:需要登录并有相应权限
- **有效期**:二维码可能有过期时间,过期后无法使用
---
## 用户档案
### 查看个人档案
1. 进入 **"心理测评管理"** → **"用户档案"**
2. 查看个人档案信息
3. 可以查看:
- 基本信息
- 测评记录
- 档案历史
### 编辑个人档案
1. 在档案页面,点击 **"编辑"** 按钮
2. 修改档案信息(如允许)
3. 点击 **"确定"** 保存
### 档案说明
- **档案内容**:由管理员配置,可能包含自定义字段
- **隐私保护**:档案信息受到隐私保护
- **权限限制**:某些字段可能无法自行修改
---
## 常见问题
### Q1: 注册后无法看到量表?
**A**: 注册后需要等待管理员为您分配量表权限。请联系管理员分配权限。
### Q2: 忘记密码怎么办?
**A**: 请联系管理员重置密码。管理员可以在用户管理中重置您的密码。
### Q3: 答题过程中可以暂停吗?
**A**: 可以。点击"暂停"按钮,系统会保存当前进度。下次登录后可以继续完成测评。
### Q4: 可以修改已提交的答案吗?
**A**: 不可以。提交后无法修改答案。请在提交前仔细检查。
### Q5: 报告什么时候生成?
**A**: 提交测评后,系统会自动生成报告。通常几秒钟内完成。
### Q6: 可以查看历史测评记录吗?
**A**: 可以。在"测评管理"中可以查看所有历史测评记录。
### Q7: 报告结果准确吗?
**A**: 报告结果基于您的答题情况。请如实回答题目,确保结果准确。但报告结果仅供参考,不能替代专业诊断。
### Q8: 可以导出报告吗?
**A**: 普通用户无法导出报告。如需导出,请联系管理员。
### Q9: 二维码扫描后无法访问?
**A**: 检查以下几点:
- 是否已登录
- 是否有相应权限
- 二维码是否过期
- 网络连接是否正常
### Q10: 如何联系管理员?
**A**: 请联系系统管理员或技术支持人员。
---
## 使用提示
### 测评前
- ✅ 确保网络连接正常
- ✅ 选择一个安静的环境
- ✅ 预留足够的答题时间
- ✅ 准备好被测评人的基本信息
### 测评中
- ✅ 认真阅读每个题目
- ✅ 根据自己的真实情况回答
- ✅ 不要猜测或随意选择
- ✅ 注意答题时间
### 测评后
- ✅ 查看生成的报告
- ✅ 理解报告内容
- ✅ 如有疑问,咨询专业人员
- ✅ 保存报告信息(如需要)
---
## 隐私说明
- 您的测评数据受到严格保护
- 只有管理员和您本人可以查看您的测评记录
- 系统不会向第三方泄露您的个人信息
- 请妥善保管您的账号密码
---
**最后更新**2025-01-XX

View File

@ -0,0 +1,264 @@
# 心理健康测评系统 - 注册用户使用指南
## 📋 目录
1. [注册流程](#注册流程)
2. [首次登录](#首次登录)
3. [等待权限分配](#等待权限分配)
4. [开始使用](#开始使用)
5. [常见问题](#常见问题)
---
## 注册流程
### 访问注册页面
1. 打开浏览器,访问系统地址(如:`http://localhost:80`
2. 在登录页面,点击 **"注册"** 链接或按钮
3. 进入注册页面
### 填写注册信息
在注册页面填写以下信息:
#### 必填信息
- **用户名**
- 用于登录的用户名
- 必须唯一,不能与其他用户重复
- 建议使用字母、数字组合
- 长度建议3-20个字符
- **密码**
- 登录密码
- 建议8位以上
- 建议包含字母和数字
- 不要使用过于简单的密码123456
- **确认密码**
- 再次输入密码
- 必须与密码一致
#### 可选信息
- **手机号**:手机号码(用于找回密码等)
- **邮箱**:邮箱地址(用于接收通知等)
### 完成注册
1. 输入验证码(如显示)
2. 阅读并同意用户协议(如有)
3. 点击 **"注册"** 按钮
4. 系统验证信息:
- 如果用户名已存在,会提示"用户名已存在"
- 如果密码不一致,会提示"两次密码不一致"
- 如果验证码错误,会提示"验证码错误"
5. 注册成功后,系统会显示"注册成功"提示
---
## 首次登录
### 登录步骤
1. 注册成功后,返回登录页面
2. 使用注册的用户名和密码登录
3. 首次登录后,您可能会看到:
- 欢迎页面
- 系统提示(如:等待管理员分配权限)
- 空白的量表列表(如果还没有权限)
### 登录后状态
注册用户首次登录后,通常处于以下状态:
- ✅ **账号已激活**:可以正常登录系统
- ⏳ **等待权限**:需要等待管理员分配量表权限
- 📋 **功能受限**:可能无法看到量表或进行测评
---
## 等待权限分配
### 为什么需要等待?
注册用户需要管理员分配量表访问权限后才能进行测评。这是为了:
- **权限控制**:确保用户只能访问被授权的量表
- **数据安全**:保护测评数据的安全
- **管理规范**:便于管理员统一管理
### 如何知道权限已分配?
权限分配后,您可以通过以下方式确认:
1. **刷新页面**:登录后刷新浏览器页面
2. **查看量表列表**:进入"测评管理",如果能看到量表列表,说明已有权限
3. **联系管理员**:如果长时间没有权限,可以联系管理员确认
### 权限说明
管理员可以为您分配:
- **量表权限**:指定您可以访问哪些量表
- **时间范围**:权限的有效期(开始时间和结束时间)
- **权限类型**:按用户、角色或部门分配
---
## 开始使用
### 获得权限后
当管理员为您分配权限后,您可以:
1. **查看可用量表**
- 进入"心理测评管理" → "测评管理"
- 点击"开始测评"
- 查看可用的量表列表
2. **开始测评**
- 选择要进行的量表
- 填写被测评人信息
- 开始答题
3. **查看报告**
- 完成测评后查看报告
- 查看历史测评记录
### 使用流程
完整的测评流程:
```
注册账号
登录系统
等待权限分配
选择量表
填写被测评人信息
开始答题
提交测评
查看报告
```
---
## 常见问题
### Q1: 注册时提示"用户名已存在"
**A**: 该用户名已被其他用户使用。请更换一个不同的用户名。
**建议**
- 使用字母+数字组合
- 添加下划线或连字符
- 使用邮箱前缀作为用户名
### Q2: 注册时提示"密码不一致"
**A**: "密码"和"确认密码"输入不一致。请重新输入,确保两次输入的密码完全相同。
### Q3: 注册后无法登录?
**A**: 检查以下几点:
- 用户名和密码是否正确
- 是否输入了正确的验证码
- 账号是否被管理员停用
- 联系管理员确认账号状态
### Q4: 登录后看不到量表?
**A**: 这是正常情况。注册用户需要等待管理员分配量表权限。请:
- 等待管理员分配权限
- 或联系管理员申请权限
### Q5: 如何联系管理员?
**A**: 请联系系统管理员或技术支持人员,告知您的用户名和需求。
### Q6: 忘记密码怎么办?
**A**: 请联系管理员重置密码。管理员可以在用户管理中重置您的密码。
### Q7: 可以修改注册信息吗?
**A**: 部分信息可以修改:
- 登录后进入"个人中心"(如有)
- 或联系管理员修改
### Q8: 注册后多久可以获得权限?
**A**: 这取决于管理员的工作安排。通常:
- 管理员会定期审核新注册用户
- 或根据申请及时分配权限
- 建议主动联系管理员申请
### Q9: 可以注册多个账号吗?
**A**: 可以,但需要:
- 使用不同的用户名
- 每个账号独立管理
- 遵守系统使用规范
### Q10: 注册信息会被泄露吗?
**A**: 不会。系统严格保护用户隐私:
- 注册信息受到加密保护
- 不会向第三方泄露
- 只有管理员可以查看(用于权限管理)
---
## 注册注意事项
### 用户名选择
- ✅ 使用易记的用户名
- ✅ 避免使用特殊字符
- ✅ 不要使用真实姓名(保护隐私)
- ❌ 不要使用过于简单的用户名abc、123
### 密码设置
- ✅ 使用8位以上的密码
- ✅ 包含字母和数字
- ✅ 定期更换密码
- ❌ 不要使用过于简单的密码
- ❌ 不要使用个人信息作为密码
### 信息填写
- ✅ 如实填写信息(如手机号、邮箱)
- ✅ 信息用于账号管理和找回密码
- ⚠️ 注意保护个人隐私
---
## 下一步
注册成功后,建议您:
1. **保存账号信息**:记录用户名和密码,妥善保管
2. **联系管理员**:主动联系管理员申请量表权限
3. **了解系统**:阅读《普通用户使用指南》,了解系统功能
4. **准备测评**:获得权限后,准备好进行测评
---
## 相关文档
- [普通用户使用指南](./使用指南-普通用户.md) - 详细的使用说明
- [系统管理员使用指南](./使用指南-系统管理员.md) - 管理员功能说明
---
**最后更新**2025-01-XX

View File

@ -0,0 +1,650 @@
# 心理健康测评系统 - 系统管理员使用指南
## 📋 目录
1. [系统概述](#系统概述)
2. [登录系统](#登录系统)
3. [量表管理](#量表管理)
4. [用户管理](#用户管理)
5. [权限管理](#权限管理)
6. [测评管理](#测评管理)
7. [报告管理](#报告管理)
8. [问卷管理](#问卷管理)
9. [预警管理](#预警管理)
10. [二维码管理](#二维码管理)
11. [用户档案管理](#用户档案管理)
12. [心理网站管理](#心理网站管理)
13. [系统设置](#系统设置)
14. [数据导出](#数据导出)
15. [常见问题](#常见问题)
---
## 系统概述
心理健康测评系统是一个基于Web的心理测评管理平台支持量表测评、自定义问卷、用户档案管理、预警管理等功能。
### 主要功能模块
- **量表管理**:创建、导入、配置心理量表
- **用户管理**:管理用户账号、分配权限
- **测评管理**:查看和管理测评记录
- **报告管理**:查看、编辑、导出测评报告
- **问卷管理**:创建自定义问卷、主观题评分
- **预警管理**:配置预警规则、查看预警信息
- **二维码管理**:生成和管理测评二维码
- **用户档案**:管理用户档案信息
- **心理网站**:管理心理教育网站内容
---
## 登录系统
### 访问系统
1. 打开浏览器,访问系统地址(如:`http://localhost:80`
2. 进入登录页面
### 默认管理员账号
- **用户名**`admin`
- **密码**`admin123`
> ⚠️ **安全提示**:首次登录后请立即修改密码!
### 登录步骤
1. 输入用户名和密码
2. 输入验证码(如显示)
3. 点击"登录"按钮
4. 登录成功后进入系统首页
---
## 量表管理
### 功能说明
量表管理是系统的核心功能,管理员可以创建、导入、配置和管理心理量表。
### 创建新量表
#### 方式一:手动创建
1. 进入 **心理测评管理** → **量表管理**
2. 点击 **"新增"** 按钮
3. 填写量表基本信息:
- **量表编码**:唯一标识(如:`SCL_90`
- **量表名称**:量表完整名称
- **量表类型**:选择类型(情绪、人格、行为等)
- **量表简介**:简短介绍
- **量表描述**:详细描述
- **题目数量**:预计题目数
- **预计完成时间**:分钟数
- **适用人群**:如"一般人群"、"青少年"等
- **作者**:量表作者
- **来源**:量表来源
- **状态**:选择"正常"(启用)
4. 点击 **"确定"** 保存
#### 方式二JSON格式导入
1. 进入 **量表管理** 页面
2. 点击 **"导入"** 按钮
3. 选择JSON格式的量表文件
4. 系统自动解析并导入量表数据
5. 检查导入结果,确认无误
> 📖 **详细说明**:参考《新量表导入完整操作指南.md》
### 配置量表
创建量表后,需要完成以下配置:
#### 1. 添加题目
1. 在量表列表中,点击 **"题目管理"** 按钮
2. 点击 **"新增"** 添加题目
3. 填写题目信息:
- **题目序号**:如 1、2、3...
- **题目内容**:题目的完整文字
- **题目类型**:单选题/多选题
- **是否必答**:是/否
4. 保存题目
#### 2. 配置选项
1. 在题目列表中,点击 **"选项管理"** 按钮
2. 为每个题目添加选项:
- **选项编码**:如 A、B、C、D
- **选项内容**:选项文字
- **选项分数**:该选项对应的分数
3. 保存选项
#### 3. 添加因子(如需要)
1. 在量表列表中,点击 **"因子管理"** 按钮
2. 点击 **"新增"** 添加因子
3. 填写因子信息:
- **因子编码**:如 F1、F2
- **因子名称**:如"躯体化因子"
- **因子描述**:详细说明
4. 保存因子
#### 4. 配置计分规则(如需要)
1. 在因子列表中,点击 **"计分规则"** 按钮
2. 为每个因子配置计分规则:
- 选择题目
- 选择选项(可选)
- 设置权重
- 选择计算方式(求和/平均/最大/最小)
3. 保存规则
#### 5. 配置结果解释
1. 进入 **心理测评管理** → **解释配置**
2. 点击 **"新增"** 添加解释
3. 填写解释信息:
- **量表**:选择量表
- **因子**:选择因子(可选)
- **分数下限/上限**:分数范围
- **等级**:如"低"、"中"、"高"
- **解释标题**:解释标题
- **解释内容**:详细解释
- **建议指导**:建议和指导
4. 保存解释
### 管理量表
- **编辑量表**:点击 **"修改"** 按钮
- **删除量表**:点击 **"删除"** 按钮(需谨慎)
- **查看详情**:点击量表名称
- **导出量表**:勾选量表后点击 **"导出"** 按钮
---
## 用户管理
### 功能说明
用户管理用于管理系统用户账号,包括创建用户、分配角色、设置权限等。
### 查看用户列表
1. 进入 **系统管理** → **用户管理**
2. 查看用户列表,可以按条件搜索:
- 用户名
- 手机号
- 状态
- 创建时间
### 创建新用户
1. 在用户管理页面,点击 **"新增"** 按钮
2. 填写用户信息:
- **用户名**:登录用户名
- **昵称**:显示名称
- **邮箱**:邮箱地址
- **手机号**:手机号码
- **性别**:男/女
- **密码**:初始密码
- **部门**:所属部门
- **角色**:选择角色
- **状态**:正常/停用
3. 点击 **"确定"** 保存
### 管理用户
- **编辑用户**:点击 **"修改"** 按钮
- **删除用户**:点击 **"删除"** 按钮
- **重置密码**:点击 **"重置密码"** 按钮
- **分配角色**:点击 **"更多"** → **"分配角色"**
- **分配量表权限**:点击 **"更多"** → **"分配量表权限"**
### 批量操作
1. 勾选多个用户
2. 点击 **"批量删除"** 或 **"批量导出"**
---
## 权限管理
### 功能说明
权限管理用于控制用户对量表的访问权限,支持按用户、角色、部门分配权限。
### 量表权限管理
1. 进入 **心理测评管理** → **量表权限管理**
2. 查看权限列表,可以按条件筛选:
- 量表名称
- 用户名称
- 部门
- 状态
### 分配权限
#### 方式一:通过权限管理页面
1. 在权限管理页面,点击 **"新增"** 按钮
2. 填写权限信息:
- **量表**:选择量表
- **用户**:选择用户(可选)
- **角色**:选择角色(可选)
- **部门**:选择部门(可选)
- **开始时间**:权限生效时间
- **结束时间**:权限失效时间
- **状态**:有效/无效
3. 点击 **"确定"** 保存
#### 方式二:通过用户管理页面
1. 进入 **系统管理** → **用户管理**
2. 找到要分配权限的用户
3. 点击 **"更多"** → **"分配量表权限"**
4. 在权限分配页面,选择量表并设置时间范围
5. 点击 **"确定"** 保存
### 权限规则
- **管理员**userId = 1自动拥有所有量表的访问权限
- **用户直接权限**:优先级最高
- **角色权限**:次优先级
- **部门权限**:再次优先级
- **全局权限**:最低优先级(所有用户)
---
## 测评管理
### 功能说明
测评管理用于查看和管理所有测评记录,包括进行中的测评和已完成的测评。
### 查看测评列表
1. 进入 **心理测评管理** → **测评管理**
2. 查看测评列表,可以按条件搜索:
- 量表名称
- 被测评人
- 测评状态
- 创建时间
### 测评状态
- **进行中**:测评尚未完成
- **已完成**:测评已完成并生成报告
- **已暂停**:测评被暂停(支持继续测评)
### 管理测评
- **查看详情**:点击测评记录
- **查看报告**:点击 **"查看报告"** 按钮
- **删除测评**:点击 **"删除"** 按钮
- **继续测评**:对于暂停的测评,可以继续完成
### 管理员快速填充(测试功能)
> ⚠️ **注意**:此功能仅用于测试,不建议用于真实测评数据。
1. 进入测评页面
2. 点击 **"快速填充"** 下拉按钮
3. 选择填充策略:
- 填充第一个选项并提交
- 填充中间选项并提交
- 填充最后一个选项并提交
- 随机填充并提交
4. 确认后系统自动填充并提交测评
---
## 报告管理
### 功能说明
报告管理用于查看、编辑、导出测评报告。
### 查看报告列表
1. 进入 **心理测评管理** → **测评报告**
2. 查看报告列表,可以按条件搜索:
- 报告标题
- 被测评人
- 量表名称
- 生成状态
- 创建时间
### 查看报告详情
1. 在报告列表中,点击 **"查看"** 按钮
2. 查看报告详细内容:
- 基本信息
- 测评结果
- 因子分析
- 结果解释
- 建议指导
### 编辑报告
1. 在报告列表中,点击 **"编辑"** 按钮
2. 修改报告内容:
- 解释标题
- 解释内容
- 建议指导
3. 点击 **"确定"** 保存
### 导出报告
1. 勾选需要导出的报告(可多选)
2. 点击 **"导出"** 按钮
3. 系统生成Excel文件并下载
---
## 问卷管理
### 功能说明
问卷管理用于创建自定义问卷,支持多种题型,适合在线考试、练习等场景。
### 创建问卷
1. 进入 **心理测评管理** → **问卷管理**
2. 点击 **"新增"** 按钮
3. 填写问卷信息:
- **问卷名称**:问卷标题
- **问卷描述**:问卷说明
- **问卷类型**:选择类型
- **状态**:正常/停用
4. 点击 **"确定"** 保存
### 添加题目
1. 在问卷列表中,点击 **"题目管理"** 按钮
2. 点击 **"新增"** 添加题目
3. 选择题目类型:
- **单选题**:只有一个正确答案
- **多选题**:可以有多个正确答案
- **判断题**:对/错
- **填空题**:文本输入
- **简答题**:短文本回答
- **问答题**:长文本回答
- **作文题**:长文本回答
4. 填写题目信息:
- **题目内容**:题目文字
- **题目分值**:该题分数
- **是否必答**:是/否
- **正确答案**:客观题的正确答案
5. 保存题目
### 主观题评分
1. 进入 **心理测评管理** → **主观题评分**
2. 查看待评分题目列表
3. 点击 **"评分"** 按钮
4. 输入得分和评语(可选)
5. 点击 **"确定"** 保存
### 批量评分
1. 勾选多个待评分题目
2. 点击 **"批量评分"** 按钮
3. 为每个题目输入得分和评语
4. 点击 **"确定"** 批量保存
---
## 预警管理
### 功能说明
预警管理用于配置预警规则,当测评结果异常时自动向管理员发出警告。
### 配置预警规则
1. 进入 **心理测评管理** → **预警规则配置**
2. 点击 **"新增"** 按钮
3. 填写预警规则信息:
- **量表**:选择量表
- **因子**:选择因子(可选)
- **规则名称**:如"重度抑郁预警"
- **预警等级**:低/中/高/紧急
- **分数下限/上限**:触发预警的分数范围
- **百分位下限/上限**:触发预警的百分位范围
- **自动解除**:是否自动解除预警
- **解除条件**:自动解除的条件
- **状态**:正常/停用
4. 点击 **"确定"** 保存
### 查看预警信息
1. 进入 **心理测评管理** → **预警管理**
2. 查看预警列表,可以按条件筛选:
- 预警等级
- 量表名称
- 被测评人
- 预警状态
- 创建时间
### 处理预警
- **查看详情**:点击预警记录查看详细信息
- **解除预警**:手动解除预警
- **导出预警**:导出预警数据
---
## 二维码管理
### 功能说明
二维码管理用于生成和管理测评二维码,方便用户通过扫码快速访问测评和查看报告。
### 生成量表测评二维码
1. 进入 **心理测评管理** → **量表管理**
2. 找到目标量表
3. 点击 **"二维码"** 按钮
4. 系统生成二维码并显示
5. 可以下载二维码图片或直接打印
### 生成报告查看二维码
1. 进入 **心理测评管理** → **测评报告**
2. 找到目标报告
3. 点击 **"二维码"** 按钮
4. 系统生成二维码
### 管理二维码
1. 进入 **心理测评管理** → **二维码管理**
2. 查看所有二维码:
- 二维码类型
- 目标信息
- 扫码次数
- 状态
- 过期时间
3. 可以重新生成或删除二维码
---
## 用户档案管理
### 功能说明
用户档案管理用于管理用户的档案信息,支持自定义档案项目。
### 查看用户档案
1. 进入 **心理测评管理** → **用户档案**
2. 查看用户档案列表
3. 可以按条件搜索用户
### 编辑用户档案
1. 在档案列表中,点击 **"编辑"** 按钮
2. 修改档案信息:
- 基本信息
- 自定义字段
3. 点击 **"确定"** 保存
### 自定义档案项目
1. 进入 **系统管理****参数设置**(如支持)
2. 配置自定义档案字段
3. 保存配置
---
## 心理网站管理
### 功能说明
心理网站管理用于管理心理教育网站的内容,包括文章、分类、评论等。
### 管理文章
1. 进入 **心理测评管理****心理网站** → **内容管理**
2. 查看文章列表
3. 可以新增、编辑、删除文章
### 管理分类
1. 进入 **心理测评管理****心理网站** → **分类管理**
2. 管理文章分类
### 管理评论
1. 进入 **心理测评管理****心理网站** → **评论管理**
2. 查看和管理用户评论
---
## 系统设置
### 角色管理
1. 进入 **系统管理** → **角色管理**
2. 查看和管理系统角色
3. 为角色分配菜单权限和按钮权限
### 菜单管理
1. 进入 **系统管理** → **菜单管理**
2. 查看和管理系统菜单
3. 可以新增、编辑、删除菜单
### 参数设置
1. 进入 **系统管理** → **参数设置**
2. 配置系统参数
### 字典管理
1. 进入 **系统管理** → **字典管理**
2. 管理系统字典数据
---
## 数据导出
### 量表导出
1. 进入 **心理测评管理** → **量表管理**
2. 勾选需要导出的量表(可多选)
3. 点击 **"导出"** 按钮
4. 系统生成JSON格式文件并下载
### 报告导出
1. 进入 **心理测评管理** → **测评报告**
2. 勾选需要导出的报告(可多选)
3. 点击 **"导出"** 按钮
4. 系统生成Excel格式文件并下载
### 数据备份
1. 进入 **系统管理****数据备份**(如支持)
2. 执行数据备份操作
---
## 常见问题
### Q1: 如何导入新量表?
**A**: 有两种方式:
1. **手动创建**:在量表管理中逐个添加题目和选项
2. **JSON导入**准备符合格式的JSON文件使用导入功能
详细说明请参考《新量表导入完整操作指南.md》
### Q2: 用户看不到量表怎么办?
**A**: 检查以下几点:
1. 量表状态是否为"正常"
2. 用户是否有量表访问权限
3. 权限是否在有效期内
4. 刷新浏览器页面
### Q3: 如何配置预警规则?
**A**:
1. 进入"预警规则配置"
2. 选择量表和因子
3. 设置分数范围或百分位范围
4. 选择预警等级
5. 保存规则
### Q4: 二维码无法访问怎么办?
**A**: 检查以下几点:
1. 二维码是否过期
2. 二维码状态是否为"有效"
3. 网络连接是否正常
4. 系统URL配置是否正确
### Q5: 如何批量分配用户权限?
**A**:
1. 进入"量表权限管理"
2. 点击"新增"
3. 选择量表
4. 选择角色或部门(不选择用户表示批量分配)
5. 设置时间范围
6. 保存
### Q6: 报告生成失败怎么办?
**A**: 检查以下几点:
1. 量表是否配置了结果解释
2. 因子计分规则是否正确
3. 测评是否已完成
4. 查看系统日志错误信息
### Q7: 如何修改报告内容?
**A**:
1. 进入"测评报告"
2. 找到目标报告
3. 点击"编辑"按钮
4. 修改解释内容或建议指导
5. 保存
---
## 技术支持
如遇到问题,请:
1. 查看系统日志
2. 检查浏览器控制台错误
3. 参考相关文档
4. 联系技术支持
---
**最后更新**2025-01-XX