批量推送

This commit is contained in:
郭庆泰
2026-08-16 13:42:26 +08:00
parent c612d4297a
commit 0c093076a4
66 changed files with 4847 additions and 1627 deletions
@@ -27,6 +27,7 @@ import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.vo.SysUserExtendVo;
import com.ruoyi.system.service.ISysDeptService;
import com.ruoyi.system.service.ISysPostService;
import com.ruoyi.system.service.ISysRoleService;
@@ -58,10 +59,10 @@ public class SysUserController extends BaseController
*/
@PreAuthorize("@ss.hasPermi('system:user:list')")
@GetMapping("/list")
public TableDataInfo list(SysUser user)
public TableDataInfo list(SysUserExtendVo vo)
{
startPage();
List<SysUser> list = userService.selectUserList(user);
List<SysUserExtendVo> list = userService.selectUserExtendList(vo);
return getDataTable(list);
}
@@ -18,13 +18,15 @@ import com.ruoyi.system.service.ISysUserService;
/**
* 业务登录 - 角色选择
* registerExecutor 完整实现 (2026-08-15 重构, 原 registerSupplier):
* registerExecutor / registerSponsor 完整实现 (2026-08-15 重构, 原 registerSupplier):
* 1. 校验短信验证码
* 2. 检查手机号/用户名是否已注册
* 3. 加密密码
* 4. 插入 sys_user (用户名=前端传入, role_type=executor, 主账号)
* 5. 插入 biz_org (executor 类型, business_nature/contact_phone 从表单)
* 注: 主账号不建 biz_person, 跟 sponsor 主账号保持一致
* 4. 插入 sys_user (用户名=前端传入, sys_user.role_type 写入 executor/sponsor, 主账号)
* 5. 插入 biz_org (对应类型, business_nature/contact_phone 从表单)
* 注: 主账号不建 biz_person
*
* 2026-08-16 重构: 删 biz_user_role_bind, 统一用 sys_user.role_type (见迁移脚本)
*/
@RestController
@RequestMapping("/business/auth")
@@ -101,7 +103,7 @@ public class BizAuthController extends BaseController {
user.setStatus("0");
userService.insertUser(user);
Long userId = user.getUserId();
// sys_user.role_type 字段需 = executor
// 业务角色统一写到 sys_user.role_type (2026-08-16 重构, 删 biz_user_role_bind)
userService.updateRoleType(userId, "executor");
// 5. 插入 biz_org (executor 类型, 主账号自己当 contact)
@@ -116,15 +118,78 @@ public class BizAuthController extends BaseController {
bizOrgService.insert(org);
Long orgId = org.getOrgId();
// 注: 主账号不建 biz_person (与 sponsor 主账号保持一致, 见 sponsor/Account.vue)
// 后续主账号在 [账号信息] 看到 [所属公司] 时, 后端可通过 sys_user.role_type 关联 biz_org 取 org_name
// 子账号 (biz_person.unit_type='sponsor' + user_id 关联 sys_user.parent_user_id) 由 [人员管理] 创建
return success("注册成功, 请等待审核").put("userId", userId).put("orgId", orgId);
}
/**
* 注册赞助方 (主账号), 流程与 executor 一致:
* 校验 → 查重 → 写 sys_user + role_type=sponsor → 写 biz_org (sponsor 类型)
*/
@PostMapping("/registerSponsor")
public AjaxResult registerSponsor(@RequestBody Map<String, Object> body) {
return success();
String username = (String) body.get("username");
String unitName = (String) body.get("unitName");
String businessNature = (String) body.get("businessNature");
String phone = (String) body.get("phone");
String code = (String) body.get("smsCode");
String password = (String) body.get("password");
String confirmPassword = (String) body.get("confirmPassword");
String uuid = (String) body.get("uuid");
// 1. 基础校验 (与 executor 同)
if (username == null || username.length() < 4 || username.length() > 20) return error("用户名长度 4-20 位");
if (!username.matches("^[A-Za-z0-9_]+$")) return error("用户名只能包含字母/数字/下划线");
if (unitName == null || unitName.isEmpty()) return error("企业名称不能为空");
if (businessNature == null || businessNature.isEmpty()) return error("企业性质不能为空");
if (phone == null || !phone.matches("^1\\d{10}$")) return error("手机号格式错误");
if (code == null || code.isEmpty()) return error("请输入短信验证码");
if (password == null || password.length() < 6 || password.length() > 20) return error("密码长度 6-20 位");
if (!password.equals(confirmPassword)) return error("两次密码输入不一致");
// 2. 校验短信验证码
SmsValidForm smsForm = new SmsValidForm();
smsForm.setPhone(phone);
smsForm.setSmsCode(code);
smsForm.setUuid(uuid);
try {
smsService.verifyCode(smsForm);
} catch (RuntimeException e) {
return error("验证码错误或已过期: " + e.getMessage());
}
// 3. 查重
if (userService.isPhoneRegistered(phone)) {
return error("该手机号已注册, 请直接登录");
}
SysUser nameCheck = new SysUser();
nameCheck.setUserName(username);
if (!userService.checkUserNameUnique(nameCheck)) {
return error("用户名已被占用: " + username);
}
// 4. 写 sys_user (role_type=sponsor)
SysUser user = new SysUser();
user.setUserName(username);
user.setNickName(unitName);
user.setPhonenumber(phone);
user.setPassword(passwordEncoder.encode(password));
user.setStatus("0");
userService.insertUser(user);
Long userId = user.getUserId();
userService.updateRoleType(userId, "sponsor");
// 5. 写 biz_org (sponsor 类型)
BizOrg org = new BizOrg();
org.setOrgId(null);
org.setOrgName(unitName);
org.setOrgType("sponsor");
org.setBusinessNature(businessNature);
org.setContactPhone(phone);
org.setContactName(unitName);
org.setStatus("0");
bizOrgService.insert(org);
Long orgId = org.getOrgId();
return success("注册成功, 请等待审核").put("userId", userId).put("orgId", orgId);
}
}
@@ -1,15 +1,22 @@
package com.ruoyi.business.controller;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.business.domain.BizExpert;
import com.ruoyi.business.domain.vo.BizExpertExportVo;
import com.ruoyi.business.domain.vo.BizExpertImportVo;
import com.ruoyi.business.service.IBizExpertService;
/**
@@ -41,11 +48,16 @@ public class BizExpertController extends BaseController
{
return success(bizExpertService.getByUserId(SecurityUtils.getUserId()));
}
/**
* admin 创建专家: 同时创建 sys_user (用户名=手机号, 密码=手机号)
* 返回 SysUser (含明文 password 给前端 toast 用)
*/
@Log(title = "专家", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody BizExpert bizExpert)
{
return toAjax(bizExpertService.insert(bizExpert));
SysUser created = bizExpertService.insert(bizExpert);
return success(created);
}
@Log(title = "专家", businessType = BusinessType.UPDATE)
@PutMapping
@@ -53,6 +65,16 @@ public class BizExpertController extends BaseController
{
return toAjax(bizExpertService.updateByPrimaryKey(bizExpert));
}
/**
* 启用/禁用专家: 同步 biz_expert.status + sys_user.status
* status='Y' 正常, status='N' 禁用
*/
@Log(title = "专家启停", businessType = BusinessType.UPDATE)
@PutMapping("/{expertId}/status")
public AjaxResult updateStatus(@PathVariable String expertId, @RequestParam String status)
{
return toAjax(bizExpertService.updateStatus(expertId, status));
}
/**
* 个人专家信息更新 (走 userId 路由, 后端强制注入当前登录用户)
* 不依赖前端传 expertId; 找不到 expert 行时自动 insert
@@ -71,4 +93,67 @@ public class BizExpertController extends BaseController
{
return toAjax(bizExpertService.deleteByPrimaryKeys(ids));
}
/**
* 导出专家列表 (中文列头)
*/
@Log(title = "专家", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('business:expert:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, BizExpert bizExpert)
{
List<BizExpert> list = bizExpertService.selectList(bizExpert);
// 转换为导出 VO (中文列头 + 审核状态/状态 可读映射)
List<BizExpertExportVo> exportList = new java.util.ArrayList<>(list.size());
for (BizExpert e : list) {
BizExpertExportVo v = new BizExpertExportVo();
v.setName(e.getName());
v.setPhone(e.getPhone());
v.setWorkUnit(e.getWorkUnit());
v.setDepartment(e.getDepartment());
v.setTitle(e.getTitle());
v.setRegion(e.getRegion());
v.setIdCard(e.getIdCard());
v.setBankCard(e.getBankCard());
v.setBankName(e.getBankName());
v.setBankProvince(e.getBankProvince());
v.setBankCity(e.getBankCity());
v.setBankAddress(e.getBankAddress());
v.setStatus(e.getStatus());
v.setAuditStatus(e.getAuditStatus());
v.setAuditBy(e.getAuditBy());
v.setAuditTime(e.getAuditTime());
v.setCreateTime(e.getCreateTime());
exportList.add(v);
}
ExcelUtil<BizExpertExportVo> util = new ExcelUtil<BizExpertExportVo>(BizExpertExportVo.class);
util.exportExcel(response, exportList, "专家数据");
}
/**
* 下载专家导入模板
*/
@GetMapping("/importTemplate")
public void importTemplate(HttpServletResponse response)
{
ExcelUtil<BizExpertImportVo> util = new ExcelUtil<BizExpertImportVo>(BizExpertImportVo.class);
util.importTemplateExcel(response, "专家数据");
}
/**
* 批量导入专家 (Excel), 参考 RuoYi 系统用户导入模式
* updateSupport=true: 同一手机号已存在 → 跳过 (按成功计)
* 返回 message 含成功/失败计数 + 失败明细
*/
@Log(title = "专家", businessType = BusinessType.IMPORT)
@PreAuthorize("@ss.hasPermi('business:expert:import')")
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception
{
ExcelUtil<BizExpertImportVo> util = new ExcelUtil<BizExpertImportVo>(BizExpertImportVo.class);
List<BizExpertImportVo> importList = util.importExcel(file.getInputStream());
String operName = SecurityUtils.getUsername();
String message = bizExpertService.importExpert(importList, updateSupport, operName);
return success(message);
}
}
@@ -13,7 +13,7 @@ import com.ruoyi.business.service.IBizOrgService;
/**
* 公司Controller (赞助方 + 执行方 共用)
* GET /business/org/list?orgType=sponsor|execution
* GET /business/org/list?orgType=sponsor|executor
*/
@RestController
@RequestMapping("/business/org")
@@ -100,8 +100,8 @@ public class BizRegisterController extends BaseController {
expert.setTitle(doctorTitle);
expert.setPracticeCertUrl(licenseCertUrl);
expert.setTitleCertUrl(titleCertUrl);
expert.setAuditStatus("0");
expert.setStatus("0");
expert.setAuditStatus("1"); // BizAuditStatusEnum.PENDING = 待审核
expert.setStatus("Y"); // biz_expert.status: Y=正常 N=禁用
expertService.insert(expert);
return success("注册成功, 请等待审核").put("userId", userId);
@@ -7,7 +7,7 @@ import com.ruoyi.common.core.domain.BaseEntity;
/**
* 公司对象 biz_org
* 用途: 赞助方(sponsor) + 执行方(execution) 共用
* 用途: 赞助方(sponsor) + 执行方(executor) 共用
* 重构说明: 原 biz_support_unit + biz_execution_unit 合并, 通过 org_type 区分
*/
public class BizOrg extends BaseEntity {
@@ -17,7 +17,7 @@ public class BizOrg extends BaseEntity {
/** org_name */
@Excel(name = "org_name")
private String orgName;
/** org_type: sponsor赞助方 / execution执行方 */
/** org_type: sponsor赞助方 / executor执行方 */
@Excel(name = "org_type")
private String orgType;
/** 企业性质 私营/国营/中外合资/外资/其他 */
@@ -52,7 +52,7 @@ public class BizProject extends BaseEntity {
/** org_name (赞助方/执行方, 由 org_type 区分) */
@Excel(name = "org_name")
private String orgName;
/** org_type: sponsor 赞助方 / execution 执行方 */
/** org_type: sponsor 赞助方 / executor 执行方 */
@Excel(name = "org_type")
private String orgType;
/** project_form */
@@ -0,0 +1,103 @@
package com.ruoyi.business.domain.vo;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;
/**
* 专家导出 VO (中文列头, 用户友好)
*
* <p>仅用于 Excel 导出, 不参与业务逻辑.
*
* @author guoju
*/
public class BizExpertExportVo {
@Excel(name = "姓名", sort = 1)
private String name;
@Excel(name = "手机号", sort = 2)
private String phone;
@Excel(name = "工作单位", sort = 3)
private String workUnit;
@Excel(name = "科室", sort = 4)
private String department;
@Excel(name = "职称", sort = 5)
private String title;
@Excel(name = "地区", sort = 6)
private String region;
@Excel(name = "身份证件号码", sort = 7)
private String idCard;
@Excel(name = "银行卡号", sort = 8)
private String bankCard;
@Excel(name = "银行名称", sort = 9)
private String bankName;
@Excel(name = "开户行省份", sort = 10)
private String bankProvince;
@Excel(name = "开户行城市", sort = 11)
private String bankCity;
@Excel(name = "开户行地址", sort = 12)
private String bankAddress;
@Excel(name = "状态", sort = 13, readConverterExp = "Y=正常,N=禁用")
private String status;
@Excel(name = "审核状态", sort = 14, readConverterExp = "0=未提交,1=待审核,2=通过,3=拒绝")
private String auditStatus;
@Excel(name = "审核人", sort = 15)
private String auditBy;
@Excel(name = "审核时间", sort = 16, dateFormat = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date auditTime;
@Excel(name = "创建时间", sort = 17, dateFormat = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getWorkUnit() { return workUnit; }
public void setWorkUnit(String workUnit) { this.workUnit = workUnit; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getRegion() { return region; }
public void setRegion(String region) { this.region = region; }
public String getIdCard() { return idCard; }
public void setIdCard(String idCard) { this.idCard = idCard; }
public String getBankCard() { return bankCard; }
public void setBankCard(String bankCard) { this.bankCard = bankCard; }
public String getBankName() { return bankName; }
public void setBankName(String bankName) { this.bankName = bankName; }
public String getBankProvince() { return bankProvince; }
public void setBankProvince(String bankProvince) { this.bankProvince = bankProvince; }
public String getBankCity() { return bankCity; }
public void setBankCity(String bankCity) { this.bankCity = bankCity; }
public String getBankAddress() { return bankAddress; }
public void setBankAddress(String bankAddress) { this.bankAddress = bankAddress; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getAuditStatus() { return auditStatus; }
public void setAuditStatus(String auditStatus) { this.auditStatus = auditStatus; }
public String getAuditBy() { return auditBy; }
public void setAuditBy(String auditBy) { this.auditBy = auditBy; }
public Date getAuditTime() { return auditTime; }
public void setAuditTime(Date auditTime) { this.auditTime = auditTime; }
public Date getCreateTime() { return createTime; }
public void setCreateTime(Date createTime) { this.createTime = createTime; }
}
@@ -0,0 +1,117 @@
package com.ruoyi.business.domain.vo;
import com.ruoyi.common.annotation.Excel;
/**
* 专家导入 VO
*
* <p>仅用于 Excel 批量导入 / 模板下载 / 导出, 不参与业务逻辑.
* 字段顺序与 Excel 列一致, 调整时同步修改模板下载体验.
*
* @author guoju
*/
public class BizExpertImportVo {
/** 姓名 */
@Excel(name = "姓名", sort = 1)
private String name;
/** 手机号 */
@Excel(name = "手机号", sort = 2)
private String phone;
/** 工作单位 */
@Excel(name = "工作单位", sort = 3)
private String workUnit;
/** 科室 */
@Excel(name = "科室", sort = 4)
private String department;
/** 职称 */
@Excel(name = "职称", sort = 5)
private String title;
/** 地区 */
@Excel(name = "地区", sort = 6)
private String region;
/** 身份证件号码 */
@Excel(name = "身份证件号码", sort = 7)
private String idCard;
/** 身份证件照片 正面URL */
@Excel(name = "身份证正面URL", sort = 8)
private String idCardFrontUrl;
/** 身份证件照片 反面URL */
@Excel(name = "身份证反面URL", sort = 9)
private String idCardBackUrl;
/** 执业医师证书 URL */
@Excel(name = "执业医师证书URL", sort = 10)
private String practiceCertUrl;
/** 职称证明 URL */
@Excel(name = "职称证明URL", sort = 11)
private String titleCertUrl;
/** 银行卡号 */
@Excel(name = "银行卡号", sort = 12)
private String bankCard;
/** 银行名称 */
@Excel(name = "银行名称", sort = 13)
private String bankName;
/** 开户行省份 */
@Excel(name = "开户行省份", sort = 14)
private String bankProvince;
/** 开户行城市 */
@Excel(name = "开户行城市", sort = 15)
private String bankCity;
/** 开户行地址 */
@Excel(name = "开户行地址", sort = 16)
private String bankAddress;
/** 状态 (Y=正常 N=禁用, 留空默认 Y) */
@Excel(name = "状态", sort = 17, readConverterExp = "Y=正常,N=禁用")
private String status;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getWorkUnit() { return workUnit; }
public void setWorkUnit(String workUnit) { this.workUnit = workUnit; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getRegion() { return region; }
public void setRegion(String region) { this.region = region; }
public String getIdCard() { return idCard; }
public void setIdCard(String idCard) { this.idCard = idCard; }
public String getIdCardFrontUrl() { return idCardFrontUrl; }
public void setIdCardFrontUrl(String idCardFrontUrl) { this.idCardFrontUrl = idCardFrontUrl; }
public String getIdCardBackUrl() { return idCardBackUrl; }
public void setIdCardBackUrl(String idCardBackUrl) { this.idCardBackUrl = idCardBackUrl; }
public String getPracticeCertUrl() { return practiceCertUrl; }
public void setPracticeCertUrl(String practiceCertUrl) { this.practiceCertUrl = practiceCertUrl; }
public String getTitleCertUrl() { return titleCertUrl; }
public void setTitleCertUrl(String titleCertUrl) { this.titleCertUrl = titleCertUrl; }
public String getBankCard() { return bankCard; }
public void setBankCard(String bankCard) { this.bankCard = bankCard; }
public String getBankName() { return bankName; }
public void setBankName(String bankName) { this.bankName = bankName; }
public String getBankProvince() { return bankProvince; }
public void setBankProvince(String bankProvince) { this.bankProvince = bankProvince; }
public String getBankCity() { return bankCity; }
public void setBankCity(String bankCity) { this.bankCity = bankCity; }
public String getBankAddress() { return bankAddress; }
public void setBankAddress(String bankAddress) { this.bankAddress = bankAddress; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
}
@@ -2,6 +2,8 @@ package com.ruoyi.business.service;
import java.util.List;
import com.ruoyi.business.domain.BizExpert;
import com.ruoyi.business.domain.vo.BizExpertImportVo;
import com.ruoyi.common.core.domain.entity.SysUser;
/**
* 专家Service接口
@@ -11,10 +13,26 @@ public interface IBizExpertService
BizExpert getById(String expertId);
BizExpert getByUserId(Long userId);
List<BizExpert> selectList(BizExpert entity);
int insert(BizExpert entity);
/**
* 新建专家: 同时创建 sys_user (用户名=手机号, 密码=手机号, role_type=doctor)
* 若 sys_user.username(=phone) 已存在, 抛 ServiceException("该手机号已注册")
* 返回 SysUser 含 username + 明文 password (仅本次返回,前端 toast 显示后即丢)
*/
SysUser insert(BizExpert entity);
int updateByPrimaryKey(BizExpert entity);
/**
* 启用/禁用专家: 同步更新 biz_expert.status + sys_user.status
* status='Y' 正常, status='N' 禁用
*/
int updateStatus(String expertId, String status);
/** 按 userId 更新或新建 (upsert) */
int updateProfileByUserId(BizExpert entity);
int deleteByPrimaryKey(String expertId);
int deleteByPrimaryKeys(String[] expertId);
/**
* 批量导入专家: 每行调用 insert, updateSupport=true 时跳过已存在手机号(视为成功)
* 返回 String 含成功/失败计数 + 失败明细 (前端 toast 显示)
*/
String importExpert(List<BizExpertImportVo> importList, boolean updateSupport, String operName);
}
@@ -4,8 +4,13 @@ import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.business.domain.BizExpert;
import com.ruoyi.business.domain.vo.BizExpertImportVo;
import com.ruoyi.business.mapper.BizExpertMapper;
import com.ruoyi.business.service.IBizExpertService;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.system.service.ISysUserService;
@Service
public class BizExpertServiceImpl implements IBizExpertService
@@ -13,6 +18,9 @@ public class BizExpertServiceImpl implements IBizExpertService
@Autowired
private BizExpertMapper bizExpertMapper;
@Autowired
private ISysUserService sysUserService;
@Override
public BizExpert getById(String expertId)
{ return bizExpertMapper.selectByPrimaryKey(expertId); }
@@ -22,11 +30,86 @@ public class BizExpertServiceImpl implements IBizExpertService
@Override
public List<BizExpert> selectList(BizExpert entity)
{ return bizExpertMapper.selectList(entity); }
/**
* admin 创建专家: 同时创建 sys_user (用户名=手机号, 密码=手机号, role_type=doctor)
* 1. 校验 phone 没注册过 (抛 ServiceException)
* 2. 创建 sys_user + bcrypt 加密密码
* 3. 设置 role_type=doctor (与公开注册一致)
* 4. 创建 biz_expert 绑定 user_id
* 5. 返回 SysUser 含明文 password (前端 toast 用完即丢)
*/
@Override
public int insert(BizExpert entity) { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "expertId"); return bizExpertMapper.insert(entity); }
public SysUser insert(BizExpert entity) {
String phone = entity.getPhone();
if (phone == null || phone.isEmpty()) {
throw new ServiceException("手机号不能为空");
}
// 0. 校验 phone 唯一 (查 sys_user, 若 username=phone 已存在即重复)
if (sysUserService.isPhoneRegistered(phone)) {
throw new ServiceException("该手机号已注册,请直接登录");
}
// 1. 创建 sys_user (用户名=phone, 密码=phone)
SysUser newUser = new SysUser();
newUser.setUserName(phone);
newUser.setNickName(entity.getName());
newUser.setPhonenumber(phone);
newUser.setPassword(SecurityUtils.encryptPassword(phone));
newUser.setStatus("0");
newUser.setDelFlag("0");
newUser.setCreateBy(SecurityUtils.getUsername());
sysUserService.insertUser(newUser);
Long userId = newUser.getUserId();
// 2. role_type = doctor (跟公开注册一致,DB 默认 executor, 专家需 doctor)
sysUserService.updateRoleType(userId, "doctor");
// 3. 创建 biz_expert 绑定 user_id
entity.setUserId(userId);
com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "expertId");
bizExpertMapper.insert(entity);
// 4. 把明文密码回填 SysUser (仅本次返回,前端 toast 显示)
newUser.setPassword(phone);
return newUser;
}
@Override
public int updateByPrimaryKey(BizExpert entity)
{ return bizExpertMapper.updateByPrimaryKey(entity); }
/**
* 启用/禁用专家: 同步 biz_expert.status + sys_user.status
* biz_expert.status: 'Y'=正常 'N'=禁用
* sys_user.status: '0'=正常 '1'=停用 (RuoYi 框架约定, 同步时转换)
*/
@Override
public int updateStatus(String expertId, String status) {
if (status == null || (!"Y".equals(status) && !"N".equals(status))) {
throw new ServiceException("status 必须是 'Y'(正常) 或 'N'(禁用)");
}
BizExpert existed = bizExpertMapper.selectByPrimaryKey(expertId);
if (existed == null) {
throw new ServiceException("专家不存在");
}
// 1. 更新 biz_expert.status
BizExpert update = new BizExpert();
update.setExpertId(expertId);
update.setStatus(status);
update.setUpdateBy(SecurityUtils.getUsername());
int n = bizExpertMapper.updateByPrimaryKey(update);
// 2. 同步 sys_user.status (登录账号启停, 'Y'→'0', 'N'→'1')
if (n > 0 && existed.getUserId() != null) {
SysUser u = new SysUser();
u.setUserId(existed.getUserId());
u.setStatus("Y".equals(status) ? "0" : "1");
u.setUpdateBy(SecurityUtils.getUsername());
sysUserService.updateUser(u);
}
return n;
}
@Override
public int updateProfileByUserId(BizExpert entity)
{
@@ -42,4 +125,107 @@ public class BizExpertServiceImpl implements IBizExpertService
@Override
public int deleteByPrimaryKeys(String[] expertId)
{ return bizExpertMapper.deleteByPrimaryKeys(expertId); }
/**
* 批量导入专家:
* 1. 每行 必填校验 (姓名/手机号/工作单位/科室/职称, 手机号正则 1[0-9]{10})
* 2. status 留空 → Y (正常); 非法值 → 抛错
* 3. sys_user 同步创建 (与单条 insert 共用 sys_userService.insertUser)
* 4. updateSupport=true 时: 同一手机号已存在 → 跳过 (按成功计)
* 5. 单行失败不中断, 错误信息累计
*/
@Override
public String importExpert(List<BizExpertImportVo> importList, boolean updateSupport, String operName) {
if (importList == null || importList.isEmpty()) {
throw new ServiceException("导入数据不能为空");
}
int successNum = 0;
int failureNum = 0;
StringBuilder successMsg = new StringBuilder();
StringBuilder failureMsg = new StringBuilder();
for (int i = 0; i < importList.size(); i++) {
BizExpertImportVo vo = importList.get(i);
int rowNo = i + 2; // Excel 行号 (1=表头)
try {
// 必填校验
if (vo.getName() == null || vo.getName().trim().isEmpty()) {
throw new ServiceException("姓名不能为空");
}
if (vo.getPhone() == null || vo.getPhone().trim().isEmpty()) {
throw new ServiceException("手机号不能为空");
}
if (!vo.getPhone().matches("^1[0-9]\\d{9}$")) {
throw new ServiceException("手机号格式不正确");
}
if (vo.getWorkUnit() == null || vo.getWorkUnit().trim().isEmpty()) {
throw new ServiceException("工作单位不能为空");
}
if (vo.getDepartment() == null || vo.getDepartment().trim().isEmpty()) {
throw new ServiceException("科室不能为空");
}
if (vo.getTitle() == null || vo.getTitle().trim().isEmpty()) {
throw new ServiceException("职称不能为空");
}
// status 校验, 留空 → Y
String status = vo.getStatus();
if (status == null || status.trim().isEmpty()) {
status = "Y";
} else if (!"Y".equals(status) && !"N".equals(status)) {
throw new ServiceException("状态只能是 Y(正常) 或 N(禁用)");
}
// 同一手机号已存在 → updateSupport=true 跳过, 否则报错
if (sysUserService.isPhoneRegistered(vo.getPhone())) {
if (updateSupport) {
successNum++;
successMsg.append("<br/>" + successNum + "、手机号 " + vo.getPhone() + " 已存在, 已跳过");
continue;
}
throw new ServiceException("手机号 " + vo.getPhone() + " 已存在");
}
// 复用单条 insert 逻辑 (含 sys_user 创建)
BizExpert entity = new BizExpert();
entity.setName(vo.getName());
entity.setPhone(vo.getPhone());
entity.setWorkUnit(vo.getWorkUnit());
entity.setDepartment(vo.getDepartment());
entity.setTitle(vo.getTitle());
entity.setRegion(vo.getRegion());
entity.setIdCard(vo.getIdCard());
entity.setIdCardFrontUrl(vo.getIdCardFrontUrl());
entity.setIdCardBackUrl(vo.getIdCardBackUrl());
entity.setPracticeCertUrl(vo.getPracticeCertUrl());
entity.setTitleCertUrl(vo.getTitleCertUrl());
entity.setBankCard(vo.getBankCard());
entity.setBankName(vo.getBankName());
entity.setBankProvince(vo.getBankProvince());
entity.setBankCity(vo.getBankCity());
entity.setBankAddress(vo.getBankAddress());
entity.setStatus(status);
// admin 批量导入默认通过审核 (与单条 admin 创建一致)
entity.setAuditStatus("2");
entity.setAuditBy(operName);
entity.setAuditTime(new java.util.Date());
entity.setCreateBy(operName);
insert(entity);
successNum++;
successMsg.append("<br/>" + successNum + "、账号 " + vo.getPhone() + " 导入成功");
} catch (Exception e) {
failureNum++;
String msg = e.getMessage();
if (msg == null) msg = e.getClass().getSimpleName();
failureMsg.append("<br/>" + failureNum + "、第 " + rowNo + " 行: " + msg);
}
}
if (failureNum > 0) {
failureMsg.insert(0, "很抱歉, 部分导入失败, 共 " + failureNum + " 条:");
}
if (successNum > 0) {
successMsg.insert(0, "恭喜您, 数据已全部导入成功!共 " + successNum + " 条, 数据如下:");
}
return successMsg.toString() + failureMsg.toString();
}
}
@@ -29,7 +29,7 @@
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectFields">
select expert_id, user_id, name, phone, region, id_card, work_unit, department, title, practice_cert_url, title_cert_url, bank_card, bank_name, bank_province, bank_city, bank_address, id_card_front_url, id_card_back_url, audit_status, audit_by, audit_time, create_by, create_time, update_by, update_time
select expert_id, user_id, name, phone, region, id_card, work_unit, department, title, practice_cert_url, title_cert_url, bank_card, bank_name, bank_province, bank_city, bank_address, id_card_front_url, id_card_back_url, audit_status, audit_by, audit_time, status, create_by, create_time, update_by, update_time
from biz_expert
</sql>
<select id="selectByUserId" resultMap="BizExpertResult" parameterType="Long">
@@ -197,7 +197,7 @@
<if test="auditStatus != null and auditStatus != ''">audit_status = #{auditStatus},</if>
<if test="auditOpinion != null and auditOpinion != ''">audit_opinion = #{auditOpinion},</if>
<if test="auditBy != null and auditBy != ''">audit_by = #{auditBy},</if>
<if test="auditTime != null and auditTime != ''">audit_time = #{auditTime},</if>
<if test="auditTime != null">audit_time = #{auditTime},</if>
<if test="status != null and status != ''">status = #{status},</if>
<if test="name != null">name = #{name},</if>
<if test="phone != null">phone = #{phone},</if>
@@ -0,0 +1,57 @@
package com.ruoyi.common.enums;
/**
* 业务审核状态
* <p>
* 通用枚举: 0=未提交, 1=待审核, 2=通过, 3=拒绝
* <p>
* 适用于 biz_expert.audit_status, biz_project.audit_status 等业务审核场景.
*
* @author guoju
*/
public enum BizAuditStatusEnum
{
/** 草稿/未提交 (用户保存但未提交审核) */
UNSUBMITTED("0", "未提交"),
/** 已提交, 等待管理员审核 */
PENDING("1", "待审核"),
/** 审核通过 */
APPROVED("2", "通过"),
/** 审核拒绝 */
REJECTED("3", "拒绝");
private final String code;
private final String info;
BizAuditStatusEnum(String code, String info)
{
this.code = code;
this.info = info;
}
public String getCode()
{
return code;
}
public String getInfo()
{
return info;
}
/**
* 根据 code 解析枚举 (找不到返回 null, 调用方自行 fallback)
*/
public static BizAuditStatusEnum of(String code)
{
if (code == null) return null;
for (BizAuditStatusEnum e : values())
{
if (e.code.equals(code)) return e;
}
return null;
}
}
@@ -0,0 +1,28 @@
package com.ruoyi.system.domain.vo;
import com.ruoyi.common.core.domain.entity.SysUser;
/**
* 用户扩展视图对象
* <p>
* 继承自 SysUser, 仅在 list 场景下附加业务字段 (sys_user.role_type),
* 不影响 SysUser 实体本身, 不参与新增/修改/详情接口。
* </p>
*/
public class SysUserExtendVo extends SysUser
{
private static final long serialVersionUID = 1L;
/** 业务角色 (admin / leader / manager / doctor / executor / sponsor) */
private String roleType;
public String getRoleType()
{
return roleType;
}
public void setRoleType(String roleType)
{
this.roleType = roleType;
}
}
+15
View File
@@ -91,3 +91,18 @@ export const bizGet = (entity, id) => request.get(`/business/${entity}/${id}`)
export const bizAdd = (entity, data) => request.post(`/business/${entity}`, data)
export const bizUpdate = (entity, data) => request.put(`/business/${entity}`, data)
export const bizDelete = (entity, ids) => request.delete(`/business/${entity}/${ids}`)
// 人员批量导入 (unitType: 'sponsor' | 'executor')
export const importPerson = (unitType, file) => {
const form = new FormData()
form.append('file', file)
return request.post(`/business/person/${unitType}Import`, form, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
// 人员导入模板下载
export const downloadImportTemplate = (unitType) =>
request.get(`/business/person/${unitType}ImportTemplate`, { responseType: 'blob' })
// 专家导入模板下载
export const downloadExpertTemplate = () =>
request.get('/business/expert/importTemplate', { responseType: 'blob' })
+63
View File
@@ -0,0 +1,63 @@
<template>
<!--
业务审核状态标签
通用封装, 后端 BizAuditStatusEnum 对应
0=未提交 1=待审核 2=通过 3=拒绝
-->
<el-tag
v-if="resolved"
:type="resolved.type"
:effect="effect"
:size="size"
>{{ resolved.text }}</el-tag>
<span v-else style="color:#c0c4cc">{{ emptyText }}</span>
</template>
<script setup>
import { computed } from 'vue'
/**
* 审核状态标签
*
* 用法:
* <audit-status-tag :status="row.auditStatus" />
* <audit-status-tag :status="row.auditStatus" size="small" effect="dark" />
*
* 兼容: 若传入旧值 ('审核通过'/'已退回'/'已驳回'/'待审核' 等中文), 自动归一化到新枚举
*/
const props = defineProps({
status: { type: [String, Number, null], default: null },
size: { type: String, default: 'small' }, // small / default / large
effect: { type: String, default: 'light' }, // light / dark / plain
emptyText: { type: String, default: '-' },
})
// 映射表: 后端 BizAuditStatusEnum 一致
const MAP = {
'0': { text: '未提交', type: 'info' },
'1': { text: '待审核', type: 'warning' },
'2': { text: '通过', type: 'success' },
'3': { text: '拒绝', type: 'danger' },
}
// 旧值兼容 (历史脏数据 / 老代码)
const LEGACY = {
'待审核': '1',
'审核通过': '2',
'通过': '2',
'已退回': '3',
'已驳回': '3',
'退回': '3',
'驳回': '3',
'拒绝': '3',
}
const resolved = computed(() => {
let s = props.status
if (s == null || s === '') return null
s = String(s).trim()
if (MAP[s]) return MAP[s]
if (LEGACY[s]) return MAP[LEGACY[s]]
return null
})
</script>
+26 -7
View File
@@ -7,17 +7,18 @@
placeholder="证书"
/>
block=true 时撑满父容器宽度 (用于表单行内全宽上传), 字体缩小到 14px
readonly=true 时不触发上传, 点击缩略图弹 el-image 预览
上传走 OSS 直传 (utils/oss.js uploadToOss)
-->
<template>
<div class="ht-image-upload-content" :class="{ 'has-file': modelValue }" @click="handleClick">
<div class="ht-image-upload-content" :class="{ 'has-file': modelValue, 'is-block': block }" @click="handleClick">
<div v-if="!modelValue && !readonly" class="ht-image-placeholder-wrap">
<span class="placeholder-text">{{ placeholder }}</span>
</div>
<div v-else-if="!modelValue && readonly" style="width: 90px;height: 60px; background: #f2f4f5;"></div>
<div v-else-if="!modelValue && readonly" :style="readonlyBoxStyle"></div>
<img v-else-if="!readonly" class="ht-image" :src="modelValue" />
<el-image v-else :src="modelValue" style="width: 90px;height: 60px; display: flex; justify-content: center; align-items: center; background: #f2f4f5;"
<el-image v-else :src="modelValue" :style="readonlyBoxStyle"
:preview-src-list="[modelValue]"></el-image>
</div>
<input
@@ -30,7 +31,7 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { uploadToOss } from '@/utils/oss'
@@ -38,7 +39,9 @@ const props = defineProps({
modelValue: { type: String, default: '' },
dir: { type: String, default: 'ry8080/image/' },
placeholder: { type: String, default: '点击上传' },
readonly: { type: Boolean, default: false }
readonly: { type: Boolean, default: false },
/** true 时撑满父容器宽度, 用于表单行内全宽上传 */
block: { type: Boolean, default: false }
})
const emit = defineEmits(['update:modelValue'])
@@ -46,6 +49,13 @@ const emit = defineEmits(['update:modelValue'])
const fileInput = ref(null)
const uploading = ref(false)
const readonlyBoxStyle = computed(() => {
if (props.block) {
return 'width: 100%; height: 100px; background: #f2f4f5; display: flex; justify-content: center; align-items: center;'
}
return 'width: 90px; height: 60px; background: #f2f4f5; display: flex; justify-content: center; align-items: center;'
})
function handleClick() {
if (props.readonly || uploading.value) return
fileInput.value?.click()
@@ -90,6 +100,10 @@ async function onFileChange(e) {
cursor: pointer;
background: #fafafa;
}
.ht-image-upload-content.is-block {
width: 100%;
height: 100px;
}
.ht-image-upload-content:hover {
border-color: #1890ff;
}
@@ -105,11 +119,16 @@ async function onFileChange(e) {
justify-content: center;
}
.placeholder-text {
font-size: 24px;
font-weight: 300;
font-size: 14px;
font-weight: 400;
color: #8c8c8c;
line-height: 1;
}
.ht-image-upload-content.is-block .placeholder-text {
font-size: 14px;
font-weight: 400;
color: #8c8c8c;
}
.ht-image {
width: 100%;
height: 100%;
+4 -2
View File
@@ -61,7 +61,8 @@ const MENU = {
{ path: '/admin/department', title: '科室管理', icon: Folder },
{ path: '/admin/title', title: '职称管理', icon: Medal },
{ path: '/admin/experts', title: '专家管理', icon: UserFilled },
{ path: '/admin/orgs', title: '公司管理', icon: Connection },
{ path: '/admin/sponsor-orgs', title: '支持单位管理', icon: Connection },
{ path: '/admin/executor-orgs', title: '服务机构管理', icon: OfficeBuilding },
{ path: '/admin/account', title: '账号信息', icon: User }
],
leader: [
@@ -76,7 +77,8 @@ const MENU = {
{ path: '/manager/projects', title: '项目管理', icon: Document },
{ path: '/manager/meetings', title: '会议管理', icon: Calendar },
{ path: '/manager/experts', title: '专家审核', icon: User },
{ path: '/manager/orgs', title: '公司管理', icon: OfficeBuilding },
{ path: '/manager/sponsor-orgs', title: '支持单位管理', icon: Connection },
{ path: '/manager/executor-orgs', title: '服务机构管理', icon: OfficeBuilding },
{ path: '/manager/support-intent', title: '支持意向', icon: Tickets },
{ path: '/manager/exec-intent', title: '执行意向', icon: Tickets },
{ path: '/manager/accounts', title: '账号管理', icon: Setting }
+21
View File
@@ -28,6 +28,17 @@ const routes = [
{ path: 'department', name: 'admin-department', component: () => import('@/views/admin/Department.vue'), meta: { title: '科室管理' } },
{ path: 'title', name: 'admin-title', component: () => import('@/views/admin/Title.vue'), meta: { title: '职称管理' } },
{ path: 'experts', name: 'admin-experts', component: () => import('@/views/admin/Experts.vue'), meta: { title: '专家管理' } },
{ path: 'experts/new', name: 'admin-experts-new', component: () => import('@/views/admin/ExpertNew.vue'), meta: { title: '新建专家' } },
{ path: 'experts/edit/:id', name: 'admin-experts-edit', component: () => import('@/views/admin/ExpertNew.vue'), meta: { title: '编辑专家' } },
{ path: 'experts/view/:id', name: 'admin-experts-view', component: () => import('@/views/admin/ExpertDetail.vue'), meta: { title: '专家详情' } },
{ path: 'sponsor-orgs', name: 'admin-sponsor-orgs', component: () => import('@/views/admin/SponsorOrgs.vue'), meta: { title: '支持单位管理' } },
{ path: 'executor-orgs', name: 'admin-executor-orgs', component: () => import('@/views/admin/ExecutorOrgs.vue'), meta: { title: '服务机构管理' } },
{ path: 'sponsor-people', name: 'admin-sponsor-people', component: () => import('@/views/admin/SponsorPeople.vue'), meta: { title: '支持单位下人员' } },
{ path: 'sponsor-people/new', name: 'admin-sponsor-people-new', component: () => import('@/views/admin/SponsorPersonNew.vue'), meta: { title: '新建人员' } },
{ path: 'sponsor-people/edit/:id', name: 'admin-sponsor-people-edit', component: () => import('@/views/admin/SponsorPersonNew.vue'), meta: { title: '编辑人员' } },
{ path: 'executor-people', name: 'admin-executor-people', component: () => import('@/views/admin/ExecutorPeople.vue'), meta: { title: '服务机构下人员' } },
{ path: 'executor-people/new', name: 'admin-executor-people-new', component: () => import('@/views/admin/ExecutorPersonNew.vue'), meta: { title: '新建人员' } },
{ path: 'executor-people/edit/:id', name: 'admin-executor-people-edit', component: () => import('@/views/admin/ExecutorPersonNew.vue'), meta: { title: '编辑人员' } },
{ path: 'orgs', name: 'admin-orgs', component: () => import('@/views/admin/Orgs.vue'), meta: { title: '公司管理' } },
{ path: 'account', name: 'admin-account', component: () => import('@/views/admin/Account.vue'), meta: { title: '账号信息' } }
]
@@ -51,6 +62,14 @@ const routes = [
{ path: 'meetings', name: 'manager-meetings', component: () => import('@/views/manager/Meetings.vue'), meta: { title: '会议管理' } },
{ path: 'meetings/new', name: 'manager-meetings-new', component: () => import('@/views/manager/MeetingNew.vue'), meta: { title: '新建会议' } },
{ path: 'experts', name: 'manager-experts', component: () => import('@/views/manager/Experts.vue'), meta: { title: '专家审核' } },
{ path: 'sponsor-orgs', name: 'manager-sponsor-orgs', component: () => import('@/views/manager/SponsorOrgs.vue'), meta: { title: '支持单位管理' } },
{ path: 'executor-orgs', name: 'manager-executor-orgs', component: () => import('@/views/manager/ExecutorOrgs.vue'), meta: { title: '服务机构管理' } },
{ path: 'sponsor-people', name: 'manager-sponsor-people', component: () => import('@/views/manager/SponsorPeople.vue'), meta: { title: '支持单位下人员' } },
{ path: 'sponsor-people/new', name: 'manager-sponsor-people-new', component: () => import('@/views/manager/SponsorPersonNew.vue'), meta: { title: '新建人员' } },
{ path: 'sponsor-people/edit/:id', name: 'manager-sponsor-people-edit', component: () => import('@/views/manager/SponsorPersonNew.vue'), meta: { title: '编辑人员' } },
{ path: 'executor-people', name: 'manager-executor-people', component: () => import('@/views/manager/ExecutorPeople.vue'), meta: { title: '服务机构下人员' } },
{ path: 'executor-people/new', name: 'manager-executor-people-new', component: () => import('@/views/manager/ExecutorPersonNew.vue'), meta: { title: '新建人员' } },
{ path: 'executor-people/edit/:id', name: 'manager-executor-people-edit', component: () => import('@/views/manager/ExecutorPersonNew.vue'), meta: { title: '编辑人员' } },
{ path: 'orgs', name: 'manager-orgs', component: () => import('@/views/manager/Orgs.vue'), meta: { title: '公司管理' } },
{ path: 'support-intent', name: 'manager-support-intent', component: () => import('@/views/manager/SupportIntent.vue'), meta: { title: '支持意向' } },
{ path: 'exec-intent', name: 'manager-exec-intent', component: () => import('@/views/manager/ExecIntent.vue'), meta: { title: '执行意向' } },
@@ -76,6 +95,8 @@ const routes = [
{ path: 'meetings', name: 'executor-meetings', component: () => import('@/views/executor/Meetings.vue'), meta: { title: '会议执行' } },
{ path: 'projects', name: 'executor-projects', component: () => import('@/views/executor/Projects.vue'), meta: { title: '项目列表' } },
{ path: 'people', name: 'executor-people', component: () => import('@/views/executor/People.vue'), meta: { title: '人员管理' } },
{ path: 'people/new', name: 'executor-people-new', component: () => import('@/views/executor/NewPerson.vue'), meta: { title: '新建人员' } },
{ path: 'people/edit/:id', name: 'executor-people-edit', component: () => import('@/views/executor/NewPerson.vue'), meta: { title: '编辑人员' } },
{ path: 'account', name: 'executor-account', component: () => import('@/views/executor/Account.vue'), meta: { title: '账号信息' } },
{ path: 'labor', name: 'executor-labor', component: () => import('@/views/executor/Labor.vue'), meta: { title: '劳务凭证' } }
]
+47 -48
View File
@@ -1,53 +1,51 @@
<template>
<div class="admin-account">
<div class="page-card">
<div class="desc">查看当前登录账号修改个人信息与密码</div>
<div class="page-card admin-account">
<div class="breadcrumb">首页 / 账号信息</div>
<el-descriptions :column="2" border style="margin-bottom: 24px">
<el-descriptions-item label="账号">{{ profile.userName }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ profile.nickName || profile.userName }}</el-descriptions-item>
<el-descriptions-item label="角色">{{ roleLabel }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ profile.phonenumber || '-' }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ profile.email || '-' }}</el-descriptions-item>
<el-descriptions-item label="最近登录">{{ profile.loginDate || '-' }}</el-descriptions-item>
</el-descriptions>
<el-descriptions :column="2" border style="margin-bottom: 24px">
<el-descriptions-item label="账号">{{ profile.userName }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ profile.nickName || profile.userName }}</el-descriptions-item>
<el-descriptions-item label="角色">{{ roleLabel }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ profile.phonenumber || '-' }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ profile.email || '-' }}</el-descriptions-item>
<el-descriptions-item label="最近登录">{{ profile.loginDate || '-' }}</el-descriptions-item>
</el-descriptions>
<el-tabs v-model="tab">
<el-tab-pane label="修改资料" name="profile">
<el-form :model="form" label-width="100px" style="max-width: 480px">
<el-form-item label="姓名">
<el-input v-model="form.nickName" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="form.phonenumber" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="saveProfile">保存</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tabs v-model="tab">
<el-tab-pane label="修改资料" name="profile">
<el-form :model="form" label-width="100px" class="account-form">
<el-form-item label="姓名">
<el-input v-model="form.nickName" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="form.phonenumber" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="saveProfile">保存</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="修改密码" name="password">
<el-form :model="pwd" label-width="100px" style="max-width: 480px">
<el-form-item label="旧密码">
<el-input v-model="pwd.oldPassword" type="password" show-password placeholder="请输入旧密码" />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="pwd.newPassword" type="password" show-password placeholder="请输入新密码" />
</el-form-item>
<el-form-item label="确认新密码">
<el-input v-model="pwd.confirmPassword" type="password" show-password placeholder="请再次输入新密码" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="changePassword">保存</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</div>
<el-tab-pane label="修改密码" name="password">
<el-form :model="pwd" label-width="100px" class="account-form">
<el-form-item label="旧密码">
<el-input v-model="pwd.oldPassword" type="password" show-password placeholder="请输入旧密码" />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="pwd.newPassword" type="password" show-password placeholder="请输入新密码" />
</el-form-item>
<el-form-item label="确认新密码">
<el-input v-model="pwd.confirmPassword" type="password" show-password placeholder="请再次输入新密码" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="changePassword">保存</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</div>
</template>
@@ -107,7 +105,8 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-account { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.account-form { max-width: 480px; }
</style>
+50 -52
View File
@@ -1,56 +1,54 @@
<template>
<div class="admin-department">
<div class="page-card">
<div class="desc">维护医生可用的科室字典停用的科室不会出现在医生选择下拉中</div>
<div class="page-card admin-department">
<div class="breadcrumb">首页 / 科室管理</div>
<el-form inline :model="q" class="filter-bar">
<el-form-item label="科室名称:">
<el-input v-model="q.name" placeholder="输入名称模糊搜索" clearable />
</el-form-item>
<el-form-item label="状态:">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option label="用" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="openAdd">新增科室</el-button>
</el-form-item>
</el-form>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="科室名称"><el-input v-model="q.name" placeholder="输入名称模糊搜索" clearable /></el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option label="启用" value="0" />
<el-option label="用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="openAdd">新增科室</el-button>
</el-form-item>
</el-form>
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="deptId" label="ID" width="80" />
<el-table-column prop="name" label="科室名称" min-width="200" />
<el-table-column prop="sort" label="排序" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'info'">{{ row.status === '0' ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="small" link :type="row.status === '0' ? 'warning' : 'success'" @click="toggleStatus(row)">
{{ row.status === '0' ? '停用' : '启用' }}
</el-button>
<el-button size="small" link type="danger" @click="removeOne(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="deptId" label="ID" width="80" />
<el-table-column prop="name" label="科室名称" min-width="200" />
<el-table-column prop="sort" label="排序" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'info'">{{ row.status === '0' ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="small" link :type="row.status === '0' ? 'warning' : 'success'" @click="toggleStatus(row)">
{{ row.status === '0' ? '停用' : '启用' }}
</el-button>
<el-button size="small" link type="danger" @click="removeOne(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
class="pager"
background
layout="total, prev, pager, next, sizes"
v-model:current-page="q.pageNum"
v-model:page-size="q.pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
:page-size="q.pageSize"
:current-page="q.pageNum"
@current-change="(p) => { q.pageNum = p; load() }"
@size-change="(s) => { q.pageSize = s; q.pageNum = 1; load() }"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
@@ -92,8 +90,8 @@ async function load() {
loading.value = true
try {
const r = await listDepartments({ ...q })
rows.value = r.rows || []
total.value = r.total || 0
rows.value = (r.data && r.data.rows) || r.rows || []
total.value = (r.data && r.data.total) || r.total || 0
} finally { loading.value = false }
}
@@ -164,9 +162,9 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-department { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.filter-bar { margin-bottom: 16px; }
.pager { margin-top: 16px; text-align: right; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+238
View File
@@ -0,0 +1,238 @@
<template>
<!--
角色: 🟦 后台 服务机构管理 (admin/executor-orgs)
说明: admin/Orgs.vue 拆出 executor 一支, 硬编码 orgType='executor'
接口: bizList('org', q) / bizAdd / bizUpdate
-->
<div class="page-card admin-executor-orgs">
<div class="breadcrumb">首页 / 服务机构管理</div>
<el-form inline :model="q" class="filter-form">
<el-form-item label="公司名称">
<el-input v-model="q.orgName" placeholder="输入公司名称" clearable style="width:200px" />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="q.taxNo" placeholder="输入税号" clearable style="width:220px" />
</el-form-item>
<el-form-item label="合作状态">
<el-select v-model="q.status" placeholder="请选择" clearable style="width:140px">
<el-option label="正常" value="正常" />
<el-option label="合作中" value="合作中" />
<el-option label="禁用" value="禁用" />
<el-option label="0正常" value="0" />
<el-option label="1禁用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="batch-bar">
<el-button type="primary" @click="openAdd">新增服务机构</el-button>
</div>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="orgName" label="公司名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="address" label="公司地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="taxNo" label="税号/统一社会信用代码" min-width="180" />
<el-table-column label="合作状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建日期" width="120" align="center">
<template #default="{ row }">{{ formatDate(row.createTime) }}</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="primary" @click="onEdit(row)">编辑</el-button>
<el-button link type="primary" @click="onPeople(row)">人员管理</el-button>
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
{{ isDisabled(row) ? '恢复' : '禁用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
:total="page.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑服务机构' : '新增服务机构'" width="640px">
<el-form :model="form" label-width="120px" :rules="rules" ref="formRef">
<el-form-item label="公司名称" prop="orgName">
<el-input v-model="form.orgName" />
</el-form-item>
<el-form-item label="公司地址">
<el-input v-model="form.address" />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="form.taxNo" />
</el-form-item>
<el-form-item label="联系人">
<el-input v-model="form.contactName" />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="form.contactPhone" maxlength="11" />
</el-form-item>
<el-form-item v-if="isEdit" label="状态">
<el-select v-model="form.status" style="width:160px">
<el-option label="0正常" value="0" />
<el-option label="合作中" value="合作中" />
<el-option label="1禁用" value="1" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="onSave">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { bizList, bizAdd, bizUpdate } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
const router = useRouter()
const q = reactive({ orgType: 'executor', orgName: '', taxNo: '', status: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
function statusTagType(s) {
if (s === '正常' || s === '0' || s === '合作中') return 'success'
if (s === '禁用' || s === '1') return 'danger'
return 'info'
}
function isDisabled(row) {
return row.status === '禁用' || row.status === '1'
}
function formatDate(d) {
if (!d) return ''
const str = String(d)
return str.length >= 10 ? str.slice(0, 10) : str
}
async function load() {
loading.value = true
try {
const { data } = await bizList('org', { ...q, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = (data && data.rows) || []
page.total = (data && data.total) || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() {
Object.assign(q, { orgType: 'executor', orgName: '', taxNo: '', status: '' })
page.pageNum = 1
load()
}
async function onToggleStatus(row) {
const disabled = isDisabled(row)
// executor 状态语义: 恢复 = '0', 禁用 = '1' (数字)
const newStatus = disabled ? '0' : '1'
const label = disabled ? '恢复' : '禁用'
if (!disabled) {
try {
await ElMessageBox.confirm(
`确定要${label}${row.orgName}」吗? ${label}后该企业下所有账号均不可登录`,
'确认',
{ type: 'warning' }
)
} catch { return }
}
try {
await bizUpdate('org', { orgId: row.orgId, status: newStatus })
ElMessage.success('已' + label)
load()
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
}
const dialogVisible = ref(false)
const isEdit = ref(false)
const saving = ref(false)
const formRef = ref(null)
const form = reactive({
orgId: null, orgType: 'executor', orgName: '', address: '', taxNo: '',
contactName: '', contactPhone: '', status: '0'
})
const rules = {
orgName: [{ required: true, message: '请输入公司名称', trigger: 'blur' }]
}
function openAdd() {
isEdit.value = false
Object.assign(form, {
orgId: null, orgType: 'executor', orgName: '', address: '', taxNo: '',
contactName: '', contactPhone: '', status: '0'
})
dialogVisible.value = true
}
function onEdit(row) {
isEdit.value = true
Object.assign(form, { ...row, orgType: 'executor' })
dialogVisible.value = true
}
function onPeople(row) {
router.push({ path: '/admin/executor-people', query: { orgId: row.orgId, orgName: row.orgName } })
}
function onView(row) {
ElMessageBox.alert(
`类型: 执行方\n` +
`名称: ${row.orgName}\n` +
`地址: ${row.address || '-'}\n` +
`税号: ${row.taxNo || '-'}\n` +
`联系人: ${row.contactName || '-'} (${row.contactPhone || '-'})\n` +
`状态: ${row.status}\n` +
`创建日期: ${formatDate(row.createTime)}`,
'服务机构详情',
{ confirmButtonText: '关闭' }
).catch(() => {})
}
async function onSave() {
await formRef.value.validate()
saving.value = true
try {
if (isEdit.value) {
await bizUpdate('org', { ...form })
ElMessage.success('修改成功')
} else {
await bizAdd('org', { ...form })
ElMessage.success('新增成功')
}
dialogVisible.value = false
load()
} catch (e) { ElMessage.error(e?.msg || '保存失败') }
finally { saving.value = false }
}
onMounted(load)
</script>
<style scoped>
.admin-executor-orgs { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+225
View File
@@ -0,0 +1,225 @@
<template>
<!--
角色: 🟦 后台 服务机构下人员 (admin/executor-people)
入口: org 表格行内人员管理按钮 (router.push orgId/orgName)
接口: bizList('person', q) / bizUpdate / bizAdd / executorImport / executorImportTemplate
-->
<div class="page-card admin-executor-people">
<div class="breadcrumb">
首页 / 服务机构管理 / 人员管理{{ orgName ? ' (' + orgName + ')' : '' }}
</div>
<el-form inline :model="q" class="filter-form">
<el-form-item label="姓名">
<el-input v-model="q.name" placeholder="姓名" clearable style="width:140px" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="q.phone" placeholder="手机号" clearable style="width:140px" />
</el-form-item>
<el-form-item label="企业名称">
<el-input v-model="q.orgName" placeholder="企业名称" clearable style="width:180px" />
</el-form-item>
<el-form-item label="部门">
<el-input v-model="q.department" placeholder="部门" clearable style="width:140px" />
</el-form-item>
<el-form-item label="职务">
<el-input v-model="q.position" placeholder="职务" clearable style="width:140px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="batch-bar">
<el-button type="primary" @click="goNew">新建人员</el-button>
<el-button @click="openImport">批量导入</el-button>
<span class="filter-tip">*批量导入模板与列表表项一致</span>
</div>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column prop="name" label="姓名" min-width="100" />
<el-table-column prop="phone" label="手机号" min-width="130" />
<el-table-column prop="orgName" label="企业名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" min-width="100" />
<el-table-column prop="position" label="职务" min-width="100" />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status || '正常' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
{{ isDisabled(row) ? '启用' : '禁用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
:total="page.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
<el-dialog v-model="importOpen" title="批量导入" width="520px" :close-on-click-modal="false">
<el-form label-width="100px">
<el-form-item label="选择文件">
<el-upload ref="uploadRef" :auto-upload="false" :limit="1" accept=".xls,.xlsx" :on-change="onFileChange" :on-remove="onFileRemove">
<el-button>选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="">
<el-button link type="primary" @click="downloadTemplate">批量导入模板下载</el-button>
</el-form-item>
<el-form-item><span class="filter-tip">*导入列: 姓名/手机号/所属公司/部门/职务/角色</span></el-form-item>
</el-form>
<template #footer>
<el-button @click="importOpen = false">取消</el-button>
<el-button type="primary" :loading="importing" @click="submitImport">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { bizList, bizUpdate, importPerson, downloadImportTemplate } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
const route = useRoute()
const router = useRouter()
const orgId = computed(() => route.query.orgId ? Number(route.query.orgId) : null)
const orgName = computed(() => route.query.orgName || '')
const q = reactive({ name: '', phone: '', orgName: '', department: '', position: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
function statusTagType(s) {
if (s === '禁用' || s === '1') return 'danger'
return 'success'
}
function isDisabled(row) {
return row.status === '禁用' || row.status === '1'
}
async function load() {
loading.value = true
try {
const params = {
...q,
orgType: 'executor',
...(orgId.value ? { orgId: orgId.value } : {}),
pageNum: page.pageNum,
pageSize: page.pageSize,
}
const { data } = await bizList('person', params)
rows.value = (data && data.rows) || []
page.total = (data && data.total) || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() {
Object.assign(q, { name: '', phone: '', orgName: '', department: '', position: '' })
page.pageNum = 1
load()
}
async function onToggleStatus(row) {
const disabled = isDisabled(row)
const newStatus = disabled ? '正常' : '禁用'
const label = disabled ? '启用' : '禁用'
try {
await ElMessageBox.confirm(
`确定要${label}${row.name}」吗?`,
'确认',
{ type: 'warning' }
)
} catch { return }
try {
await bizUpdate('person', { personId: row.personId, status: newStatus })
ElMessage.success('已' + label)
load()
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
}
// ========== 新建人员 ==========
function goNew() {
router.push({
path: '/admin/executor-people/new',
query: {
...(orgId.value ? { orgId: orgId.value } : {}),
...(orgName.value ? { orgName: orgName.value } : {})
}
})
}
// ========== 批量导入 ==========
const importOpen = ref(false)
const importing = ref(false)
const uploadRef = ref()
const importFile = ref(null)
function openImport() { importFile.value = null; importOpen.value = true }
function onFileChange(file) { importFile.value = file.raw }
function onFileRemove() { importFile.value = null }
async function downloadTemplate() {
try {
const res = await downloadImportTemplate('executor')
const blob = new Blob([res.data], { type: res.data.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '服务机构人员导入模板.xlsx'
a.click()
URL.revokeObjectURL(url)
} catch { ElMessage.error('模板下载失败') }
}
async function submitImport() {
if (!importFile.value) { ElMessage.warning('请先选择文件'); return }
importing.value = true
try {
const res = await importPerson('executor', importFile.value)
const payload = res.data || res
const ok = payload.ok ?? 0
const total = payload.total ?? 0
const failed = (payload.results || []).filter(r => !r.ok)
if (failed.length === 0) {
ElMessage.success(`导入成功 ${ok}/${total}`)
} else {
ElMessageBox.alert(
`成功 ${ok} 条, 失败 ${failed.length} 条:\n` +
failed.map(f => `${f.rowNo}${f.name || ''}: ${f.message}`).join('\n'),
'导入结果', { type: 'warning' }
)
}
importOpen.value = false
load()
} catch (e) {
ElMessage.error(e?.msg || '导入失败')
} finally {
importing.value = false
}
}
onMounted(load)
</script>
<style scoped>
.admin-executor-people { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.filter-tip { color: #909399; font-size: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+258
View File
@@ -0,0 +1,258 @@
<template>
<!--
专家详情独立页 (只读)
1:1 ExpertNew.vue 风格 (page-card + 面包屑 + form-card nested + 2 )
区别: 所有控件禁用, 仅展示
-->
<div class="page-card expert-detail">
<!-- 面包屑 -->
<div class="breadcrumb">
<a @click="goBack">专家管理</a> &gt; 专家详情
</div>
<!-- 表单卡片 -->
<el-card v-loading="loading" shadow="never" class="form-card nested">
<el-form :model="form" label-width="120px" disabled>
<!-- Row 1: 姓名 | 手机号 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="姓名">
<el-input v-model="form.name" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手机号">
<el-input v-model="form.phone" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 2: 工作单位 | 科室 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="工作单位">
<el-input v-model="form.workUnit" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="科室">
<el-input v-model="form.department" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 3: 职称 | 地区 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="职称">
<el-input v-model="form.title" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="地区">
<el-input v-model="form.region" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 4: 执业医师证书 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="执业医师证书">
<oss-image-uploader
v-model="form.practiceCertUrl"
dir="ry8080/expert/practice/"
placeholder="+"
readonly
/>
</el-form-item>
</el-col>
</el-row>
<!-- Row 5: 职称证明 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="职称证明">
<oss-image-uploader
v-model="form.titleCertUrl"
dir="ry8080/expert/title/"
placeholder="+"
readonly
/>
</el-form-item>
</el-col>
</el-row>
<!-- Row 6: 身份证件号码 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="身份证件号码">
<el-input v-model="form.idCard" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 7: 身份证件照片 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="身份证件照片">
<id-card-uploader
:front-url="form.idCardFrontUrl"
:back-url="form.idCardBackUrl"
dir="ry8080/expert/idcard/"
readonly
/>
</el-form-item>
</el-col>
</el-row>
<!-- Row 8: 银行卡号 | 银行名称 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="银行卡号">
<el-input v-model="form.bankCard" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="银行名称">
<el-input v-model="form.bankName" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 9: 开户行省份 | 开户行城市 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="开户行省份">
<el-input v-model="form.bankProvince" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开户行城市">
<el-input v-model="form.bankCity" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 10: 开户行地址 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="开户行地址">
<el-input v-model="form.bankAddress" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 操作按钮 -->
<div class="form-actions">
<el-button @click="goBack">返回</el-button>
</div>
</div>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { bizGet } from '@/api/public'
import OssImageUploader from '@/components/OssImageUploader.vue'
import IdCardUploader from '@/components/IdCardUploader.vue'
const router = useRouter()
const route = useRoute()
const expertId = route.params.id
const loading = ref(false)
const form = reactive({
expertId: null,
name: '',
phone: '',
workUnit: '',
department: '',
title: '',
region: '',
practiceCertUrl: '',
titleCertUrl: '',
idCard: '',
idCardFrontUrl: '',
idCardBackUrl: '',
bankCard: '',
bankName: '',
bankProvince: '',
bankCity: '',
bankAddress: '',
})
function goBack() {
router.push('/admin/experts')
}
async function loadDetail() {
if (!expertId) {
ElMessage.error('缺少专家ID')
goBack()
return
}
loading.value = true
try {
const res = await bizGet('expert', expertId)
if (res?.code === 200 && res.data) {
const d = res.data
Object.assign(form, {
expertId: d.expertId,
name: d.name || '',
phone: d.phone || '',
workUnit: d.workUnit || '',
department: d.department || '',
title: d.title || '',
region: d.region || '',
practiceCertUrl: d.practiceCertUrl || '',
titleCertUrl: d.titleCertUrl || '',
idCard: d.idCard || '',
idCardFrontUrl: d.idCardFrontUrl || '',
idCardBackUrl: d.idCardBackUrl || '',
bankCard: d.bankCard || '',
bankName: d.bankName || '',
bankProvince: d.bankProvince || '',
bankCity: d.bankCity || '',
bankAddress: d.bankAddress || '',
})
} else {
ElMessage.error(res?.msg || '加载失败')
goBack()
}
} catch (e) {
ElMessage.error(e?.msg || e?.message || '加载失败')
goBack()
} finally {
loading.value = false
}
}
onMounted(loadDetail)
</script>
<style scoped>
/* 1:1 抄 ExpertNew.vue */
.expert-detail { max-width: 1200px; }
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 16px; }
.breadcrumb a { color: var(--brand-primary); cursor: pointer; text-decoration: none; }
.breadcrumb a:hover { color: var(--brand-primary-deep, var(--brand-primary)); }
.form-card.nested { background: transparent; border: none; box-shadow: none; }
:deep(.form-card .el-card__body) { padding: 24px 28px; }
.form-actions { margin-top: 16px; display: flex; gap: 8px; }
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep, var(--brand-primary)); border-color: var(--brand-primary-deep, var(--brand-primary)); }
:deep(.el-button) { border-radius: 4px; }
:deep(.el-input__wrapper), :deep(.el-textarea__inner) { border-radius: 4px; }
/* 只读时禁用表单 input 鼠标样式, 保留视觉 */
:deep(.el-form.is-disabled .el-input__wrapper) { background-color: #f5f7fa; box-shadow: 0 0 0 1px #e4e7ed inset; }
</style>
+355
View File
@@ -0,0 +1,355 @@
<template>
<!--
1:1 sponsor/NewPerson.vue 风格
15 字段专家新建/编辑独立页
编辑/新建复用: 通过 route.params.id 区分
-->
<div class="page-card expert-new">
<!-- 面包屑 -->
<div class="breadcrumb">
<a @click="goBack">专家管理</a> &gt; {{ isEdit ? '编辑专家' : '新建专家' }}
</div>
<!-- 表单卡片 -->
<el-card v-loading="loadingDetail" shadow="never" class="form-card nested">
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
<!-- Row 1: 姓名 | 手机号 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入姓名" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 2: 工作单位 | 科室 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="工作单位" prop="workUnit">
<el-input v-model="form.workUnit" placeholder="请输入工作单位" maxlength="200" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="科室" prop="department">
<doctor-dept-select
v-model="form.department"
value-field="label"
placeholder="输入关键词搜索"
filterable
style="width:100%"
/>
</el-form-item>
</el-col>
</el-row>
<!-- Row 3: 职称 | 地区 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="职称" prop="title">
<doctor-title-select
v-model="form.title"
value-field="label"
placeholder="输入关键词搜索"
filterable
style="width:100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="地区">
<area-cascader
v-model="form.region"
format="string"
join-sep="/"
placeholder="请选择省/市/区"
style="width:100%"
/>
</el-form-item>
</el-col>
</el-row>
<!-- Row 4: 执业医师证书 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="执业医师证书">
<oss-image-uploader
v-model="form.practiceCertUrl"
dir="ry8080/expert/practice/"
placeholder="+"
/>
</el-form-item>
</el-col>
</el-row>
<!-- Row 5: 职称证明 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="职称证明">
<oss-image-uploader
v-model="form.titleCertUrl"
dir="ry8080/expert/title/"
placeholder="+"
/>
</el-form-item>
</el-col>
</el-row>
<!-- Row 6: 身份证件号码 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="身份证件号码">
<el-input v-model="form.idCard" placeholder="请输入身份证件号码" maxlength="18" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 7: 身份证件照片 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="身份证件照片">
<id-card-uploader
v-model:front-url="form.idCardFrontUrl"
v-model:back-url="form.idCardBackUrl"
dir="ry8080/expert/idcard/"
/>
</el-form-item>
</el-col>
</el-row>
<!-- Row 8: 银行卡号 | 银行名称 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="银行卡号">
<el-input v-model="form.bankCard" placeholder="请输入银行卡号" maxlength="19" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="银行名称">
<el-input v-model="form.bankName" placeholder="如: 中国工商银行" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 9: 开户行省份 | 开户行城市 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="开户行省份">
<el-input v-model="form.bankProvince" placeholder="如: 北京市" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开户行城市">
<el-input v-model="form.bankCity" placeholder="如: 北京市" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 10: 开户行地址 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="开户行地址">
<el-input v-model="form.bankAddress" placeholder="请输入开户行详细地址" maxlength="200" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 操作按钮 -->
<div class="form-actions">
<el-button type="primary" :loading="saving" @click="onSave">保存</el-button>
<el-button @click="confirmCancel">取消</el-button>
</div>
</div>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { bizAdd, bizUpdate, bizGet } from '@/api/public'
import { useUserStore } from '@/store/user'
import DoctorTitleSelect from '@/components/DoctorTitleSelect.vue'
import DoctorDeptSelect from '@/components/DoctorDeptSelect.vue'
import AreaCascader from '@/components/AreaCascader.vue'
import IdCardUploader from '@/components/IdCardUploader.vue'
import OssImageUploader from '@/components/OssImageUploader.vue'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const formRef = ref(null)
const saving = ref(false)
const loadingDetail = ref(false)
const isEdit = !!route.params.id
const expertId = route.params.id || null
const form = reactive({
expertId: null,
name: '',
phone: '',
workUnit: '',
department: '',
title: '',
region: '',
practiceCertUrl: '',
titleCertUrl: '',
idCard: '',
idCardFrontUrl: '',
idCardBackUrl: '',
bankCard: '',
bankName: '',
bankProvince: '',
bankCity: '',
bankAddress: '',
})
// 5 必填字段 + 手机号正则 (项目标准)
const rules = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[0-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' },
],
workUnit: [{ required: true, message: '请输入工作单位', trigger: 'blur' }],
department:[{ required: true, message: '请选择科室', trigger: 'change' }],
title: [{ required: true, message: '请选择职称', trigger: 'change' }],
}
function goBack() {
router.push('/admin/experts')
}
async function loadDetail() {
if (!expertId) return
loadingDetail.value = true
try {
const res = await bizGet('expert', expertId)
if (res?.code === 200 && res.data) {
const d = res.data
Object.assign(form, {
expertId: d.expertId,
name: d.name || '',
phone: d.phone || '',
workUnit: d.workUnit || '',
department: d.department || '',
title: d.title || '',
region: d.region || '',
practiceCertUrl: d.practiceCertUrl || '',
titleCertUrl: d.titleCertUrl || '',
idCard: d.idCard || '',
idCardFrontUrl: d.idCardFrontUrl || '',
idCardBackUrl: d.idCardBackUrl || '',
bankCard: d.bankCard || '',
bankName: d.bankName || '',
bankProvince: d.bankProvince || '',
bankCity: d.bankCity || '',
bankAddress: d.bankAddress || '',
})
} else {
ElMessage.error(res?.msg || '加载失败')
goBack()
}
} catch (e) {
ElMessage.error(e?.msg || e?.message || '加载失败')
goBack()
} finally {
loadingDetail.value = false
}
}
function confirmCancel() {
// 检查表单是否有内容 (新建时只要 form.name 即可)
const hasContent = form.name || form.phone || form.workUnit || form.department || form.title
if (!isEdit && !hasContent) { goBack(); return }
ElMessageBox.confirm('确定取消?未保存的内容将丢失', '提示', { type: 'warning' })
.then(() => goBack())
.catch(() => {})
}
// 业务"重复/已存在"提示用 warning 黄 (memory 规则)
function isDuplicateError(msg) {
return /已存在|重复|已注册|duplicate|uk_expert_phone/i.test(msg || '')
}
async function onSave() {
try {
await formRef.value.validate()
} catch {
ElMessage.warning('请填写必填项')
return
}
saving.value = true
try {
const payload = { ...form }
if (isEdit) {
// 编辑: 不改 expertId 主键, 也不重置 auditStatus
delete payload.expertId
} else {
// 新建: admin 创建, 自动通过审核 (BizAuditStatusEnum.APPROVED='2'), 状态正常
payload.auditStatus = '2'
payload.auditBy = userStore.user?.userName || userStore.user?.nickName || 'admin'
payload.auditTime = new Date().toISOString().slice(0, 19).replace('T', ' ')
payload.status = 'Y'
}
const res = isEdit
? await bizUpdate('expert', payload)
: await bizAdd('expert', payload)
if (res?.code === 200) {
if (!isEdit && res.data) {
// 新建成功: 后端同时创建 sys_user, 提示账号信息
const u = res.data
ElMessageBox.alert(
`专家账号已创建\n账号: ${u.userName || form.phone}\n初始密码: ${u.password || form.phone}\n请记录后告知使用人 (密码仅本次显示)`,
'创建成功',
{ type: 'success', confirmButtonText: '我已记录, 返回' }
).then(() => goBack())
.catch(() => goBack())
} else {
ElMessage.success(isEdit ? '修改成功' : '新建成功')
goBack()
}
} else {
const msg = res?.msg || '保存失败'
if (isDuplicateError(msg)) ElMessage.warning('该手机号已注册,请更换')
else ElMessage.error(msg)
}
} catch (e) {
const msg = e?.msg || e?.message || '保存失败'
if (isDuplicateError(msg)) ElMessage.warning('该手机号已注册,请更换')
else ElMessage.error(msg)
} finally {
saving.value = false
}
}
onMounted(() => {
if (isEdit) loadDetail()
})
</script>
<style scoped>
/* 1:1 抄 sponsor/NewPerson.vue */
.expert-new { max-width: 1200px; }
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 16px; }
.breadcrumb a { color: var(--brand-primary); cursor: pointer; text-decoration: none; }
.breadcrumb a:hover { color: var(--brand-primary-deep, var(--brand-primary)); }
.form-card.nested { background: transparent; border: none; box-shadow: none; }
:deep(.form-card .el-card__body) { padding: 24px 28px; }
.form-actions { margin-top: 16px; display: flex; gap: 8px; }
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep, var(--brand-primary)); border-color: var(--brand-primary-deep, var(--brand-primary)); }
:deep(.el-button) { border-radius: 4px; }
:deep(.el-input__wrapper), :deep(.el-textarea__inner) { border-radius: 4px; }
</style>
+253 -245
View File
@@ -4,96 +4,95 @@
字段映射: manager/Experts.vue 11 + 操作列, 容器/风格改用 admin 通用 page-card + desc
接口: bizList('expert', q) / bizAdd / bizUpdate
-->
<div class="admin-experts">
<div class="page-card">
<div class="desc">维护全平台评审专家档案, 管理其执业证书职称证明与账号状态</div>
<div class="page-card admin-experts">
<div class="breadcrumb">首页 / 专家管理</div>
<el-form inline :model="q" class="filter-bar">
<el-form-item label="姓名">
<el-input v-model="q.name" placeholder="输入姓名" clearable style="width:160px" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="q.phone" placeholder="输入手机号" clearable maxlength="11" style="width:160px" />
</el-form-item>
<el-form-item label="工作单位">
<el-input v-model="q.workUnit" placeholder="输入工作单位" clearable style="width:200px" />
</el-form-item>
<el-form-item label="审核状态">
<el-select v-model="q.auditStatus" placeholder="请选择" clearable style="width:140px">
<el-option label="待审核" value="0" />
<el-option label="通过" value="1" />
<el-option label="退回" value="2" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部状态" clearable style="width:120px">
<el-option label="启用" value="1" />
<el-option label="禁用" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="姓名"><el-input v-model="q.name" placeholder="输入姓名" clearable style="width:160px" /></el-form-item>
<el-form-item label="手机号"><el-input v-model="q.phone" placeholder="输入手机号" clearable maxlength="11" style="width:160px" /></el-form-item>
<el-form-item label="工作单位"><el-input v-model="q.workUnit" placeholder="输入工作单位" clearable style="width:200px" /></el-form-item>
<el-form-item label="审核状态">
<el-select v-model="q.auditStatus" placeholder="请选择" clearable style="width:140px">
<el-option label="未提交" value="0" />
<el-option label="待审核" value="1" />
<el-option label="通过" value="2" />
<el-option label="拒绝" value="3" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部状态" clearable style="width:120px">
<el-option label="正常" value="Y" />
<el-option label="禁用" value="N" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 批量按钮区 ( People.vue batch-bar 风格一致) ========== -->
<div class="batch-bar">
<el-button type="primary" @click="goNew">新建专家</el-button>
<el-button @click="openImport">批量导入</el-button>
<el-button :disabled="!selection.length" @click="onBatchAudit">批量审核</el-button>
<el-button @click="openImportPracticeCert">批量导入执业证书</el-button>
<el-button @click="openImportTitleCert">批量导入职称证明</el-button>
</div>
<div class="page-card">
<div class="batch-bar">
<el-button type="primary" @click="openAdd">新建专家</el-button>
<el-button @click="openImport">批量导入</el-button>
<el-button :disabled="!selection.length" @click="onBatchAudit">批量审核</el-button>
<el-button @click="openImportPracticeCert">批量导入执业证书</el-button>
<el-button @click="openImportTitleCert">批量导入职称证明</el-button>
</div>
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSel">
<el-table-column type="selection" width="44" />
<el-table-column prop="name" label="姓名" width="100" fixed />
<el-table-column prop="phone" label="手机号" width="130" />
<el-table-column prop="workUnit" label="工作单位" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="科室" width="100" />
<el-table-column prop="title" label="职称" width="100" />
<el-table-column label="执业证书" width="170">
<template #default="{ row }">
<template v-if="row.practiceCertUrl">
<el-button link type="primary" @click="onViewCert(row.practiceCertUrl)">查看</el-button>
<el-button link type="primary" @click="onDownloadCert(row.practiceCertUrl, row.phone)">下载</el-button>
</template>
<span v-else style="color:#c0c4cc">-</span>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSel">
<el-table-column type="selection" width="44" />
<el-table-column prop="name" label="姓名" width="100" fixed />
<el-table-column prop="phone" label="手机号" width="130" />
<el-table-column prop="workUnit" label="工作单位" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="科室" width="100" />
<el-table-column prop="title" label="职称" width="100" />
<el-table-column label="执业证书" width="170">
<template #default="{ row }">
<template v-if="row.practiceCertUrl">
<el-button link type="primary" @click="onPreviewCert(row.practiceCertUrl)">查看</el-button>
<el-button link type="primary" @click="onDownloadCert(row.practiceCertUrl, row.phone)">下载</el-button>
</template>
</el-table-column>
<el-table-column label="职称证明" width="170">
<template #default="{ row }">
<template v-if="row.titleCertUrl">
<el-button link type="primary" @click="onViewCert(row.titleCertUrl)">查看</el-button>
<el-button link type="primary" @click="onDownloadCert(row.titleCertUrl, row.phone)">下载</el-button>
</template>
<span v-else style="color:#c0c4cc">-</span>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="职称证明" width="170">
<template #default="{ row }">
<template v-if="row.titleCertUrl">
<el-button link type="primary" @click="onPreviewCert(row.titleCertUrl)">查看</el-button>
<el-button link type="primary" @click="onDownloadCert(row.titleCertUrl, row.phone)">下载</el-button>
</template>
</el-table-column>
<el-table-column label="审核状态" width="100" align="center">
<template #default="{ row }">
<span :class="['stage-tag', auditStatusClass(row.auditStatus)]">{{ auditStatusText(row.auditStatus) }}</span>
</template>
</el-table-column>
<el-table-column prop="auditTime" label="审批时间" width="160" align="center">
<template #default="{ row }">{{ fmtTime(row.auditTime) }}</template>
</el-table-column>
<el-table-column prop="auditBy" label="审批人" width="120" align="center" />
<el-table-column label="操作" width="260" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onAudit(row)">审核</el-button>
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link :type="row.status === '1' ? 'danger' : 'success'" @click="row.status === '1' ? onDisable(row) : onEnable(row)">
{{ row.status === '1' ? '禁用' : '恢复' }}
</el-button>
<el-button link type="info" v-if="row.auditStatus === '2'" @click="onViewAuditOpinion(row)">已退回</el-button>
</template>
</el-table-column>
</el-table>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="审核状态" width="100" align="center">
<template #default="{ row }">
<audit-status-tag :status="row.auditStatus" />
</template>
</el-table-column>
<el-table-column label="启用状态" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.status === 'N' ? 'danger' : 'success'">
{{ row.status === 'N' ? '禁用' : '正常' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="260" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onAudit(row)">审核</el-button>
<el-button link type="primary" @click="goEdit(row)">修改</el-button>
<el-button link type="primary" @click="goView(row)">查看</el-button>
<el-button link :type="row.status === 'N' ? 'success' : 'danger'" @click="row.status === 'N' ? onEnable(row) : onDisable(row)">
{{ row.status === 'N' ? '恢复' : '禁用' }}
</el-button>
<el-button link type="info" v-if="row.auditStatus === '3'" @click="onViewAuditOpinion(row)">已拒绝</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
@@ -102,35 +101,11 @@
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
style="margin-top: 12px; text-align: right;"
/>
</div>
<!-- 新建/编辑专家 -->
<el-dialog v-model="editOpen" :title="editForm.expertId ? '编辑专家' : '新建专家'" width="640px">
<el-form :model="editForm" label-width="120px" :rules="editRules" ref="editFormRef">
<el-form-item label="姓名" prop="name"><el-input v-model="editForm.name" /></el-form-item>
<el-form-item label="手机号" prop="phone"><el-input v-model="editForm.phone" maxlength="11" /></el-form-item>
<el-form-item label="工作单位" prop="workUnit"><el-input v-model="editForm.workUnit" /></el-form-item>
<el-form-item label="科室" prop="department"><el-input v-model="editForm.department" placeholder="如: 呼吸科" /></el-form-item>
<el-form-item label="职称" prop="title">
<el-select v-model="editForm.title" placeholder="请选择" style="width:100%">
<el-option label="住院医师" value="住院医师" />
<el-option label="主治医师" value="主治医师" />
<el-option label="副主任医师" value="副主任医师" />
<el-option label="主任医师" value="主任医师" />
<el-option label="教授" value="教授" />
<el-option label="副教授" value="副教授" />
</el-select>
</el-form-item>
<el-form-item label="执业证书 URL"><el-input v-model="editForm.practiceCertUrl" /></el-form-item>
<el-form-item label="职称证明 URL"><el-input v-model="editForm.titleCertUrl" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="editOpen=false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="submitEdit">确定</el-button>
</template>
</el-dialog>
<!-- 新建/编辑专家: 跳转独立页 /admin/experts/new /experts/edit/:id -->
<!-- (旧的 edit dialog 已删) -->
<!-- 审核 -->
<el-dialog v-model="auditOpen" title="审核" width="520px">
@@ -138,8 +113,8 @@
<el-form :model="auditForm" label-width="100px">
<el-form-item label="审核结果">
<el-radio-group v-model="auditForm.result">
<el-radio value="1">通过</el-radio>
<el-radio value="2">退回</el-radio>
<el-radio value="2">通过</el-radio>
<el-radio value="3">拒绝</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见">
@@ -152,27 +127,33 @@
</template>
</el-dialog>
<!-- 批量导入专家 -->
<el-dialog v-model="importOpen" title="批量导入专家" width="520px">
<div style="margin-bottom:12px;font-size:13px">
<el-link type="primary" @click="onDownloadTpl">批量导入模板下载</el-link>
<!-- 批量导入专家 (RuoYi 用户导入风格) -->
<el-dialog v-model="importOpen" title="批量导入专家" width="400px" append-to-body>
<el-upload
ref="importUploadRef"
:limit="1"
accept=".xlsx, .xls"
:headers="importHeaders"
:action="importUrl"
:disabled="importing"
:on-progress="onImportProgress"
:on-success="onImportSuccess"
:auto-upload="false"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处, <em>点击上传</em></div>
<div slot="tip" class="el-upload__tip el-upload__tip--inline">
<el-checkbox v-model="importUpdateSupport" :disabled="importing" /> 是否更新已存在的专家
<el-link style="font-size:12px;vertical-align:baseline;margin-left:8px" type="primary" :underline="false" @click.stop="onDownloadTpl">下载模板</el-link>
</div>
</el-upload>
<!-- 导入结果反馈 (RuoYi 风格: 内联在 dialog ) -->
<div v-if="importResultMsg" class="upload-result" v-html="importResultMsg" />
<div slot="footer" class="dialog-footer">
<el-button @click="importOpen=false"> </el-button>
<el-button type="primary" :loading="importing" @click="submitImport"> </el-button>
</div>
<p style="color:#606266;margin-bottom:8px">*批量导入模板与列表表项一致</p>
<el-form label-width="100px">
<el-form-item label="审核结果">
<el-radio-group v-model="importAuditResult">
<el-radio value="1">通过</el-radio>
<el-radio value="2">退回</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="选择文件">
<el-input v-model="importFileUrl" placeholder="上传文件 URL 或选择文件" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="importOpen=false">取消</el-button>
<el-button type="primary" :loading="importing" @click="submitImport">确定</el-button>
</template>
</el-dialog>
<!-- 批量导入执业证书 -->
@@ -195,34 +176,40 @@
</template>
</el-dialog>
<!-- 详情查看 -->
<el-dialog v-model="viewOpen" title="专家详情" width="640px">
<el-descriptions :column="2" border v-if="currentRow">
<el-descriptions-item label="姓名">{{ currentRow.name }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ currentRow.phone }}</el-descriptions-item>
<el-descriptions-item label="工作单位">{{ currentRow.workUnit }}</el-descriptions-item>
<el-descriptions-item label="科室">{{ currentRow.department }}</el-descriptions-item>
<el-descriptions-item label="职称">{{ currentRow.title }}</el-descriptions-item>
<el-descriptions-item label="审核状态">{{ auditStatusText(currentRow.auditStatus) }}</el-descriptions-item>
<el-descriptions-item label="执业证书">
<el-link v-if="currentRow.practiceCertUrl" type="primary" @click="onViewCert(currentRow.practiceCertUrl)">查看证书</el-link>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="职称证明">
<el-link v-if="currentRow.titleCertUrl" type="primary" @click="onViewCert(currentRow.titleCertUrl)">查看证书</el-link>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="审批时间">{{ fmtTime(currentRow.auditTime) }}</el-descriptions-item>
<el-descriptions-item label="审批人">{{ currentRow.auditBy }}</el-descriptions-item>
</el-descriptions>
<!-- 详情查看: 已改跳独立页 /admin/experts/view/:id, 这里删除 dialog -->
<!-- 证书/证明 图片预览 dialog -->
<el-dialog v-model="previewOpen" title="证书预览" width="640px" :show-close="true" destroy-on-close>
<div class="cert-preview">
<el-image
:src="previewUrl"
fit="contain"
:preview-src-list="[previewUrl]"
style="width:100%;max-height:70vh"
loading="lazy"
>
<template #error>
<div class="cert-preview-error">图片加载失败, <a @click="onDownloadCert(previewUrl)">下载</a>查看</div>
</template>
</el-image>
</div>
<template #footer>
<el-button @click="onDownloadCert(previewUrl)">下载</el-button>
<el-button type="primary" @click="previewOpen=false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { bizList, bizAdd, bizUpdate } from '@/api/public'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import request from '@/utils/request'
import { bizList, bizUpdate, downloadExpertTemplate } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
import AuditStatusTag from '@/components/AuditStatusTag.vue'
const router = useRouter()
const q = reactive({ name: '', phone: '', workUnit: '', auditStatus: '', status: '' })
const rows = ref([])
@@ -231,12 +218,23 @@ const loading = ref(false)
const submitting = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
// 跳独立页 (新建/编辑)
function goNew() {
router.push('/admin/experts/new')
}
function goEdit(row) {
router.push('/admin/experts/edit/' + row.expertId)
}
function goView(row) {
router.push('/admin/experts/view/' + row.expertId)
}
async function load() {
loading.value = true
try {
const { data } = await bizList('expert', { ...q, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = data?.rows || []
page.total = data?.total || 0
rows.value = (data && data.rows) || []
page.total = (data && data.total) || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
@@ -247,16 +245,7 @@ function reset() {
}
function onSel(rs) { selection.value = rs }
function auditStatusText(s) {
if (s === '1') return '通过'
if (s === '2') return '退回'
return '待审核'
}
function auditStatusClass(s) {
if (s === '1') return 'stage-done'
if (s === '2') return 'stage-reject'
return 'stage-reviewing'
}
// auditStatusText / auditStatusClass 已删除: 改用 AuditStatusTag 组件
function fmtTime(d) {
if (!d) return ''
const dt = new Date(d)
@@ -264,62 +253,19 @@ function fmtTime(d) {
return `${dt.getFullYear()}.${pad(dt.getMonth() + 1)}.${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`
}
// ===== 新建/编辑 =====
const editOpen = ref(false)
const editFormRef = ref(null)
const editForm = reactive({
expertId: '', name: '', phone: '', workUnit: '', department: '', title: '',
practiceCertUrl: '', titleCertUrl: ''
})
const editRules = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
workUnit: [{ required: true, message: '请输入工作单位', trigger: 'blur' }],
department: [{ required: true, message: '请输入科室', trigger: 'blur' }],
title: [{ required: true, message: '请选择职称', trigger: 'change' }]
}
function openAdd() {
Object.assign(editForm, { expertId: '', name: '', phone: '', workUnit: '', department: '', title: '', practiceCertUrl: '', titleCertUrl: '' })
editOpen.value = true
}
function onEdit(row) {
Object.assign(editForm, {
expertId: row.expertId, name: row.name, phone: row.phone, workUnit: row.workUnit,
department: row.department, title: row.title,
practiceCertUrl: row.practiceCertUrl || '', titleCertUrl: row.titleCertUrl || ''
})
editOpen.value = true
}
async function submitEdit() {
await editFormRef.value.validate()
submitting.value = true
try {
if (editForm.expertId) {
await bizUpdate('expert', { ...editForm })
ElMessage.success('修改成功')
} else {
await bizAdd('expert', {
...editForm,
auditStatus: '1', auditBy: 'admin',
auditTime: new Date().toISOString().slice(0, 19).replace('T', ' '),
status: '1'
})
ElMessage.success('新建成功')
}
editOpen.value = false
load()
} catch (e) { ElMessage.error(e?.msg || '保存失败') }
finally { submitting.value = false }
}
// auditStatusText / auditStatusClass 已删除: 改用 AuditStatusTag 组件
// ===== 新建/编辑: 跳独立页 (本页面不再保留 dialog) =====
// ===== 审核 =====
const auditOpen = ref(false)
const auditForm = reactive({ expertId: '', name: '', phone: '', result: '1', opinion: '' })
// result: '2'=通过, '3'=拒绝 (BizAuditStatusEnum: 0未提交 1待审核 2通过 3拒绝)
const auditForm = reactive({ expertId: '', name: '', phone: '', result: '2', opinion: '' })
function onAudit(row) {
auditForm.expertId = row.expertId
auditForm.name = row.name
auditForm.phone = row.phone
auditForm.result = '1'
auditForm.result = '2'
auditForm.opinion = ''
auditOpen.value = true
}
@@ -333,7 +279,7 @@ async function submitAudit() {
auditBy: 'admin',
auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ')
})
ElMessage.success(auditForm.result === '1' ? '审核通过' : '已退回')
ElMessage.success(auditForm.result === '2' ? '审核通过' : '已拒绝')
auditOpen.value = false
load()
} catch { ElMessage.error('审核失败') }
@@ -351,7 +297,7 @@ async function onBatchAudit() {
for (const r of selection.value) {
try {
await bizUpdate('expert', {
expertId: r.expertId, auditStatus: '1', auditOpinion: '批量通过',
expertId: r.expertId, auditStatus: '2', auditOpinion: '批量通过',
auditBy: 'admin', auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ')
})
ok++
@@ -363,34 +309,39 @@ async function onBatchAudit() {
load()
}
// ===== 启用/禁用 =====
// ===== 启用/禁用: 专用 endpoint (PUT /business/expert/{expertId}/status) =====
async function onDisable(row) {
try {
await bizUpdate('expert', { expertId: row.expertId, status: '0' })
await request.put(`/business/expert/${row.expertId}/status`, null, { params: { status: 'N' } })
ElMessage.success('已禁用'); load()
} catch { ElMessage.error('禁用失败') }
} catch (e) { ElMessage.error(e?.msg || '禁用失败') }
}
async function onEnable(row) {
try {
await bizUpdate('expert', { expertId: row.expertId, status: '1' })
await request.put(`/business/expert/${row.expertId}/status`, null, { params: { status: 'Y' } })
ElMessage.success('已恢复'); load()
} catch { ElMessage.error('恢复失败') }
} catch (e) { ElMessage.error(e?.msg || '恢复失败') }
}
// ===== 详情 / 退回意见 =====
const viewOpen = ref(false)
const currentRow = ref(null)
function onView(row) { currentRow.value = row; viewOpen.value = true }
// 详情查看已改跳独立页 /admin/experts/view/:id (见 goView)
function onViewAuditOpinion(row) {
ElMessageBox.alert(
`退回时间: ${fmtTime(row.auditTime) || '-'}\n审批人: ${row.auditBy || '-'}\n退回意见: ${row.auditOpinion || '(无)'}`,
`专家 ${row.name} 审核已退回`,
`专家 ${row.name} 审核已拒绝`,
{ type: 'warning', confirmButtonText: '知道了' }
).catch(() => {})
}
// ===== 证书查看/下载 =====
function onViewCert(url) { if (url) window.open(url, '_blank') }
// 图片预览: dialog 弹窗直接显示 (用 el-image 自带 preview-src-list 支持点击放大)
const previewOpen = ref(false)
const previewUrl = ref('')
function onPreviewCert(url) {
if (!url) return
previewUrl.value = url
previewOpen.value = true
}
function onDownloadCert(url, phone) {
if (!url) return
const a = document.createElement('a')
@@ -399,22 +350,65 @@ function onDownloadCert(url, phone) {
document.body.appendChild(a); a.click(); document.body.removeChild(a)
}
// ===== 批量导入 dialog =====
// ===== 批量导入 dialog (RuoYi 用户导入风格) =====
const importOpen = ref(false)
const importAuditResult = ref('1')
const importFileUrl = ref('')
const importing = ref(false)
const importUploadRef = ref(null)
const importUpdateSupport = ref(false)
const importResultMsg = ref('')
// importHeaders 与 importUrl 用于 el-upload auto-upload=false 时的手动提交
const importHeaders = computed(() => ({
Authorization: 'Bearer ' + (localStorage.getItem('ry_token') || '')
}))
const importUrl = computed(() => '/dev-api/business/expert/importData?updateSupport=' + importUpdateSupport.value)
function openImport() {
importAuditResult.value = '1'; importFileUrl.value = ''; importOpen.value = true
importResultMsg.value = ''
importUpdateSupport.value = false
// 清空 el-upload 已选文件 (手动触发, 否则下次打开还显示上次的)
if (importUploadRef.value) {
importUploadRef.value.clearFiles()
}
importOpen.value = true
}
function onDownloadTpl() { ElMessage.info('批量导入模板下载 — 待后端提供静态文件') }
async function submitImport() {
if (!importFileUrl.value) return ElMessage.warning('请选择文件')
function onImportProgress() {
importing.value = true
await new Promise(r => setTimeout(r, 500))
}
function onImportSuccess(res, file) {
importing.value = false
ElMessage.info(`已提交批量导入任务 — 待后端实现`)
importOpen.value = false
// 后端返回 {code:200, msg:"..."}
importResultMsg.value = res?.msg || '导入成功'
if (res?.code === 200) {
ElMessage.success('导入完成')
load()
} else {
ElMessage.error(res?.msg || '导入失败')
}
}
// 与 SponsorPeople.vue downloadTemplate 同款: 走 RuoYi 极简模式 (blob + a.download + 延迟释放)
// 注意: a 必须挂到 body 上 (Firefox 要求), URL.revokeObjectURL 要延后 (避免下载未启动就释放)
function onDownloadTpl() {
downloadExpertTemplate().then((res) => {
const blob = new Blob([res.data], { type: res.data.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '专家导入模板.xlsx'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
setTimeout(() => URL.revokeObjectURL(url), 1000)
}).catch((e) => {
ElMessage.error(e?.message || '模板下载失败')
})
}
function submitImport() {
if (!importUploadRef.value) return ElMessage.warning('上传组件未就绪')
importUploadRef.value.submit()
}
// 批量导入执业证书
@@ -447,14 +441,28 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-experts { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.filter-bar { margin-bottom: 0; }
.batch-bar { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; flex-wrap: wrap; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
.stage-tag { display: inline-block; padding: 2px 10px; border-radius: 3px; font-size: 12px; border: 1px solid; }
.stage-done { color: #67c23a; border-color: #c2e7b0; background: #f0f9eb; }
.stage-reviewing { color: #e6a23c; border-color: #f5dab1; background: #fdf6ec; }
.stage-reject { color: #f56c6c; border-color: #fbc4c4; background: #fef0f0; }
/* 证书预览 dialog */
.cert-preview { display: flex; justify-content: center; align-items: center; min-height: 200px; background: #f5f7fa; border-radius: 4px; padding: 8px; }
.cert-preview-error { color: #909399; padding: 40px 0; }
/* 批量导入 dialog (RuoYi 风格) */
.upload-result {
margin-top: 12px; padding: 10px 12px;
background: #f5f7fa; border-radius: 4px;
font-size: 13px; line-height: 1.8;
max-height: 200px; overflow-y: auto;
}
:deep(.el-upload__tip--inline) {
display: flex; align-items: center; justify-content: center;
gap: 8px; margin-top: 8px;
}
/* stage-* CSS 已删除: 改用 AuditStatusTag 组件 (el-tag 自带颜色) */
</style>
+39 -41
View File
@@ -1,45 +1,43 @@
<template>
<div class="admin-meetings">
<div class="page-card">
<div class="desc">查看系统中所有会议 (跨角色全量)</div>
<div class="page-card admin-meetings">
<div class="breadcrumb">首页 / 会议全览</div>
<el-form inline :model="q" class="filter-bar">
<el-form-item label="会议主题:">
<el-input v-model="q.title" placeholder="输入会议主题" clearable />
</el-form-item>
<el-form-item label="状态:">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option v-for="s in statusOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="会议主题"><el-input v-model="q.title" placeholder="输入会议主题" clearable /></el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option v-for="s in statusOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="title" label="会议主题" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="STATUS_TAG[row.status] || 'info'">{{ STATUS_LABEL[row.status] || row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="startTime" label="开始时间" width="170" />
<el-table-column prop="endTime" label="结束时间" width="170" />
<el-table-column prop="createTime" label="创建时间" width="170" />
</el-table>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="title" label="会议主题" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="STATUS_TAG[row.status] || 'info'">{{ STATUS_LABEL[row.status] || row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="startTime" label="开始时间" width="170" />
<el-table-column prop="endTime" label="结束时间" width="170" />
<el-table-column prop="createTime" label="创建时间" width="170" />
</el-table>
<div class="pager">
<el-pagination
class="pager"
background
layout="total, prev, pager, next, sizes"
v-model:current-page="q.pageNum"
v-model:page-size="q.pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
:page-size="q.pageSize"
:current-page="q.pageNum"
@current-change="(p) => { q.pageNum = p; load() }"
@size-change="(s) => { q.pageSize = s; q.pageNum = 1; load() }"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
</div>
@@ -66,8 +64,8 @@ async function load() {
loading.value = true
try {
const r = await bizList('meeting', { ...q })
rows.value = r.rows || r.data?.rows || []
total.value = r.total || r.data?.total || 0
rows.value = r.rows || (r.data && r.data.rows) || []
total.value = r.total || (r.data && r.data.total) || 0
} finally {
loading.value = false
}
@@ -82,9 +80,9 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-meetings { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.filter-bar { margin-bottom: 16px; }
.pager { margin-top: 16px; text-align: right; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+75 -77
View File
@@ -7,77 +7,78 @@
biz_service_org 整体删除
接口: bizList('org', q) / bizUpdate
-->
<div class="admin-orgs">
<div class="page-card">
<div class="desc">管理公司档案 (赞助方 + 执行方共用一张表, 通过 org_type 区分)</div>
<div class="page-card admin-orgs">
<div class="breadcrumb">首页 / 公司管理</div>
<el-form inline :model="q" class="filter-bar">
<el-form-item label="公司类型">
<el-select v-model="q.orgType" placeholder="全部" clearable style="width:140px">
<el-option label="赞助方" value="sponsor" />
<el-option label="执行方" value="executor" />
</el-select>
</el-form-item>
<el-form-item label="公司名称">
<el-input v-model="q.orgName" placeholder="输入公司名称" clearable style="width:200px" />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="q.taxNo" placeholder="输入税号" clearable style="width:220px" />
</el-form-item>
<el-form-item label="合作状态">
<el-select v-model="q.status" placeholder="请选择" clearable style="width:140px">
<el-option label="正常" value="正常" />
<el-option label="合作中" value="合作中" />
<el-option label="禁用" value="禁用" />
<el-option label="0正常" value="0" />
<el-option label="1禁用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="公司类型">
<el-select v-model="q.orgType" placeholder="全部" clearable style="width:140px">
<el-option label="赞助方" value="sponsor" />
<el-option label="执行方" value="executor" />
</el-select>
</el-form-item>
<el-form-item label="公司名称">
<el-input v-model="q.orgName" placeholder="输入公司名称" clearable style="width:200px" />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="q.taxNo" placeholder="输入税号" clearable style="width:220px" />
</el-form-item>
<el-form-item label="合作状态">
<el-select v-model="q.status" placeholder="请选择" clearable style="width:140px">
<el-option label="正常" value="正常" />
<el-option label="合作中" value="合作中" />
<el-option label="禁用" value="禁用" />
<el-option label="0正常" value="0" />
<el-option label="1禁用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 工具栏 ( People.vue batch-bar 风格一致) ========== -->
<div class="batch-bar">
<el-button type="primary" @click="openAdd">新增公司</el-button>
</div>
<div class="page-card">
<div class="toolbar">
<el-button type="primary" @click="openAdd">新增公司</el-button>
</div>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column label="公司类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.orgType === 'sponsor' ? 'success' : 'primary'" disable-transitions>
{{ row.orgType === 'sponsor' ? '赞助方' : '执行方' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="orgName" label="公司名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="address" label="公司地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="taxNo" label="税号/统一社会信用代码" min-width="180" />
<el-table-column label="合作状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建日期" width="120" align="center">
<template #default="{ row }">{{ formatDate(row.createTime) }}</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="primary" @click="onEdit(row)">编辑</el-button>
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
{{ isDisabled(row) ? '恢复' : '禁用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column label="公司类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.orgType === 'sponsor' ? 'success' : 'primary'" disable-transitions>
{{ row.orgType === 'sponsor' ? '赞助方' : '执行方' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="orgName" label="公司名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="address" label="公司地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="taxNo" label="税号/统一社会信用代码" min-width="180" />
<el-table-column label="合作状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建日期" width="120" align="center">
<template #default="{ row }">{{ formatDate(row.createTime) }}</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="primary" @click="onEdit(row)">编辑</el-button>
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
{{ isDisabled(row) ? '恢复' : '禁用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
@@ -86,10 +87,7 @@
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
style="margin-top: 12px; text-align: right;"
/>
<p class="hint-bottom">*点击禁用后, 该公司下所有账号均不可登录</p>
</div>
<!-- 查看 / 编辑 dialog -->
@@ -160,8 +158,8 @@ async function load() {
loading.value = true
try {
const { data } = await bizList('org', { ...q, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = data?.rows || []
page.total = data?.total || 0
rows.value = (data && data.rows) || []
page.total = (data && data.total) || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
@@ -253,10 +251,10 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-orgs { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.filter-bar { margin-bottom: 0; }
.toolbar { display: flex; gap: 8px; margin-bottom: 12px; }
.hint-bottom { font-size: 12px; color: #ff4d4f; margin-top: 12px; text-align: right; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+50 -55
View File
@@ -1,59 +1,54 @@
<template>
<div class="admin-projects">
<div class="page-card">
<div class="desc">查看系统中所有项目 (跨角色全量)</div>
<div class="page-card admin-projects">
<div class="breadcrumb">首页 / 项目全览</div>
<!-- 筛选 -->
<el-form inline :model="q" class="filter-bar">
<el-form-item label="项目编号:">
<el-input v-model="q.projectNo" placeholder="输入项目编号" clearable />
</el-form-item>
<el-form-item label="项目名称:">
<el-input v-model="q.projectName" placeholder="输入项目名称" clearable />
</el-form-item>
<el-form-item label="状态:">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option v-for="s in statusOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item label="是否结题:">
<el-select v-model="q.isFinished" placeholder="全部" clearable>
<el-option label="未结题" value="0" />
<el-option label="已结题" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="输入项目编号" clearable /></el-form-item>
<el-form-item label="项目名称"><el-input v-model="q.projectName" placeholder="输入项目名称" clearable /></el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option v-for="s in statusOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item label="是否结题">
<el-select v-model="q.isFinished" placeholder="全部" clearable>
<el-option label="未结题" value="0" />
<el-option label="已结题" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="projectNo" label="项目编号" width="140" />
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="STATUS_TAG[row.status] || 'info'">{{ STATUS_LABEL[row.status] || row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="isFinished" label="结题" width="80">
<template #default="{ row }">
<el-tag :type="row.isFinished === '1' ? 'success' : 'info'">{{ row.isFinished === '1' ? '已结题' : '未结题' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
</el-table>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="projectNo" label="项目编号" width="140" />
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="STATUS_TAG[row.status] || 'info'">{{ STATUS_LABEL[row.status] || row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="isFinished" label="结题" width="80">
<template #default="{ row }">
<el-tag :type="row.isFinished === '1' ? 'success' : 'info'">{{ row.isFinished === '1' ? '已结题' : '未结题' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
</el-table>
<div class="pager">
<el-pagination
class="pager"
background
layout="total, prev, pager, next, sizes"
v-model:current-page="q.pageNum"
v-model:page-size="q.pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
:page-size="q.pageSize"
:current-page="q.pageNum"
@current-change="(p) => { q.pageNum = p; load() }"
@size-change="(s) => { q.pageSize = s; q.pageNum = 1; load() }"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
</div>
@@ -80,8 +75,8 @@ async function load() {
loading.value = true
try {
const r = await bizList('project', { ...q })
rows.value = r.rows || r.data?.rows || []
total.value = r.total || r.data?.total || 0
rows.value = r.rows || (r.data && r.data.rows) || []
total.value = r.total || (r.data && r.data.total) || 0
} finally {
loading.value = false
}
@@ -96,9 +91,9 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-projects { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.filter-bar { margin-bottom: 16px; }
.pager { margin-top: 16px; text-align: right; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+88 -28
View File
@@ -1,23 +1,21 @@
<template>
<div class="admin-roles">
<div class="page-card">
<div class="desc">查看 6 种角色的用户分布与人员明细</div>
<div class="page-card admin-roles">
<div class="breadcrumb">首页 / 角色管理</div>
<el-row :gutter="16" class="role-cards">
<el-col v-for="r in roleStats" :key="r.code" :xs="12" :sm="8" :md="4">
<div class="role-card" :class="{ active: r.code === selected }" @click="selectRole(r.code)">
<div class="role-label">{{ r.label }}</div>
<div class="role-count">{{ r.count }}</div>
<div class="role-hint">点击查看详情</div>
</div>
</el-col>
</el-row>
</div>
<el-row :gutter="16" class="role-cards">
<el-col v-for="r in roleStats" :key="r.code" :xs="12" :sm="8" :md="4">
<div class="role-card" :class="{ active: r.code === selected }" @click="selectRole(r.code)">
<div class="role-label">{{ r.label }}</div>
<div class="role-count">{{ r.count }}</div>
<div class="role-hint">点击查看详情</div>
</div>
</el-col>
</el-row>
<!-- 选中角色详情 -->
<div v-if="selected" class="page-card" style="margin-top: 16px">
<!-- 选中角色详情 ( page-card , sub-title 分割) -->
<template v-if="selected">
<h3 class="sub-title">{{ ROLE_LABEL[selected] }} - 成员列表</h3>
<el-table :data="rows" border stripe v-loading="loading" style="margin-top: 12px">
<el-table :data="rows" border stripe v-loading="loading" class="role-table">
<el-table-column prop="userId" label="用户ID" width="80" />
<el-table-column prop="userName" label="账号" width="140" />
<el-table-column prop="nickName" label="姓名" width="140" />
@@ -29,19 +27,30 @@
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button size="small" link :type="row.status === '0' ? 'danger' : 'success'" @click="toggleStatus(row)">
{{ row.status === '0' ? '停用' : '启用' }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { listUser } from '@/api/system'
import request from '@/utils/request'
import { ElMessage } from 'element-plus'
const ROLE_LABEL = {
admin: '后台管理员', leader: '项目负责人', manager: '合规人员',
doctor: '评审专家', executor: '执行人', sponsor: '赞助方'
}
// 卡片展示的角色 (隐藏 admin / leader)
const VISIBLE_ROLES = ['manager', 'doctor', 'executor', 'sponsor']
const roleStats = ref([])
const selected = ref('')
@@ -49,17 +58,20 @@ const rows = ref([])
const loading = ref(false)
async function loadStats() {
const codes = Object.keys(ROLE_LABEL)
const arr = []
for (const code of codes) {
for (const code of VISIBLE_ROLES) {
try {
const r = await listUser({ pageNum: 1, pageSize: 1, roleType: code })
arr.push({ code, label: ROLE_LABEL[code], count: r.total || 0 })
arr.push({ code, label: ROLE_LABEL[code], count: (r.data && r.data.total) || r.total || 0 })
} catch (e) {
arr.push({ code, label: ROLE_LABEL[code], count: 0 })
}
}
roleStats.value = arr
// 默认选中第一个角色并加载列表
if (arr.length && !selected.value) {
await selectRole(arr[0].code)
}
}
async function selectRole(code) {
@@ -67,24 +79,72 @@ async function selectRole(code) {
loading.value = true
try {
const r = await listUser({ pageNum: 1, pageSize: 200, roleType: code })
rows.value = r.rows || []
rows.value = (r.data && r.data.rows) || r.rows || []
} finally {
loading.value = false
}
}
async function toggleStatus(row) {
const newStatus = row.status === '0' ? '1' : '0'
try {
await request({
url: '/system/user/changeStatus',
method: 'put',
data: { userId: row.userId, status: newStatus }
})
ElMessage.success('已' + (newStatus === '0' ? '启用' : '停用'))
// 刷新当前角色列表 + 顶部卡片计数
await selectRole(selected.value)
await loadStats()
} catch (e) {
ElMessage.error(e?.msg || '操作失败')
}
}
onMounted(loadStats)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-roles { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.role-cards { margin-top: 8px; }
.role-card { background: #fafbfc; border: 1px solid #ebeef5; border-radius: 4px; padding: 16px; text-align: center; cursor: pointer; transition: all .2s; }
.role-card:hover, .role-card.active { border-color: var(--brand-primary); background: #fff; box-shadow: 0 2px 8px rgba(24,144,255,.12); }
.role-label { font-size: 14px; color: #606266; margin-bottom: 8px; }
.role-count { font-size: 28px; font-weight: 600; color: var(--brand-primary); margin-bottom: 4px; }
.role-card {
background: #fafbfc;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.role-card:hover, .role-card.active {
border-color: var(--brand-primary);
background: #fff;
box-shadow: 0 2px 8px rgba(30,58,138,0.12);
}
.role-label { font-size: 14px; color: #8c8c8c; margin-bottom: 8px; }
.role-count {
font-size: 28px;
font-weight: 600;
margin-bottom: 4px;
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-primary-deep) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.role-hint { font-size: 12px; color: #c0c4cc; }
.sub-title { font-size: 15px; font-weight: 600; color: #1a1a1a; margin: 0; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
.sub-title {
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
margin: 24px 0 12px;
padding-left: 10px;
border-left: 3px solid var(--brand-primary);
line-height: 1;
}
.role-table { margin-top: 12px; }
</style>
+238
View File
@@ -0,0 +1,238 @@
<template>
<!--
角色: 🟦 后台 支持单位管理 (admin/sponsor-orgs)
说明: admin/Orgs.vue 拆出 sponsor 一支, 硬编码 orgType='sponsor'
接口: bizList('org', q) / bizAdd / bizUpdate
-->
<div class="page-card admin-sponsor-orgs">
<div class="breadcrumb">首页 / 支持单位管理</div>
<!-- ========== 筛选区 ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="公司名称">
<el-input v-model="q.orgName" placeholder="输入公司名称" clearable style="width:200px" />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="q.taxNo" placeholder="输入税号" clearable style="width:220px" />
</el-form-item>
<el-form-item label="合作状态">
<el-select v-model="q.status" placeholder="请选择" clearable style="width:140px">
<el-option label="正常" value="正常" />
<el-option label="合作中" value="合作中" />
<el-option label="禁用" value="禁用" />
<el-option label="0正常" value="0" />
<el-option label="1禁用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="batch-bar">
<el-button type="primary" @click="openAdd">新增支持单位</el-button>
</div>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="orgName" label="公司名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="address" label="公司地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="taxNo" label="税号/统一社会信用代码" min-width="180" />
<el-table-column label="合作状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建日期" width="120" align="center">
<template #default="{ row }">{{ formatDate(row.createTime) }}</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="primary" @click="onEdit(row)">编辑</el-button>
<el-button link type="primary" @click="onPeople(row)">人员管理</el-button>
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
{{ isDisabled(row) ? '恢复' : '禁用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
:total="page.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑支持单位' : '新增支持单位'" width="640px">
<el-form :model="form" label-width="120px" :rules="rules" ref="formRef">
<el-form-item label="公司名称" prop="orgName">
<el-input v-model="form.orgName" />
</el-form-item>
<el-form-item label="公司地址">
<el-input v-model="form.address" />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="form.taxNo" />
</el-form-item>
<el-form-item label="联系人">
<el-input v-model="form.contactName" />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="form.contactPhone" maxlength="11" />
</el-form-item>
<el-form-item v-if="isEdit" label="状态">
<el-select v-model="form.status" style="width:160px">
<el-option label="正常" value="正常" />
<el-option label="合作中" value="合作中" />
<el-option label="禁用" value="禁用" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="onSave">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { bizList, bizAdd, bizUpdate } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
const router = useRouter()
const q = reactive({ orgType: 'sponsor', orgName: '', taxNo: '', status: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
function statusTagType(s) {
if (s === '正常' || s === '0' || s === '合作中') return 'success'
if (s === '禁用' || s === '1') return 'danger'
return 'info'
}
function isDisabled(row) {
return row.status === '禁用' || row.status === '1'
}
function formatDate(d) {
if (!d) return ''
const str = String(d)
return str.length >= 10 ? str.slice(0, 10) : str
}
async function load() {
loading.value = true
try {
const { data } = await bizList('org', { ...q, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = (data && data.rows) || []
page.total = (data && data.total) || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() {
Object.assign(q, { orgType: 'sponsor', orgName: '', taxNo: '', status: '' })
page.pageNum = 1
load()
}
async function onToggleStatus(row) {
const disabled = isDisabled(row)
const newStatus = disabled ? '正常' : '禁用'
const label = disabled ? '恢复' : '禁用'
if (!disabled) {
try {
await ElMessageBox.confirm(
`确定要${label}${row.orgName}」吗? ${label}后该企业下所有账号均不可登录`,
'确认',
{ type: 'warning' }
)
} catch { return }
}
try {
await bizUpdate('org', { orgId: row.orgId, status: newStatus })
ElMessage.success('已' + label)
load()
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
}
const dialogVisible = ref(false)
const isEdit = ref(false)
const saving = ref(false)
const formRef = ref(null)
const form = reactive({
orgId: null, orgType: 'sponsor', orgName: '', address: '', taxNo: '',
contactName: '', contactPhone: '', status: '正常'
})
const rules = {
orgName: [{ required: true, message: '请输入公司名称', trigger: 'blur' }]
}
function openAdd() {
isEdit.value = false
Object.assign(form, {
orgId: null, orgType: 'sponsor', orgName: '', address: '', taxNo: '',
contactName: '', contactPhone: '', status: '正常'
})
dialogVisible.value = true
}
function onEdit(row) {
isEdit.value = true
Object.assign(form, { ...row, orgType: 'sponsor' })
dialogVisible.value = true
}
function onPeople(row) {
router.push({ path: '/admin/sponsor-people', query: { orgId: row.orgId, orgName: row.orgName } })
}
function onView(row) {
ElMessageBox.alert(
`类型: 赞助方\n` +
`名称: ${row.orgName}\n` +
`地址: ${row.address || '-'}\n` +
`税号: ${row.taxNo || '-'}\n` +
`联系人: ${row.contactName || '-'} (${row.contactPhone || '-'})\n` +
`状态: ${row.status}\n` +
`创建日期: ${formatDate(row.createTime)}`,
'支持单位详情',
{ confirmButtonText: '关闭' }
).catch(() => {})
}
async function onSave() {
await formRef.value.validate()
saving.value = true
try {
if (isEdit.value) {
await bizUpdate('org', { ...form })
ElMessage.success('修改成功')
} else {
await bizAdd('org', { ...form })
ElMessage.success('新增成功')
}
dialogVisible.value = false
load()
} catch (e) { ElMessage.error(e?.msg || '保存失败') }
finally { saving.value = false }
}
onMounted(load)
</script>
<style scoped>
.admin-sponsor-orgs { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+226
View File
@@ -0,0 +1,226 @@
<template>
<!--
角色: 🟦 后台 支持单位下人员 (admin/sponsor-people)
入口: org 表格行内人员管理按钮 (router.push orgId/orgName)
接口: bizList('person', q) / bizUpdate / bizAdd / sponsorImport / sponsorImportTemplate
-->
<div class="page-card admin-sponsor-people">
<div class="breadcrumb">
首页 / 支持单位管理 / 人员管理{{ orgName ? ' (' + orgName + ')' : '' }}
</div>
<el-form inline :model="q" class="filter-form">
<el-form-item label="姓名">
<el-input v-model="q.name" placeholder="姓名" clearable style="width:140px" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="q.phone" placeholder="手机号" clearable style="width:140px" />
</el-form-item>
<el-form-item label="企业名称">
<el-input v-model="q.orgName" placeholder="企业名称" clearable style="width:180px" />
</el-form-item>
<el-form-item label="部门">
<el-input v-model="q.department" placeholder="部门" clearable style="width:140px" />
</el-form-item>
<el-form-item label="职务">
<el-input v-model="q.position" placeholder="职务" clearable style="width:140px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="batch-bar">
<el-button type="primary" @click="goNew">新建人员</el-button>
<el-button @click="openImport">批量导入</el-button>
<span class="filter-tip">*批量导入模板与列表表项一致</span>
</div>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column prop="name" label="姓名" min-width="100" />
<el-table-column prop="phone" label="手机号" min-width="130" />
<el-table-column prop="orgName" label="企业名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" min-width="100" />
<el-table-column prop="position" label="职务" min-width="100" />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status || '正常' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
{{ isDisabled(row) ? '启用' : '禁用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
:total="page.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
<!-- 批量导入 -->
<el-dialog v-model="importOpen" title="批量导入" width="520px" :close-on-click-modal="false">
<el-form label-width="100px">
<el-form-item label="选择文件">
<el-upload ref="uploadRef" :auto-upload="false" :limit="1" accept=".xls,.xlsx" :on-change="onFileChange" :on-remove="onFileRemove">
<el-button>选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="">
<el-button link type="primary" @click="downloadTemplate">批量导入模板下载</el-button>
</el-form-item>
<el-form-item><span class="filter-tip">*导入列: 姓名/手机号/所属公司/部门/职务/角色</span></el-form-item>
</el-form>
<template #footer>
<el-button @click="importOpen = false">取消</el-button>
<el-button type="primary" :loading="importing" @click="submitImport">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { bizList, bizUpdate, importPerson, downloadImportTemplate } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
const route = useRoute()
const router = useRouter()
const orgId = computed(() => route.query.orgId ? Number(route.query.orgId) : null)
const orgName = computed(() => route.query.orgName || '')
const q = reactive({ name: '', phone: '', orgName: '', department: '', position: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
function statusTagType(s) {
if (s === '禁用' || s === '1') return 'danger'
return 'success'
}
function isDisabled(row) {
return row.status === '禁用' || row.status === '1'
}
async function load() {
loading.value = true
try {
const params = {
...q,
orgType: 'sponsor',
...(orgId.value ? { orgId: orgId.value } : {}),
pageNum: page.pageNum,
pageSize: page.pageSize,
}
const { data } = await bizList('person', params)
rows.value = (data && data.rows) || []
page.total = (data && data.total) || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() {
Object.assign(q, { name: '', phone: '', orgName: '', department: '', position: '' })
page.pageNum = 1
load()
}
async function onToggleStatus(row) {
const disabled = isDisabled(row)
const newStatus = disabled ? '正常' : '禁用'
const label = disabled ? '启用' : '禁用'
try {
await ElMessageBox.confirm(
`确定要${label}${row.name}」吗?`,
'确认',
{ type: 'warning' }
)
} catch { return }
try {
await bizUpdate('person', { personId: row.personId, status: newStatus })
ElMessage.success('已' + label)
load()
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
}
// ========== 新建人员 ==========
function goNew() {
router.push({
path: '/admin/sponsor-people/new',
query: {
...(orgId.value ? { orgId: orgId.value } : {}),
...(orgName.value ? { orgName: orgName.value } : {})
}
})
}
// ========== 批量导入 ==========
const importOpen = ref(false)
const importing = ref(false)
const uploadRef = ref()
const importFile = ref(null)
function openImport() { importFile.value = null; importOpen.value = true }
function onFileChange(file) { importFile.value = file.raw }
function onFileRemove() { importFile.value = null }
async function downloadTemplate() {
try {
const res = await downloadImportTemplate('sponsor')
const blob = new Blob([res.data], { type: res.data.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '支持单位人员导入模板.xlsx'
a.click()
URL.revokeObjectURL(url)
} catch { ElMessage.error('模板下载失败') }
}
async function submitImport() {
if (!importFile.value) { ElMessage.warning('请先选择文件'); return }
importing.value = true
try {
const res = await importPerson('sponsor', importFile.value)
const payload = res.data || res
const ok = payload.ok ?? 0
const total = payload.total ?? 0
const failed = (payload.results || []).filter(r => !r.ok)
if (failed.length === 0) {
ElMessage.success(`导入成功 ${ok}/${total}`)
} else {
ElMessageBox.alert(
`成功 ${ok} 条, 失败 ${failed.length} 条:\n` +
failed.map(f => `${f.rowNo}${f.name || ''}: ${f.message}`).join('\n'),
'导入结果', { type: 'warning' }
)
}
importOpen.value = false
load()
} catch (e) {
ElMessage.error(e?.msg || '导入失败')
} finally {
importing.value = false
}
}
onMounted(load)
</script>
<style scoped>
.admin-sponsor-people { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.filter-tip { color: #909399; font-size: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+50 -52
View File
@@ -1,56 +1,54 @@
<template>
<div class="admin-title">
<div class="page-card">
<div class="desc">维护医生可用的职称字典停用的职称不会出现在医生选择下拉中</div>
<div class="page-card admin-title">
<div class="breadcrumb">首页 / 职称管理</div>
<el-form inline :model="q" class="filter-bar">
<el-form-item label="职称名称:">
<el-input v-model="q.name" placeholder="输入名称模糊搜索" clearable />
</el-form-item>
<el-form-item label="状态:">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option label="用" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="openAdd">新增职称</el-button>
</el-form-item>
</el-form>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="职称名称"><el-input v-model="q.name" placeholder="输入名称模糊搜索" clearable /></el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option label="启用" value="0" />
<el-option label="用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="openAdd">新增职称</el-button>
</el-form-item>
</el-form>
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="titleId" label="ID" width="80" />
<el-table-column prop="name" label="职称名称" min-width="200" />
<el-table-column prop="sort" label="排序" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'info'">{{ row.status === '0' ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="small" link :type="row.status === '0' ? 'warning' : 'success'" @click="toggleStatus(row)">
{{ row.status === '0' ? '停用' : '启用' }}
</el-button>
<el-button size="small" link type="danger" @click="removeOne(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="titleId" label="ID" width="80" />
<el-table-column prop="name" label="职称名称" min-width="200" />
<el-table-column prop="sort" label="排序" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'info'">{{ row.status === '0' ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="small" link :type="row.status === '0' ? 'warning' : 'success'" @click="toggleStatus(row)">
{{ row.status === '0' ? '停用' : '启用' }}
</el-button>
<el-button size="small" link type="danger" @click="removeOne(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
class="pager"
background
layout="total, prev, pager, next, sizes"
v-model:current-page="q.pageNum"
v-model:page-size="q.pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
:page-size="q.pageSize"
:current-page="q.pageNum"
@current-change="(p) => { q.pageNum = p; load() }"
@size-change="(s) => { q.pageSize = s; q.pageNum = 1; load() }"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
@@ -92,8 +90,8 @@ async function load() {
loading.value = true
try {
const r = await listTitles({ ...q })
rows.value = r.rows || []
total.value = r.total || 0
rows.value = (r.data && r.data.rows) || r.rows || []
total.value = (r.data && r.data.total) || r.total || 0
} finally { loading.value = false }
}
@@ -164,9 +162,9 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-title { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.filter-bar { margin-bottom: 16px; }
.pager { margin-top: 16px; text-align: right; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+61 -67
View File
@@ -1,70 +1,63 @@
<template>
<div class="admin-users">
<div class="page-card">
<div class="desc">查看系统中所有用户修改角色与状态</div>
<div class="page-card admin-users">
<div class="breadcrumb">首页 / 用户管理</div>
<!-- 筛选 -->
<el-form inline :model="q" class="filter-bar">
<el-form-item label="用户账号:">
<el-input v-model="q.userName" placeholder="输入用户账号" clearable />
</el-form-item>
<el-form-item label="姓名:">
<el-input v-model="q.nickName" placeholder="输入姓名" clearable />
</el-form-item>
<el-form-item label="角色:">
<el-select v-model="q.roleType" placeholder="全部角色" clearable>
<el-option v-for="r in roleOptions" :key="r.value" :label="r.label" :value="r.value" />
</el-select>
</el-form-item>
<el-form-item label="状态:">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option label="启用" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="用户账号"><el-input v-model="q.userName" placeholder="输入用户账号" clearable /></el-form-item>
<el-form-item label="姓名"><el-input v-model="q.nickName" placeholder="输入姓名" clearable /></el-form-item>
<el-form-item label="角色">
<el-select v-model="q.roleType" placeholder="全部角色" clearable>
<el-option v-for="r in roleOptions" :key="r.value" :label="r.label" :value="r.value" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option label="启用" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- 列表 -->
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="userId" label="用户ID" width="80" />
<el-table-column prop="userName" label="账号" width="120" />
<el-table-column prop="nickName" label="姓名" width="140" />
<el-table-column prop="roleType" label="角色" width="100">
<template #default="{ row }">{{ ROLE_LABEL[row.roleType] || row.roleType }}</template>
</el-table-column>
<el-table-column prop="phonenumber" label="手机号" width="130" />
<el-table-column prop="email" label="邮箱" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'danger'">{{ row.status === '0' ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="small" link :type="row.status === '0' ? 'danger' : 'success'" @click="toggleStatus(row)">
{{ row.status === '0' ? '停用' : '启用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="userId" label="用户ID" width="80" />
<el-table-column prop="userName" label="账号" width="120" />
<el-table-column prop="nickName" label="姓名" width="140" />
<el-table-column prop="roleType" label="角色" width="100">
<template #default="{ row }">{{ ROLE_LABEL[row.roleType] || row.roleType }}</template>
</el-table-column>
<el-table-column prop="phonenumber" label="手机号" width="130" />
<el-table-column prop="email" label="邮箱" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'danger'">{{ row.status === '0' ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="small" link :type="row.status === '0' ? 'danger' : 'success'" @click="toggleStatus(row)">
{{ row.status === '0' ? '停用' : '启用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pager">
<el-pagination
class="pager"
background
layout="total, prev, pager, next, sizes"
v-model:current-page="q.pageNum"
v-model:page-size="q.pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
:page-size="q.pageSize"
:current-page="q.pageNum"
@current-change="(p) => { q.pageNum = p; load() }"
@size-change="(s) => { q.pageSize = s; q.pageNum = 1; load() }"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
@@ -118,8 +111,8 @@ async function load() {
loading.value = true
try {
const r = await listUser({ ...q })
rows.value = r.rows || []
total.value = r.total || 0
rows.value = (r.data && r.data.rows) || r.rows || []
total.value = (r.data && r.data.total) || r.total || 0
} catch (e) {
console.error(e)
} finally {
@@ -146,7 +139,8 @@ function openEdit(row) {
async function saveEdit() {
try {
await request({ url: '/system/user/edit', method: 'put', data: { ...editForm } })
// 2026-08-16: 角色统一存 sys_user.role_type, 单次 PUT 即可 (mapper updateUser 已包含 role_type 字段)
await request({ url: '/system/user', method: 'put', data: { ...editForm } })
ElMessage.success('保存成功')
editVisible.value = false
load()
@@ -174,9 +168,9 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-users { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.filter-bar { margin-bottom: 16px; }
.pager { margin-top: 16px; text-align: right; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+70 -18
View File
@@ -1,8 +1,6 @@
<template>
<div class="admin-workbench">
<!-- 标题 -->
<h2 class="page-title">系统总览</h2>
<div class="page-card admin-workbench">
<div class="breadcrumb">首页</div>
<!-- KPI : 用户/项目/会议/已结题 -->
<div class="stats-grid">
@@ -70,7 +68,7 @@ async function load() {
for (const code of codes) {
try {
const r = await listUser({ pageNum: 1, pageSize: 1, roleType: code })
all.push({ code, label: ROLE_LABEL[code], count: r.total || 0 })
all.push({ code, label: ROLE_LABEL[code], count: (r.data && r.data.total) || r.total || 0 })
} catch (e) {
all.push({ code, label: ROLE_LABEL[code], count: 0 })
}
@@ -79,13 +77,13 @@ async function load() {
// 项目数
const ps = await bizList('project', { pageNum: 1, pageSize: 1 })
stats.value.totalProjects = ps.total || ps.data?.total || 0
stats.value.totalProjects = ps.total || (ps.data && ps.data.total) || 0
// 已结题
const fp = await bizList('project', { pageNum: 1, pageSize: 1, isFinished: '1' })
stats.value.finishedProjects = fp.total || fp.data?.total || 0
stats.value.finishedProjects = fp.total || (fp.data && fp.data.total) || 0
// 会议数
const ms = await bizList('meeting', { pageNum: 1, pageSize: 1 })
stats.value.totalMeetings = ms.total || ms.data?.total || 0
stats.value.totalMeetings = ms.total || (ms.data && ms.data.total) || 0
} catch (e) {
console.warn('admin workbench load failed', e)
}
@@ -95,18 +93,72 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-workbench { padding: 16px; }
.page-title { font-size: 20px; font-weight: 600; color: #1a1a1a; margin-bottom: 16px; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.stats-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; margin-bottom: 12px; }
.stat-card { background: #fff; border-radius: 4px; padding: 24px; border: 1px solid #ebeef5; cursor: pointer; text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; justify-content: center; transition: opacity .15s, border-color .15s; min-height: 120px; }
.stat-card:hover { border-color: var(--brand-primary); opacity: .9; }
.stat-label { font-size: 14px; color: #606266; margin-bottom: 16px; }
.stat-value { font-size: 32px; font-weight: 600; color: var(--brand-primary); line-height: 1.2; }
.stat-card {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 24px;
cursor: pointer;
text-decoration: none;
color: inherit;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: border-color 0.2s, box-shadow 0.2s;
min-height: 120px;
}
.stat-card:hover { border-color: var(--brand-primary); box-shadow: 0 2px 8px rgba(30,58,138,0.15); }
.stat-label { font-size: 14px; color: #8c8c8c; margin-bottom: 16px; }
.stat-value {
font-size: 32px;
font-weight: 600;
line-height: 1.2;
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-primary-deep) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hint-text { font-size: 12px; color: #ff4d4f; margin: 12px 0 20px; text-align: center; }
.section { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; margin-top: 16px; }
.section-title { font-size: 15px; font-weight: 600; color: #1a1a1a; margin-bottom: 16px; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
.section {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 20px 24px;
margin-top: 16px;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 16px;
padding-left: 10px;
border-left: 3px solid var(--brand-primary);
line-height: 1;
}
.role-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 12px; }
.role-card { background: #fafbfc; border: 1px solid #ebeef5; border-radius: 4px; padding: 16px; text-align: center; }
.role-name { font-size: 13px; color: #606266; margin-bottom: 8px; }
.role-count { font-size: 24px; font-weight: 600; color: var(--brand-primary); }
.role-card {
background: #fafbfc;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 16px;
text-align: center;
}
.role-name { font-size: 13px; color: #8c8c8c; margin-bottom: 8px; }
.role-count {
font-size: 24px;
font-weight: 600;
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-primary-deep) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
</style>
+1 -1
View File
@@ -37,7 +37,7 @@ const detailOpen = ref(false)
async function load() {
try {
const r = await listMyMessages({ limit: 50 }); const list = r.rows
const r = await listMyMessages({ limit: 50 }); const list = (r.data && r.data.rows) || r.rows || []
rows.value = list.map(a => ({
id: a.id,
category: a.category,
+29 -29
View File
@@ -1,29 +1,29 @@
<template>
<div class="executor-account">
<div class="page-card executor-account">
<div class="breadcrumb">首页 / 账号信息</div>
<div class="page-card">
<el-form :model="form" label-width="120px" style="max-width:520px;">
<el-form-item label="姓名">
<el-input v-model="form.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
<el-form-item label="原密码">
<el-input v-model="form.oldPwd" type="password" show-password placeholder="请输入原密码" />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="form.newPwd" type="password" show-password placeholder="请输入新密码" />
</el-form-item>
<el-form-item label="确认密码">
<el-input v-model="form.confirmPwd" type="password" show-password placeholder="请再次输入新密码" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSave">保存</el-button>
<el-button @click="onCancel">取消</el-button>
</el-form-item>
</el-form>
</div>
<p class="page-sub">管理账号信息, 修改密码或手机号</p>
<el-form :model="form" label-width="120px" class="account-form">
<el-form-item label="姓名">
<el-input v-model="form.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
<el-form-item label="原密码">
<el-input v-model="form.oldPwd" type="password" show-password placeholder="请输入原密码" />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="form.newPwd" type="password" show-password placeholder="请输入新密码" />
</el-form-item>
<el-form-item label="确认密码">
<el-input v-model="form.confirmPwd" type="password" show-password placeholder="请再次输入新密码" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSave">保存</el-button>
<el-button @click="onCancel">取消</el-button>
</el-form-item>
</el-form>
</div>
</template>
@@ -68,9 +68,9 @@ function onCancel() {
</script>
<style scoped>
.executor-account { padding: 0; }
.breadcrumb { color: #909399; font-size: 13px; margin-bottom: 12px; }
.page-card { background: #fff; border-radius: 4px; padding: 24px; border: 1px solid #ebeef5; }
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
/* 整页面板 (与 People.vue 风格一致) */
.executor-account { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.page-sub { font-size: 13px; color: #909399; margin-bottom: 16px; }
.account-form { max-width: 520px; }
</style>
+61 -57
View File
@@ -1,54 +1,52 @@
<template>
<div class="executor-labor">
<div class="page-card executor-labor">
<div class="breadcrumb">首页 / 劳务凭证</div>
<p class="page-sub">管理会务执行产生的劳务凭证, 上传付款凭证 PDF</p>
<div class="filter-card">
<el-form :inline="true" :model="q" size="default">
<el-form-item label="会议名称:">
<el-input v-model="q.meetingName" clearable placeholder="输入会议名称" style="width:200px" />
</el-form-item>
<el-form-item label="专家姓名:">
<el-input v-model="q.expertName" clearable placeholder="输入专家姓名" style="width:160px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- 红字提示 ( People.vue 一致) -->
<div class="hint-list">
<p>*结算前, 执行方和支持方无法看到凭证材料菜单, 结算后才能看到</p>
<p>*结算后, 已完结状态, 执行方不能再编辑</p>
</div>
<div class="page-card">
<p class="tip">*结算前执行方和支持方无法看到凭证材料菜单结算后才能看到结算材料</p>
<p class="tip">*结算后已完结状态 执行方不能再进行编辑</p>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="会议名称"><el-input v-model="q.meetingName" clearable placeholder="输入会议名称" style="width:200px" /></el-form-item>
<el-form-item label="专家姓名"><el-input v-model="q.expertName" clearable placeholder="输入专家姓名" style="width:160px" /></el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column prop="voucherNo" label="凭证编号" width="140" />
<el-table-column prop="meetingName" label="会议名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="expertName" label="专家姓名" width="110" />
<el-table-column prop="idCard" label="身份证号" width="180" />
<el-table-column prop="bankAccount" label="银行账号" width="220" />
<el-table-column label="金额" width="120" align="right">
<template #default="{ row }">¥ {{ formatMoney(row.amount) }}</template>
</el-table-column>
<el-table-column prop="submitDate" label="提交日期" width="160" />
<el-table-column label="付款凭证.pdf" width="180" align="center">
<template #default="{ row }">
<el-link v-if="row.paymentVoucherUrl" type="primary" @click="previewVoucher(row.paymentVoucherUrl)">查看</el-link>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="success" @click="onUploadVoucher(row)">上传付款凭证</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="load" @size-change="load" />
</div>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column prop="voucherNo" label="凭证编号" width="140" />
<el-table-column prop="meetingName" label="会议名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="expertName" label="专家姓名" width="110" />
<el-table-column prop="idCard" label="身份证号" width="180" />
<el-table-column prop="bankAccount" label="银行账号" width="220" />
<el-table-column label="金额" width="120" align="right">
<template #default="{ row }">¥ {{ formatMoney(row.amount) }}</template>
</el-table-column>
<el-table-column prop="submitDate" label="提交日期" width="160" />
<el-table-column label="付款凭证.pdf" width="180" align="center">
<template #default="{ row }">
<el-link v-if="row.paymentVoucherUrl" type="primary" @click="previewVoucher(row.paymentVoucherUrl)">查看</el-link>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="success" @click="onUploadVoucher(row)">上传付款凭证</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="load" @size-change="load" />
</div>
<el-dialog v-model="detailOpen" title="劳务凭证详情" width="640px">
@@ -198,18 +196,24 @@ onMounted(load)
</script>
<style scoped>
.executor-labor { padding: 0; }
.breadcrumb { color: #909399; font-size: 13px; margin-bottom: 12px; }
.filter-card, .page-card { background: #fff; border-radius: 4px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.tip { font-size: 12px; color: #f56c6c; margin: 4px 0; }
/* 整页面板 (与 People.vue 风格一致) */
.executor-labor { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.page-sub { font-size: 13px; color: #909399; margin-bottom: 16px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
:deep(.el-table .el-button) { border-radius: 4px; }
:deep(.el-table .el-button.is-link) { color: var(--brand-primary); background: transparent; border: none; }
:deep(.el-table .el-button.is-link:hover) { color: var(--brand-primary-deep, var(--brand-primary)); background: transparent; }
:deep(.el-table .el-button.is-link.is-disabled) { color: #c0c4cc; background: transparent; }
:deep(.el-button--warning) { background: #e6a23c; border-color: #e6a23c; border-radius: 4px; }
:deep(.el-table .el-button) { border-radius: 4px; }
/* 红字提示 (与 People.vue 一致) */
.hint-list {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 8px;
padding: 12px 20px;
margin-bottom: 16px;
font-size: 12px;
color: #ff4d4f;
line-height: 1.8;
}
.hint-list p { margin-bottom: 4px; }
.hint-list p:last-child { margin-bottom: 0; }
</style>
+68 -84
View File
@@ -1,80 +1,70 @@
<template>
<div class="executor-meetings">
<div class="page-card executor-meetings">
<div class="breadcrumb">首页 / 会议列表</div>
<p class="page-sub">查看执行方参与的会议, 提交监督意见</p>
<div class="filter-card">
<el-form :inline="true" :model="q" size="default">
<el-form-item label="项目编号:">
<el-input v-model="q.projectNo" clearable placeholder="输入项目编号" style="width:160px" />
</el-form-item>
<el-form-item label="会议名称:">
<el-input v-model="q.meetingName" clearable placeholder="输入会议名称" style="width:200px" />
</el-form-item>
<el-form-item label="是否系列会:">
<el-select v-model="q.isSeries" clearable placeholder="请选择" style="width:140px">
<el-option label="是" value="1" />
<el-option label="否" value="0" />
</el-select>
</el-form-item>
<el-form-item label="是否结题:">
<el-select v-model="q.isFinished" clearable placeholder="请选择" style="width:140px">
<el-option label="已结题" value="1" />
<el-option label="未结题" value="0" />
</el-select>
</el-form-item>
<el-form-item label="期数:">
<el-input v-model="q.sessionNo" clearable placeholder="输入期数" style="width:100px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="项目编号"><el-input v-model="q.projectNo" clearable placeholder="输入项目编号" style="width:160px" /></el-form-item>
<el-form-item label="会议名称"><el-input v-model="q.meetingName" clearable placeholder="输入会议名称" style="width:200px" /></el-form-item>
<el-form-item label="是否系列会">
<el-select v-model="q.isSeries" clearable placeholder="请选择" style="width:140px">
<el-option label="是" value="1" />
<el-option label="否" value="0" />
</el-select>
</el-form-item>
<el-form-item label="是否结题">
<el-select v-model="q.isFinished" clearable placeholder="请选择" style="width:140px">
<el-option label="已结题" value="1" />
<el-option label="未结题" value="0" />
</el-select>
</el-form-item>
<el-form-item label="期数"><el-input v-model="q.sessionNo" clearable placeholder="输入期数" style="width:100px" /></el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="page-card">
<div class="toolbar">
<span class="hint">已选 {{ selected.length }} </span>
</div>
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelect">
<el-table-column type="selection" width="48" />
<el-table-column prop="projectNo" label="项目编号" width="120" />
<el-table-column prop="meetingName" label="会议名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="isSeries" label="是否系列会" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.isSeries === '1' || row.isSeries === '是' ? 'success' : 'info'" size="small">
{{ row.isSeries === '1' || row.isSeries === '是' ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="seriesTopic" label="系列会主题" min-width="160" show-overflow-tooltip />
<el-table-column label="期数/总期数" width="110" align="center">
<template #default="{ row }">{{ row.periodNo || 0 }}/{{ row.totalPeriods || 0 }}</template>
</el-table-column>
<el-table-column prop="currentStage" label="当前阶段" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.currentStage === '执行中' ? 'success' : 'info'" size="small">{{ row.currentStage || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="isFinished" label="是否结题" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.isFinished === '1' || row.isFinished === '已结题' ? 'warning' : 'info'" size="small">
{{ row.isFinished === '1' || row.isFinished === '已结题' ? '已结题' : '未结题' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="warning" @click="onSupervision(row)">查看监督意见</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="load" @size-change="load" />
</div>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelect">
<el-table-column type="selection" width="48" />
<el-table-column prop="projectNo" label="项目编号" width="120" />
<el-table-column prop="meetingName" label="会议名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="isSeries" label="是否系列会" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.isSeries === '1' || row.isSeries === '是' ? 'success' : 'info'" size="small">
{{ row.isSeries === '1' || row.isSeries === '是' ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="seriesTopic" label="系列会主题" min-width="160" show-overflow-tooltip />
<el-table-column label="期数/总期数" width="110" align="center">
<template #default="{ row }">{{ row.periodNo || 0 }}/{{ row.totalPeriods || 0 }}</template>
</el-table-column>
<el-table-column prop="currentStage" label="当前阶段" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.currentStage === '执行中' ? 'success' : 'info'" size="small">{{ row.currentStage || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="isFinished" label="是否结题" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.isFinished === '1' || row.isFinished === '已结题' ? 'warning' : 'info'" size="small">
{{ row.isFinished === '1' || row.isFinished === '已结题' ? '已结题' : '未结题' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="warning" @click="onSupervision(row)">查看监督意见</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="load" @size-change="load" />
</div>
<!-- 监督意见 弹窗 -->
@@ -182,16 +172,10 @@ onMounted(load)
</script>
<style scoped>
.executor-meetings { padding: 0; }
.breadcrumb { color: #909399; font-size: 13px; margin-bottom: 12px; }
.filter-card, .page-card { background: #fff; border-radius: 4px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.hint { font-size: 13px; color: #909399; }
/* 整页面板 (与 People.vue 风格一致) */
.executor-meetings { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.page-sub { font-size: 13px; color: #909399; margin-bottom: 16px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
:deep(.el-table .el-button) { border-radius: 4px; }
:deep(.el-table .el-button.is-link) { color: var(--brand-primary); background: transparent; border: none; }
:deep(.el-table .el-button.is-link:hover) { color: var(--brand-primary-deep); background: transparent; }
:deep(.el-table .el-button.is-link.is-disabled) { color: #c0c4cc; background: transparent; }
</style>
+48 -12
View File
@@ -1,7 +1,7 @@
<template>
<div class="executor-overview">
<h1 class="page-title">首页</h1>
<p class="hint">*点击数字跳转到相应的列表页</p>
<div class="page-card executor-overview">
<div class="breadcrumb">首页</div>
<h1 class="page-title" style="display:none"></h1>
<div class="stats-grid">
<router-link class="stat-card" to="/executor/projects">
@@ -62,16 +62,52 @@ onMounted(loadStats)
</script>
<style scoped>
.executor-overview { padding: 0; }
.page-title { font-size: 22px; font-weight: 600; color: #1a1a1a; margin: 0 0 8px; }
.hint { font-size: 12px; color: #f56c6c; margin: 0 0 16px; }
/* 整页面板 (与 People.vue 风格一致) */
.executor-overview { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.page-title { display: none; }
/* 统计卡片 */
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 20px; }
.stat-card { background: #fff; border: 1px solid #ebeef5; border-radius: 4px; padding: 32px 24px; text-decoration: none; color: inherit; display: block; text-align: center; }
.stat-card:hover { border-color: var(--brand-primary); }
.stat-label { font-size: 14px; color: #909399; margin-bottom: 16px; }
.stat-value { font-size: 40px; font-weight: 700; color: var(--brand-primary); line-height: 1; }
.section { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.section-title { font-size: 15px; font-weight: 600; color: #1a1a1a; margin: 0 0 16px; }
.stat-card {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 32px 24px;
text-decoration: none;
color: inherit;
display: block;
text-align: center;
transition: border-color 0.2s, box-shadow 0.2s;
}
.stat-card:hover { border-color: var(--brand-primary); box-shadow: 0 2px 8px rgba(30,58,138,0.15); }
.stat-label { font-size: 14px; color: #8c8c8c; margin-bottom: 16px; }
.stat-value {
font-size: 40px;
font-weight: 700;
line-height: 1;
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-primary-deep) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* 消息通知 */
.section {
background: #fff;
border-radius: 8px;
padding: 20px 24px;
border: 1px solid #f0f0f0;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 16px;
padding-left: 10px;
border-left: 3px solid var(--brand-primary);
line-height: 1;
}
.notice-list { list-style: none; padding: 0; margin: 0; }
.notice-item { padding: 12px 0; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; gap: 12px; }
.notice-item:last-child { border-bottom: none; }
+84 -94
View File
@@ -1,87 +1,82 @@
<template>
<div class="executor-projects">
<div class="page-card executor-projects">
<div class="breadcrumb">首页 / 项目列表</div>
<p class="page-sub">查看执行方承接的项目, 进行项目分配与评价</p>
<div class="filter-card">
<el-form :inline="true" :model="query" size="default">
<el-form-item label="项目编号:">
<el-input v-model="query.projectNo" clearable placeholder="项目编号" style="width:160px" />
</el-form-item>
<el-form-item label="项目名称:">
<el-input v-model="query.projectName" clearable placeholder="项目名称" style="width:200px" />
</el-form-item>
<el-form-item label="项目时间:">
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px" />
</el-form-item>
<el-form-item label="是否结题:">
<el-select v-model="query.isFinished" clearable placeholder="请选择" style="width:140px">
<el-option label="已结题" value="1" />
<el-option label="未结题" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadList">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="query" class="filter-form">
<el-form-item label="项目编号"><el-input v-model="query.projectNo" clearable placeholder="项目编号" style="width:160px" /></el-form-item>
<el-form-item label="项目名称"><el-input v-model="query.projectName" clearable placeholder="项目名称" style="width:200px" /></el-form-item>
<el-form-item label="项目时间">
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px" />
</el-form-item>
<el-form-item label="是否结题">
<el-select v-model="query.isFinished" clearable placeholder="请选择" style="width:140px">
<el-option label="已结题" value="1" />
<el-option label="未结题" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadList">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 批量按钮 ( People.vue 风格一致) ========== -->
<div class="batch-bar">
<el-button type="primary" :disabled="!selected.length" @click="onAssign">项目分配</el-button>
<el-button type="primary" :disabled="!selected.length" @click="onBatchRate">批量评价</el-button>
<span class="filter-tip">已选 {{ selected.length }} </span>
</div>
<div class="page-card">
<div class="batch-bar">
<el-button type="primary" :disabled="!selected.length" @click="onAssign">项目分配</el-button>
<el-button type="primary" :disabled="!selected.length" @click="onBatchRate">批量评价</el-button>
<span class="hint">已选 {{ selected.length }} </span>
<span class="tip">*支持方管理员在分配项目的时候只能分配监察员</span>
</div>
<el-table :data="list" border stripe v-loading="loading" @selection-change="sel=>selected=sel">
<el-table-column type="selection" width="48" />
<el-table-column prop="projectNo" label="项目编号" width="120" />
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<el-table-column label="总场次/总期数" width="110" align="center">
<template #default="{ row }">{{ row.totalSessions || 0 }}/{{ row.totalPeriods || 0 }}</template>
</el-table-column>
<el-table-column prop="doneSessions" label="已执行" width="80" align="center" />
<el-table-column prop="todoSessions" label="未执行" width="80" align="center" />
<el-table-column label="总金额" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.totalAmount) }}</template>
</el-table-column>
<el-table-column label="可用金额" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.availableAmount) }}</template>
</el-table-column>
<el-table-column label="已支付劳务费" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.paidLaborAmount) }}</template>
</el-table-column>
<el-table-column label="已支付会务费" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.paidMeetingAmount) }}</template>
</el-table-column>
<el-table-column label="执行单位评价(支持方)" width="180" align="center">
<template #default="{ row }">
<el-rate v-model="row.ratingScore" disabled show-score :max="5" />
</template>
</el-table-column>
<el-table-column prop="isFinished" label="是否结题" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.isFinished === '1' || row.isFinished === '已结题' ? 'warning' : 'info'" size="small">
{{ row.isFinished === '1' || row.isFinished === '已结题' ? '已结题' : '未结题' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="startTime" label="项目开始时间" width="150" />
<el-table-column prop="endTime" label="项目结束时间" width="150" />
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="viewDetail(row)">查看</el-button>
<el-button link type="primary" @click="onAssign(row)">分配</el-button>
<el-button link type="primary" @click="onRate(row)">项目评价</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="loadList" @size-change="loadList" />
</div>
<p class="footnote">*点击后可查看具体评价维度评价标准与供应商系统保持一致</p>
<!-- ========== 表格 ( People.vue 风格一致) ========== -->
<el-table :data="list" border stripe v-loading="loading" @selection-change="sel=>selected=sel">
<el-table-column type="selection" width="48" />
<el-table-column prop="projectNo" label="项目编号" width="120" />
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<el-table-column label="总场次/总期数" width="110" align="center">
<template #default="{ row }">{{ row.totalSessions || 0 }}/{{ row.totalPeriods || 0 }}</template>
</el-table-column>
<el-table-column prop="doneSessions" label="已执行" width="80" align="center" />
<el-table-column prop="todoSessions" label="未执行" width="80" align="center" />
<el-table-column label="总金额" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.totalAmount) }}</template>
</el-table-column>
<el-table-column label="可用金额" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.availableAmount) }}</template>
</el-table-column>
<el-table-column label="已支付劳务费" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.paidLaborAmount) }}</template>
</el-table-column>
<el-table-column label="已支付会务费" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.paidMeetingAmount) }}</template>
</el-table-column>
<el-table-column label="执行单位评价(支持方)" width="180" align="center">
<template #default="{ row }">
<el-rate v-model="row.ratingScore" disabled show-score :max="5" />
</template>
</el-table-column>
<el-table-column prop="isFinished" label="是否结题" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.isFinished === '1' || row.isFinished === '已结题' ? 'warning' : 'info'" size="small">
{{ row.isFinished === '1' || row.isFinished === '已结题' ? '已结题' : '未结题' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="startTime" label="项目开始时间" width="150" />
<el-table-column prop="endTime" label="项目结束时间" width="150" />
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="viewDetail(row)">查看</el-button>
<el-button link type="primary" @click="onAssign(row)">分配</el-button>
<el-button link type="primary" @click="onRate(row)">项目评价</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="loadList" @size-change="loadList" />
</div>
<!-- 项目分配 弹窗 -->
@@ -182,8 +177,8 @@ async function loadList() {
params.endTime = dateRange.value[1]
}
const r = await bizList('project', params)
list.value = r.rows || []
page.total = r.total || 0
list.value = (r.data && r.data.rows) || r.rows || []
page.total = (r.data && r.data.total) || r.total || 0
} catch (e) { list.value = []; page.total = 0 }
finally { loading.value = false }
}
@@ -200,7 +195,7 @@ function reset() {
function viewDetail(row) { detail.value = row; detailOpen.value = true }
async function loadPeople() {
try { const r = await bizList('person', { pageNum: 1, pageSize: 200 }); people.value = (r.rows || []).map(p => ({ userId: p.personId, name: p.name })) }
try { const r = await bizList('person', { pageNum: 1, pageSize: 200 }); people.value = ((r.data && r.data.rows) || r.rows || []).map(p => ({ userId: p.personId, name: p.name })) }
catch { people.value = [] }
}
@@ -259,17 +254,12 @@ onMounted(() => { loadList(); loadPeople() })
</script>
<style scoped>
.executor-projects { padding: 0; }
.breadcrumb { color: #909399; font-size: 13px; margin-bottom: 12px; }
.filter-card, .page-card { background: #fff; border-radius: 4px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.batch-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
.hint { font-size: 13px; color: #909399; }
.tip { font-size: 12px; color: #f56c6c; margin-left: 12px; }
/* 整页面板 (与 People.vue 风格一致) */
.executor-projects { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.page-sub { font-size: 13px; color: #909399; margin-bottom: 16px; }
.batch-bar { margin-bottom: 12px; display: flex; gap: 8px; align-items: center; }
.filter-tip { color: #909399; font-size: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
.footnote { font-size: 12px; color: #f56c6c; margin-top: 8px; }
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
:deep(.el-table .el-button) { border-radius: 4px; }
:deep(.el-table .el-button.is-link) { color: var(--brand-primary); }
:deep(.el-table .el-button.is-link:hover) { color: var(--brand-primary-deep); }
.filter-form { margin-bottom: 12px; }
</style>
+1 -1
View File
@@ -81,7 +81,7 @@ async function loadStats() {
async function loadNotices() {
try {
const r = await listMyMessages({ limit: 50 }); const list = r.rows
const r = await listMyMessages({ limit: 50 }); const list = (r.data && r.data.rows) || r.rows || []
notices.value = list.map(n => ({
id: n.id,
title: n.title,
+2 -2
View File
@@ -149,8 +149,8 @@ async function load() {
loading.value = true
try {
const r = await bizList('meeting', { ...q, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = r.rows || []
page.total = r.total || 0
rows.value = (r.data && r.data.rows) || r.rows || []
page.total = (r.data && r.data.total) || r.total || 0
} catch (e) {
rows.value = []
page.total = 0
+3 -3
View File
@@ -219,8 +219,8 @@ async function load() {
loading.value = true
try {
const r = await bizList('project', { ...q, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = r.rows || []
page.total = r.total || 0
rows.value = (r.data && r.data.rows) || r.rows || []
page.total = (r.data && r.data.total) || r.total || 0
} catch (e) { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
@@ -228,7 +228,7 @@ async function load() {
async function loadPersons() {
try {
const r = await bizList('person', { pageNum: 1, pageSize: 100 })
personList.value = r.rows || []
personList.value = (r.data && r.data.rows) || r.rows || []
} catch (e) { personList.value = [] }
}
+35 -29
View File
@@ -1,31 +1,31 @@
<template>
<div class="manager-accounts">
<div class="page-card">
<div class="desc">查看当前登录账号修改个人信息与密码</div>
<el-descriptions :column="2" border style="margin-bottom: 24px">
<el-descriptions-item label="账号">{{ profile.userName }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ profile.nickName || profile.userName }}</el-descriptions-item>
<el-descriptions-item label="角色">{{ (profile.roleGroup || '').split(',')[0] || '合规人员' }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ profile.status === '0' ? '正常' : '停用' }}</el-descriptions-item>
<el-descriptions-item label="手机">{{ profile.phonenumber || '-' }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ profile.email || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ profile.createTime || '-' }}</el-descriptions-item>
</el-descriptions>
<div class="page-card manager-accounts">
<div class="breadcrumb">首页 / 账号管理</div>
<p class="page-sub">查看当前登录账号修改个人信息与密码</p>
<el-form :model="form" label-width="120px" style="max-width:600px" :rules="rules" ref="formRef">
<div class="section-title">修改个人信息</div>
<el-form-item label="姓名" prop="nickName"><el-input v-model="form.nickName" placeholder="请输入姓名" /></el-form-item>
<el-form-item label="手机号" prop="phonenumber"><el-input v-model="form.phonenumber" placeholder="请输入手机号" /></el-form-item>
<el-form-item label="邮箱" prop="email"><el-input v-model="form.email" placeholder="请输入邮箱" /></el-form-item>
<el-descriptions :column="2" border style="margin-bottom: 24px">
<el-descriptions-item label="账号">{{ profile.userName }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ profile.nickName || profile.userName }}</el-descriptions-item>
<el-descriptions-item label="角色">{{ (profile.roleGroup || '').split(',')[0] || '合规人员' }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ profile.status === '0' ? '正常' : '停用' }}</el-descriptions-item>
<el-descriptions-item label="手机">{{ profile.phonenumber || '-' }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ profile.email || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ profile.createTime || '-' }}</el-descriptions-item>
</el-descriptions>
<div class="section-title">修改密码</div>
<el-form-item label="原密码" prop="oldPassword"><el-input v-model="form.oldPassword" type="password" show-password placeholder="请输入原密码" /></el-form-item>
<el-form-item label="新密码" prop="newPassword"><el-input v-model="form.newPassword" type="password" show-password placeholder="请输入新密码" /></el-form-item>
<el-form-item label="确认密码" prop="confirmPassword"><el-input v-model="form.confirmPassword" type="password" show-password placeholder="请再次输入新密码" /></el-form-item>
<div class="form-tip">* 密码长度 6-20 ,支持数字字母特殊字符;留空表示不修改密码</div>
<el-form-item><el-button type="primary" :loading="saving" @click="onSave">保存</el-button><el-button @click="onCancel">取消</el-button></el-form-item>
</el-form>
</div>
<el-form :model="form" label-width="120px" style="max-width:600px" :rules="rules" ref="formRef">
<div class="section-title">修改个人信息</div>
<el-form-item label="姓名" prop="nickName"><el-input v-model="form.nickName" placeholder="请输入姓名" /></el-form-item>
<el-form-item label="手机号" prop="phonenumber"><el-input v-model="form.phonenumber" placeholder="请输入手机号" /></el-form-item>
<el-form-item label="邮箱" prop="email"><el-input v-model="form.email" placeholder="请输入邮箱" /></el-form-item>
<div class="section-title">修改密码</div>
<el-form-item label="原密码" prop="oldPassword"><el-input v-model="form.oldPassword" type="password" show-password placeholder="请输入原密码" /></el-form-item>
<el-form-item label="新密码" prop="newPassword"><el-input v-model="form.newPassword" type="password" show-password placeholder="请输入新密码" /></el-form-item>
<el-form-item label="确认密码" prop="confirmPassword"><el-input v-model="form.confirmPassword" type="password" show-password placeholder="请再次输入新密码" /></el-form-item>
<div class="filter-tip">* 密码长度 6-20 ,支持数字字母特殊字符;留空表示不修改密码</div>
<el-form-item><el-button type="primary" :loading="saving" @click="onSave">保存</el-button><el-button @click="onCancel">取消</el-button></el-form-item>
</el-form>
</div>
</template>
@@ -92,9 +92,15 @@ onMounted(loadProfile)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-accounts { padding: 16px; }
.page-card { background: #fff; border-radius: 6px; padding: 24px; border: 1px solid #ebeef5; }
.desc { color: #909399; font-size: 13px; margin-bottom: 16px; }
.section-title { font-weight: 600; margin: 16px 0 12px; padding-left: 8px; border-left: 3px solid var(--brand-primary); }
.form-tip { color: #909399; font-size: 12px; margin-bottom: 12px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.page-sub { font-size: 13px; color: #909399; margin-bottom: 16px; }
.filter-tip { color: #909399; font-size: 12px; margin-bottom: 12px; }
.section-title {
font-weight: 600;
margin: 16px 0 12px;
padding-left: 8px;
border-left: 3px solid var(--brand-primary);
}
</style>
+41 -38
View File
@@ -1,39 +1,41 @@
<template>
<div class="manager-exec-intent">
<div class="page-card">
<div class="desc">查看专家对项目的执行意向含待签约/已签约/已拒绝/已退出</div>
<el-form inline :model="q">
<el-form-item label="项目编号:"><el-input v-model="q.projectNo" clearable /></el-form-item>
<el-form-item label="项目名称:"><el-input v-model="q.projectName" clearable /></el-form-item>
<el-form-item label="姓名:"><el-input v-model="q.name" clearable /></el-form-item>
<el-form-item label="工作单位:"><el-input v-model="q.workUnit" clearable /></el-form-item>
<el-form-item label="手机号:"><el-input v-model="q.phone" clearable /></el-form-item>
<el-form-item label="签约状态:">
<el-select v-model="q.onboardStatus" clearable>
<el-option label="待签约" value="待签约" /><el-option label="已签约" value="已签约" />
<el-option label="已拒绝" value="已拒绝" /><el-option label="已退出" value="已退出" />
</el-select>
</el-form-item>
<el-form-item><el-button type="primary" @click="load">查找</el-button><el-button @click="reset">重置</el-button></el-form-item>
</el-form>
</div>
<div class="page-card">
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="projectNo" label="意向项目编号" width="140" />
<el-table-column prop="projectName" label="意向项目名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="workUnit" label="工作单位" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" width="120" />
<el-table-column prop="position" label="职务" width="100" />
<el-table-column prop="phone" label="手机号" width="130" />
<el-table-column label="签约状态" width="100">
<template #default="{ row }">
<el-tag :type="row.onboardStatus==='已签约' ? 'success' : row.onboardStatus==='已拒绝' ? 'danger' : row.onboardStatus==='已退出' ? 'info' : 'warning'">{{ row.onboardStatus }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
</el-table>
<div class="page-card manager-exec-intent">
<div class="breadcrumb">首页 / 执行意向</div>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="项目编号"><el-input v-model="q.projectNo" clearable /></el-form-item>
<el-form-item label="项目名称"><el-input v-model="q.projectName" clearable /></el-form-item>
<el-form-item label="姓名"><el-input v-model="q.name" clearable /></el-form-item>
<el-form-item label="工作单位"><el-input v-model="q.workUnit" clearable /></el-form-item>
<el-form-item label="手机号"><el-input v-model="q.phone" clearable /></el-form-item>
<el-form-item label="签约状态">
<el-select v-model="q.onboardStatus" clearable>
<el-option label="待签约" value="待签约" /><el-option label="已签约" value="已签约" />
<el-option label="已拒绝" value="已拒绝" /><el-option label="已退出" value="已退出" />
</el-select>
</el-form-item>
<el-form-item><el-button type="primary" @click="load">查找</el-button><el-button @click="reset">重置</el-button></el-form-item>
</el-form>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="projectNo" label="意向项目编号" width="140" />
<el-table-column prop="projectName" label="意向项目名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="workUnit" label="工作单位" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" width="120" />
<el-table-column prop="position" label="职务" width="100" />
<el-table-column prop="phone" label="手机号" width="130" />
<el-table-column label="签约状态" width="100">
<template #default="{ row }">
<el-tag :type="row.onboardStatus==='已签约' ? 'success' : row.onboardStatus==='已拒绝' ? 'danger' : row.onboardStatus==='已退出' ? 'info' : 'warning'">{{ row.onboardStatus }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
</div>
</div>
@@ -62,8 +64,9 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-exec-intent { padding: 16px; }
.page-card { background: #fff; border-radius: 6px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.page-title { font-size: 20px; font-weight: 600; margin-bottom: 4px; }
.desc { color: #909399; font-size: 13px; margin-bottom: 12px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+120
View File
@@ -0,0 +1,120 @@
<template>
<!--
角色: 🟨 合规 服务机构查看 (manager/executor-orgs)
说明: manager/Orgs.vue 拆出 executor 一支, 仅查看
接口: bizList('org', q)
-->
<div class="page-card manager-executor-orgs">
<div class="breadcrumb">首页 / 服务机构管理</div>
<el-form inline :model="q" class="filter-form">
<el-form-item label="公司名称">
<el-input v-model="q.orgName" placeholder="输入公司名称" clearable />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="q.taxNo" placeholder="输入税号" clearable />
</el-form-item>
<el-form-item label="合作状态">
<el-select v-model="q.status" placeholder="请选择" clearable>
<el-option label="0正常" value="0" />
<el-option label="合作中" value="合作中" />
<el-option label="1禁用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="orgName" label="公司名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="address" label="公司地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="taxNo" label="税号/统一社会信用代码" min-width="180" />
<el-table-column label="合作状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建日期" width="120" align="center">
<template #default="{ row }">{{ row.createTime ? String(row.createTime).slice(0,10) : '' }}</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="view(row)">查看</el-button>
<el-button link type="primary" @click="goPeople(row)">人员管理</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
</div>
<el-dialog v-model="detailOpen" title="服务机构详情" width="640px" v-if="currentRow">
<el-descriptions :column="1" border>
<el-descriptions-item label="公司类型">
<el-tag type="primary">执行方</el-tag>
</el-descriptions-item>
<el-descriptions-item label="公司名称">{{ currentRow.orgName }}</el-descriptions-item>
<el-descriptions-item label="公司地址">{{ currentRow.address }}</el-descriptions-item>
<el-descriptions-item label="税号/统一社会信用代码">{{ currentRow.taxNo }}</el-descriptions-item>
<el-descriptions-item label="合作状态">
<el-tag :type="statusTagType(currentRow.status)">{{ currentRow.status }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建日期">{{ currentRow.createTime ? String(currentRow.createTime).slice(0,10) : '' }}</el-descriptions-item>
<el-descriptions-item label="联系人">{{ currentRow.contactName || '-' }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ currentRow.contactPhone || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="detailOpen=false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { bizList } from '@/api/public'
const router = useRouter()
const q = ref({ orgType: 'executor', orgName: '', taxNo: '', status: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
const currentRow = ref(null)
const detailOpen = ref(false)
function statusTagType(s) {
if (s === '正常' || s === '0' || s === '合作中') return 'success'
if (s === '禁用' || s === '1') return 'danger'
return 'info'
}
async function load() {
loading.value = true
try {
const { data } = await bizList('org', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = data?.rows || []; page.total = data?.total || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() { q.value = { orgType: 'executor', orgName: '', taxNo: '', status: '' }; page.pageNum = 1; load() }
function view(row) { currentRow.value = row; detailOpen.value = true }
function goPeople(row) {
router.push({ path: '/manager/executor-people', query: { orgId: row.orgId, orgName: row.orgName } })
}
onMounted(load)
</script>
<style scoped>
.manager-executor-orgs { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
@@ -0,0 +1,225 @@
<template>
<!--
角色: 🟨 合规 服务机构下人员 (manager/executor-people)
入口: org 表格行内人员管理按钮 (router.push orgId/orgName)
接口: bizList('person', q) / bizUpdate / bizAdd / executorImport / executorImportTemplate
-->
<div class="page-card manager-executor-people">
<div class="breadcrumb">
首页 / 服务机构管理 / 人员管理{{ orgName ? ' (' + orgName + ')' : '' }}
</div>
<el-form inline :model="q" class="filter-form">
<el-form-item label="姓名">
<el-input v-model="q.name" placeholder="姓名" clearable style="width:140px" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="q.phone" placeholder="手机号" clearable style="width:140px" />
</el-form-item>
<el-form-item label="企业名称">
<el-input v-model="q.orgName" placeholder="企业名称" clearable style="width:180px" />
</el-form-item>
<el-form-item label="部门">
<el-input v-model="q.department" placeholder="部门" clearable style="width:140px" />
</el-form-item>
<el-form-item label="职务">
<el-input v-model="q.position" placeholder="职务" clearable style="width:140px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="batch-bar">
<el-button type="primary" @click="goNew">新建人员</el-button>
<el-button @click="openImport">批量导入</el-button>
<span class="filter-tip">*批量导入模板与列表表项一致</span>
</div>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column prop="name" label="姓名" min-width="100" />
<el-table-column prop="phone" label="手机号" min-width="130" />
<el-table-column prop="orgName" label="企业名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" min-width="100" />
<el-table-column prop="position" label="职务" min-width="100" />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status || '正常' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
{{ isDisabled(row) ? '启用' : '禁用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
:total="page.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
<el-dialog v-model="importOpen" title="批量导入" width="520px" :close-on-click-modal="false">
<el-form label-width="100px">
<el-form-item label="选择文件">
<el-upload ref="uploadRef" :auto-upload="false" :limit="1" accept=".xls,.xlsx" :on-change="onFileChange" :on-remove="onFileRemove">
<el-button>选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="">
<el-button link type="primary" @click="downloadTemplate">批量导入模板下载</el-button>
</el-form-item>
<el-form-item><span class="filter-tip">*导入列: 姓名/手机号/所属公司/部门/职务/角色</span></el-form-item>
</el-form>
<template #footer>
<el-button @click="importOpen = false">取消</el-button>
<el-button type="primary" :loading="importing" @click="submitImport">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { bizList, bizUpdate, importPerson, downloadImportTemplate } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
const route = useRoute()
const router = useRouter()
const orgId = computed(() => route.query.orgId ? Number(route.query.orgId) : null)
const orgName = computed(() => route.query.orgName || '')
const q = reactive({ name: '', phone: '', orgName: '', department: '', position: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
function statusTagType(s) {
if (s === '禁用' || s === '1') return 'danger'
return 'success'
}
function isDisabled(row) {
return row.status === '禁用' || row.status === '1'
}
async function load() {
loading.value = true
try {
const params = {
...q,
orgType: 'executor',
...(orgId.value ? { orgId: orgId.value } : {}),
pageNum: page.pageNum,
pageSize: page.pageSize,
}
const { data } = await bizList('person', params)
rows.value = (data && data.rows) || []
page.total = (data && data.total) || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() {
Object.assign(q, { name: '', phone: '', orgName: '', department: '', position: '' })
page.pageNum = 1
load()
}
async function onToggleStatus(row) {
const disabled = isDisabled(row)
const newStatus = disabled ? '正常' : '禁用'
const label = disabled ? '启用' : '禁用'
try {
await ElMessageBox.confirm(
`确定要${label}${row.name}」吗?`,
'确认',
{ type: 'warning' }
)
} catch { return }
try {
await bizUpdate('person', { personId: row.personId, status: newStatus })
ElMessage.success('已' + label)
load()
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
}
// ========== 新建人员 ==========
function goNew() {
router.push({
path: '/manager/executor-people/new',
query: {
...(orgId.value ? { orgId: orgId.value } : {}),
...(orgName.value ? { orgName: orgName.value } : {})
}
})
}
// ========== 批量导入 ==========
const importOpen = ref(false)
const importing = ref(false)
const uploadRef = ref()
const importFile = ref(null)
function openImport() { importFile.value = null; importOpen.value = true }
function onFileChange(file) { importFile.value = file.raw }
function onFileRemove() { importFile.value = null }
async function downloadTemplate() {
try {
const res = await downloadImportTemplate('executor')
const blob = new Blob([res.data], { type: res.data.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '服务机构人员导入模板.xlsx'
a.click()
URL.revokeObjectURL(url)
} catch { ElMessage.error('模板下载失败') }
}
async function submitImport() {
if (!importFile.value) { ElMessage.warning('请先选择文件'); return }
importing.value = true
try {
const res = await importPerson('executor', importFile.value)
const payload = res.data || res
const ok = payload.ok ?? 0
const total = payload.total ?? 0
const failed = (payload.results || []).filter(r => !r.ok)
if (failed.length === 0) {
ElMessage.success(`导入成功 ${ok}/${total}`)
} else {
ElMessageBox.alert(
`成功 ${ok} 条, 失败 ${failed.length} 条:\n` +
failed.map(f => `${f.rowNo}${f.name || ''}: ${f.message}`).join('\n'),
'导入结果', { type: 'warning' }
)
}
importOpen.value = false
load()
} catch (e) {
ElMessage.error(e?.msg || '导入失败')
} finally {
importing.value = false
}
}
onMounted(load)
</script>
<style scoped>
.manager-executor-people { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.filter-tip { color: #909399; font-size: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+43 -51
View File
@@ -1,8 +1,8 @@
<template>
<div class="page-card">
<div class="desc">审核专家注册信息</div>
<div class="page-card manager-experts">
<div class="breadcrumb">首页 / 专家审核</div>
<!-- ========== 筛选区 (按原型 top=174: 姓名/手机号/工作单位/审核状态) ========== -->
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="姓名">
<el-input v-model="q.name" placeholder="" clearable style="width:160px" />
@@ -15,9 +15,10 @@
</el-form-item>
<el-form-item label="审核状态">
<el-select v-model="q.auditStatus" placeholder="请选择" clearable style="width:140px">
<el-option label="待审核" value="0" />
<el-option label="通过" value="1" />
<el-option label="退回" value="2" />
<el-option label="未提交" value="0" />
<el-option label="待审核" value="1" />
<el-option label="通过" value="2" />
<el-option label="拒绝" value="3" />
</el-select>
</el-form-item>
<el-form-item>
@@ -25,10 +26,8 @@
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- ========== 批量按钮区 (按原型 top=226: 新建专家/批量导入/批量审核/批量导入执业证书/批量导入职称证明) ========== -->
<div class="page-card">
<!-- ========== 批量按钮区 ( People.vue 风格一致) ========== -->
<div class="batch-bar">
<el-button type="primary" @click="openAdd">新建专家</el-button>
<el-button @click="openImport">批量导入</el-button>
@@ -65,7 +64,7 @@
</el-table-column>
<el-table-column label="审核状态" width="100" align="center">
<template #default="{ row }">
<span :class="['stage-tag', auditStatusClass(row.auditStatus)]">{{ auditStatusText(row.auditStatus) }}</span>
<audit-status-tag :status="row.auditStatus" />
</template>
</el-table-column>
<el-table-column prop="auditTime" label="审批时间" width="160" align="center">
@@ -77,16 +76,18 @@
<el-button link type="primary" @click="onAudit(row)">审核</el-button>
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link :type="row.status === '1' ? 'danger' : 'success'" @click="row.status === '1' ? onDisable(row) : onEnable(row)">
{{ row.status === '1' ? '禁用' : '恢复' }}
<el-button link :type="row.status === 'N' ? 'success' : 'danger'" @click="row.status === 'N' ? onEnable(row) : onDisable(row)">
{{ row.status === 'N' ? '恢复' : '禁用' }}
</el-button>
<el-button link type="info" v-if="row.auditStatus === '2'" @click="onViewAuditOpinion(row)">退回</el-button>
<el-button link type="info" v-if="row.auditStatus === '3'" @click="onViewAuditOpinion(row)">拒绝</el-button>
<el-button link type="primary" v-if="row.practiceCertUrl || row.titleCertUrl || row.resumeUrl" @click="onDownloadAll(row)">下载</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
</div>
</div>
<!-- ========== Dialog: 新建/编辑专家 (按原型 top=239 确定/取消) ========== -->
@@ -121,8 +122,8 @@
<el-form :model="auditForm" label-width="100px">
<el-form-item label="审核结果">
<el-radio-group v-model="auditForm.result">
<el-radio value="1">通过</el-radio>
<el-radio value="2">退回</el-radio>
<el-radio value="2">通过</el-radio>
<el-radio value="3">拒绝</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见">
@@ -144,8 +145,8 @@
<el-form label-width="100px">
<el-form-item label="审核结果">
<el-radio-group v-model="importAuditResult">
<el-radio value="1">通过</el-radio>
<el-radio value="2">退回</el-radio>
<el-radio value="2">通过</el-radio>
<el-radio value="3">拒绝</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见">
@@ -190,7 +191,7 @@
<el-descriptions-item label="工作单位">{{ currentRow.workUnit }}</el-descriptions-item>
<el-descriptions-item label="科室">{{ currentRow.department }}</el-descriptions-item>
<el-descriptions-item label="职称">{{ currentRow.title }}</el-descriptions-item>
<el-descriptions-item label="审核状态">{{ auditStatusText(currentRow.auditStatus) }}</el-descriptions-item>
<el-descriptions-item label="审核状态"><audit-status-tag :status="currentRow.auditStatus" /></el-descriptions-item>
<el-descriptions-item label="执业证书">
<el-link v-if="currentRow.practiceCertUrl" type="primary" @click="onViewCert(currentRow.practiceCertUrl)">查看证书</el-link>
<span v-else>-</span>
@@ -208,6 +209,7 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { bizList, bizAdd, bizUpdate } from '@/api/public'
import AuditStatusTag from '@/components/AuditStatusTag.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const q = ref({ name: '', phone: '', workUnit: '', auditStatus: '' })
@@ -233,17 +235,7 @@ function reset() {
}
function onSel(rs) { selection.value = rs }
// ========== 工具: 审核状态显示/颜色 ==========
function auditStatusText(s) {
if (s === '1') return '通过'
if (s === '2') return '退回'
return '待审核'
}
function auditStatusClass(s) {
if (s === '1') return 'stage-done'
if (s === '2') return 'stage-reject'
return 'stage-reviewing'
}
// auditStatusText / auditStatusClass 已删除: 改用 AuditStatusTag 组件
function fmtTime(d) {
if (!d) return ''
const dt = new Date(d)
@@ -285,7 +277,7 @@ async function submitEdit() {
await bizUpdate('expert', { ...editForm })
ElMessage.success('修改成功')
} else {
await bizAdd('expert', { ...editForm, auditStatus: '1', auditBy: 'admin', auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ') })
await bizAdd('expert', { ...editForm, auditStatus: '2', auditBy: 'admin', auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ') })
ElMessage.success('新建成功')
}
editOpen.value = false
@@ -296,12 +288,13 @@ async function submitEdit() {
// ========== 审核 dialog ==========
const auditOpen = ref(false)
const auditForm = reactive({ expertId: '', name: '', phone: '', result: '1', opinion: '' })
// result: '2'=通过, '3'=拒绝 (BizAuditStatusEnum: 0未提交 1待审核 2通过 3拒绝)
const auditForm = reactive({ expertId: '', name: '', phone: '', result: '2', opinion: '' })
function onAudit(row) {
auditForm.expertId = row.expertId
auditForm.name = row.name
auditForm.phone = row.phone
auditForm.result = '1'
auditForm.result = '2'
auditForm.opinion = ''
auditOpen.value = true
}
@@ -315,7 +308,7 @@ async function submitAudit() {
auditBy: 'admin', // 后端应取登录用户
auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ')
})
ElMessage.success(auditForm.result === '1' ? '审核通过' : '已退回')
ElMessage.success(auditForm.result === '2' ? '审核通过' : '已拒绝')
auditOpen.value = false
load()
} catch { ElMessage.error('审核失败') }
@@ -329,7 +322,7 @@ async function onBatchAudit() {
submitting.value = true
let ok = 0
for (const r of selection.value) {
try { await bizUpdate('expert', { expertId: r.expertId, auditStatus: '1', auditOpinion: '批量通过', auditBy: 'admin', auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ') }); ok++ } catch {}
try { await bizUpdate('expert', { expertId: r.expertId, auditStatus: '2', auditOpinion: '批量通过', auditBy: 'admin', auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ') }); ok++ } catch {}
}
submitting.value = false
ElMessage.success(`批量审核通过 ${ok}/${selection.value.length}`)
@@ -339,10 +332,10 @@ async function onBatchAudit() {
// ========== 禁用/恢复 ==========
async function onDisable(row) {
try { await bizUpdate('expert', { expertId: row.expertId, status: '0' }); ElMessage.success('已禁用'); load() } catch { ElMessage.error('禁用失败') }
try { await bizUpdate('expert', { expertId: row.expertId, status: 'N' }); ElMessage.success('已禁用'); load() } catch { ElMessage.error('禁用失败') }
}
async function onEnable(row) {
try { await bizUpdate('expert', { expertId: row.expertId, status: '1' }); ElMessage.success('已恢复'); load() } catch { ElMessage.error('恢复失败') }
try { await bizUpdate('expert', { expertId: row.expertId, status: 'Y' }); ElMessage.success('已恢复'); load() } catch { ElMessage.error('恢复失败') }
}
// ========== 查看详情 ==========
@@ -350,11 +343,11 @@ const viewOpen = ref(false)
const currentRow = ref(null)
function onView(row) { currentRow.value = row; viewOpen.value = true }
// ========== 已退回: 查看退回意见 ==========
// ========== 已拒绝: 查看拒绝意见 ==========
function onViewAuditOpinion(row) {
ElMessageBox.alert(
`退回时间: ${fmtTime(row.auditTime) || '-'}\n审批人: ${row.auditBy || '-'}\n退回意见: ${row.auditOpinion || '(无)'}`,
`专家 ${row.name} 审核已退回`,
`拒绝时间: ${fmtTime(row.auditTime) || '-'}\n审批人: ${row.auditBy || '-'}\n拒绝意见: ${row.auditOpinion || '(无)'}`,
`专家 ${row.name} 审核已拒绝`,
{ type: 'warning', confirmButtonText: '知道了' }
).catch(() => {})
}
@@ -382,12 +375,12 @@ function onDownloadAll(row) {
// ========== 批量导入 dialog ==========
const importOpen = ref(false)
const importAuditResult = ref('1')
const importAuditResult = ref('2')
const importAuditOpinion = ref('')
const importFileUrl = ref('')
const importing = ref(false)
function openImport() {
importAuditResult.value = '1'
importAuditResult.value = '2'
importAuditOpinion.value = ''
importFileUrl.value = ''
importOpen.value = true
@@ -400,7 +393,7 @@ async function submitImport() {
importing.value = true
await new Promise(r => setTimeout(r, 500))
importing.value = false
ElMessage.info(`已提交批量导入任务 (审核结果: ${importAuditResult.value === '1' ? '通过' : '退回'}) — 待后端实现`)
ElMessage.info(`已提交批量导入任务 (审核结果: ${importAuditResult.value === '2' ? '通过' : '拒绝'}) — 待后端实现`)
importOpen.value = false
}
@@ -434,13 +427,12 @@ onMounted(load)
</script>
<style scoped>
.page-card { background: #fff; border: 1px solid #ebeef5; border-radius: 6px; padding: 16px; margin-bottom: 12px; }
.desc { color: #606266; font-size: 13px; margin-bottom: 12px; }
.filter-form .el-form-item { margin-right: 12px; margin-bottom: 8px; }
.batch-bar { display: flex; gap: 8px; flex-wrap: wrap; }
/* 整页面板 (与 People.vue 风格一致) */
.manager-experts { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; flex-wrap: wrap; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
.stage-tag { display: inline-block; padding: 2px 10px; border-radius: 3px; font-size: 12px; border: 1px solid; }
.stage-done { color: #67c23a; border-color: #c2e7b0; background: #f0f9eb; }
.stage-reviewing { color: #e6a23c; border-color: #f5dab1; background: #fdf6ec; }
.stage-reject { color: #f56c6c; border-color: #fbc4c4; background: #fef0f0; }
/* stage-* CSS 已删除: 改用 AuditStatusTag 组件 (el-tag 自带颜色) */
</style>
+7 -4
View File
@@ -1,9 +1,9 @@
<template>
<div class="page-card manager-meeting-new">
<h2 style="margin-bottom:16px">新建会议</h2>
<div class="breadcrumb">首页 / 会议管理 / 新建会议</div>
<p class="page-sub" v-if="preselectProjectId">已选择项目: {{ preselectProjectId }}</p>
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" style="max-width:700px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" class="meeting-form">
<el-form-item label="项目编号" prop="projectNo">
<el-input v-model="form.projectNo" :disabled="!!preselectProjectId" placeholder="项目编号" />
</el-form-item>
@@ -100,13 +100,16 @@ onMounted(async () => {
if (preselectProjectId) {
try {
const r = await bizGet('project', preselectProjectId)
form.projectNo = r.data?.projectNo || ''
form.projectNo = (r.data && r.data.projectNo) || ''
} catch (e) {}
}
})
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-meeting-new { padding: 16px; }
.page-sub { color: #909399; font-size: 13px; margin-bottom: 12px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.page-sub { color: #909399; font-size: 13px; margin-bottom: 16px; }
.meeting-form { max-width: 700px; }
</style>
+87 -100
View File
@@ -1,89 +1,80 @@
<template>
<div class="manager-meetings">
<!-- ========== 筛选区 (严格按 manager 角色原型 会议管理_1.html 第一行 6 + 第二行 3 ) ========== -->
<div class="page-card">
<el-form inline :model="q" class="filter-form">
<el-form-item label="项目编号">
<el-input v-model="q.projectNo" placeholder="" clearable style="width:140px" />
</el-form-item>
<el-form-item label="会议ID">
<el-input v-model="q.meetingId" placeholder="" clearable style="width:140px" />
</el-form-item>
<el-form-item label="会议名称">
<el-input v-model="q.meetingName" placeholder="" clearable style="width:160px" />
</el-form-item>
<el-form-item label="第?期">
<el-input v-model="q.periodNo" placeholder="" clearable style="width:80px" />
</el-form-item>
<el-form-item label="会议时间">
<el-date-picker v-model="q.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:170px" />
<span style="margin:0 4px"></span>
<el-date-picker v-model="q.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:170px" />
</el-form-item>
<el-form-item label="项目形式">
<el-select v-model="q.projectForm" placeholder="请选择" clearable style="width:140px">
<el-option label="线上" value="线上" />
<el-option label="线下" value="线下" />
<el-option label="线上+线下" value="线上+线下" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="当前阶段">
<el-select v-model="q.currentStage" placeholder="请选择" clearable style="width:140px">
<el-option label="未执行" value="未执行" />
<el-option label="执行中" value="执行中" />
<el-option label="已执行" value="已执行" />
<el-option label="已完结" value="已完结" />
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="q.remark" placeholder="" clearable style="width:140px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="page-card manager-meetings">
<div class="breadcrumb">首页 / 会议管理</div>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="" clearable style="width:140px" /></el-form-item>
<el-form-item label="会议ID"><el-input v-model="q.meetingId" placeholder="" clearable style="width:140px" /></el-form-item>
<el-form-item label="会议名称"><el-input v-model="q.meetingName" placeholder="" clearable style="width:160px" /></el-form-item>
<el-form-item label="第?期"><el-input v-model="q.periodNo" placeholder="" clearable style="width:80px" /></el-form-item>
<el-form-item label="会议时间">
<el-date-picker v-model="q.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:170px" />
<span style="margin:0 4px"></span>
<el-date-picker v-model="q.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:170px" />
</el-form-item>
<el-form-item label="项目形式">
<el-select v-model="q.projectForm" placeholder="请选择" clearable style="width:140px">
<el-option label="线上" value="线上" />
<el-option label="线下" value="线下" />
<el-option label="线上+线下" value="线上+线下" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="当前阶段">
<el-select v-model="q.currentStage" placeholder="请选择" clearable style="width:140px">
<el-option label="未执行" value="未执行" />
<el-option label="执行中" value="执行中" />
<el-option label="已执行" value="已执行" />
<el-option label="已完结" value="已完结" />
</el-select>
</el-form-item>
<el-form-item label="备注"><el-input v-model="q.remark" placeholder="" clearable style="width:140px" /></el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 批量提交按钮 (manager 角色专有) ========== -->
<div class="batch-bar">
<el-button type="primary" plain :disabled="!selectedIds.length" @click="onBatchSubmit">批量提交</el-button>
<span v-if="selectedIds.length" class="filter-tip">已选 {{ selectedIds.length }} </span>
</div>
<!-- ========== 表格区 ( manager 角色原型 会议管理_1.html: 操作列 4 按钮 查看/修改/提交/复制) ========== -->
<div class="page-card">
<!-- 批量提交按钮 (manager 角色才有) -->
<div class="batch-bar" v-if="selectedIds.length">
<el-button type="primary" plain @click="onBatchSubmit" :disabled="!selectedIds.length">批量提交</el-button>
<span class="selected-tip">已选 {{ selectedIds.length }} </span>
</div>
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelectionChange">
<el-table-column type="selection" width="44" />
<el-table-column prop="projectNo" label="项目编号" width="130" fixed />
<el-table-column prop="meetingId" label="会议ID" width="120" align="center" />
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
<el-table-column prop="meetingName" label="会议名称" min-width="180" show-overflow-tooltip />
<el-table-column label="会议开始时间" width="160" align="center">
<template #default="{ row }">{{ fmtTime(row.startTime) }}</template>
</el-table-column>
<el-table-column label="会议结束时间" width="160" align="center">
<template #default="{ row }">{{ fmtTime(row.endTime) }}</template>
</el-table-column>
<el-table-column prop="totalPeriods" label="总期数" width="80" align="center" />
<el-table-column label="期数" width="80" align="center">
<template #default="{ row }"> {{ row.periodNo || 0 }} </template>
</el-table-column>
<el-table-column prop="currentStage" label="当前阶段" width="100" align="center">
<template #default="{ row }">
<span :class="['stage-tag', 'stage-' + stageClass(row.currentStage)]">{{ row.currentStage }}</span>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
<el-button link type="primary" @click="onSubmit(row)">提交</el-button>
<el-button link type="primary" @click="onCopy(row)">复制</el-button>
</template>
</el-table-column>
</el-table>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelectionChange">
<el-table-column type="selection" width="44" />
<el-table-column prop="projectNo" label="项目编号" width="130" fixed />
<el-table-column prop="meetingId" label="会议ID" width="120" align="center" />
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
<el-table-column prop="meetingName" label="会议名称" min-width="180" show-overflow-tooltip />
<el-table-column label="会议开始时间" width="160" align="center">
<template #default="{ row }">{{ fmtTime(row.startTime) }}</template>
</el-table-column>
<el-table-column label="会议结束时间" width="160" align="center">
<template #default="{ row }">{{ fmtTime(row.endTime) }}</template>
</el-table-column>
<el-table-column prop="totalPeriods" label="总期数" width="80" align="center" />
<el-table-column label="期数" width="80" align="center">
<template #default="{ row }"> {{ row.periodNo || 0 }} </template>
</el-table-column>
<el-table-column prop="currentStage" label="当前阶段" width="100" align="center">
<template #default="{ row }">
<span :class="['stage-tag', 'stage-' + stageClass(row.currentStage)]">{{ row.currentStage }}</span>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
<el-button link type="primary" @click="onSubmit(row)">提交</el-button>
<el-button link type="primary" @click="onCopy(row)">复制</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
@@ -289,19 +280,15 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-meetings { padding: 16px; }
.page-card {
background: #fff;
border-radius: 6px;
padding: 16px;
border: 1px solid #ebeef5;
margin-bottom: 12px;
}
.filter-form .el-form-item { margin-right: 12px; margin-bottom: 12px; }
.batch-bar { margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
.batch-bar .selected-tip { color: #909399; font-size: 13px; margin-left: 8px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.filter-tip { color: #909399; font-size: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
/* 当前阶段 tag 颜色 */
/* 当前阶段 tag 颜色 (业务专用, 其它页没有) */
.stage-tag {
display: inline-block;
padding: 2px 10px;
@@ -309,11 +296,11 @@ onMounted(load)
font-size: 12px;
border: 1px solid;
}
.stage-pending { color: #909399; border-color: #dcdfe6; background: #f5f7fa; }
.stage-running { color: #409eff; border-color: #b3d8ff; background: #ecf5ff; }
.stage-done { color: #67c23a; border-color: #c2e7b0; background: #f0f9eb; }
.stage-pending { color: #909399; border-color: #dcdfe6; background: #f5f7fa; }
.stage-running { color: var(--brand-primary); border-color: #b3c5e0; background: #eef1f8; }
.stage-done { color: #67c23a; border-color: #c2e7b0; background: #f0f9eb; }
.stage-reviewing { color: #e6a23c; border-color: #f5dab1; background: #fdf6ec; }
.stage-waiting { color: #f56c6c; border-color: #fbc4c4; background: #fef0f0; }
.stage-frozen { color: #909399; border-color: #dcdfe6; background: #f5f7fa; opacity: 0.6; }
.stage-default { color: #606266; border-color: #dcdfe6; background: #fafafa; }
.stage-waiting { color: #ff4d4f; border-color: #fbc4c4; background: #fef0f0; }
.stage-frozen { color: #909399; border-color: #dcdfe6; background: #f5f7fa; opacity: 0.6; }
.stage-default { color: #606266; border-color: #dcdfe6; background: #fafafa; }
</style>
+63 -77
View File
@@ -4,69 +4,66 @@
重构说明: manager/SupportUnits.vue (仅看支持单位) 改为可看所有公司
接口: bizList('org', q) / bizUpdate
-->
<div class="manager-orgs">
<div class="page-card">
<div class="page-title">公司查看</div>
<div class="desc">公司管理 &gt; 公司查看 (赞助方 + 执行方)</div>
<el-form inline :model="q">
<el-form-item label="公司类型:">
<el-select v-model="q.orgType" placeholder="全部" clearable style="width:140px">
<el-option label="赞助方" value="sponsor" />
<el-option label="执行方" value="executor" />
</el-select>
</el-form-item>
<el-form-item label="公司名称:">
<el-input v-model="q.orgName" placeholder="输入公司名称" clearable />
</el-form-item>
<el-form-item label="税号/统一社会信用代码:">
<el-input v-model="q.taxNo" placeholder="输入税号" clearable />
</el-form-item>
<el-form-item label="合作状态:">
<el-select v-model="q.status" placeholder="请选择" clearable>
<el-option label="正常/合作中" value="0" />
<el-option label="合作中" value="合作中" />
<el-option label="禁用" value="禁用" />
</el-select>
</el-form-item>
<el-form-item>
<el-button class="action-btn" @click="load">查找</el-button>
<el-button class="action-btn secondary" @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="page-card manager-orgs">
<div class="breadcrumb">首页 / 公司管理</div>
<div class="page-card">
<div class="page-toolbar">
<el-button class="action-btn secondary" @click="goBack">返回</el-button>
</div>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column label="类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.orgType === 'sponsor' ? 'success' : 'primary'" disable-transitions>
{{ row.orgType === 'sponsor' ? '赞助方' : '执行方' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="orgName" label="公司名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="address" label="公司地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="taxNo" label="税号/统一社会信用代码" min-width="180" />
<el-table-column label="合作状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建日期" width="120" align="center">
<template #default="{ row }">{{ row.createTime ? String(row.createTime).slice(0,10) : '' }}</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="view(row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="公司类型">
<el-select v-model="q.orgType" placeholder="全部" clearable style="width:140px">
<el-option label="赞助方" value="sponsor" />
<el-option label="执行方" value="executor" />
</el-select>
</el-form-item>
<el-form-item label="公司名称">
<el-input v-model="q.orgName" placeholder="输入公司名称" clearable />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="q.taxNo" placeholder="输入税号" clearable />
</el-form-item>
<el-form-item label="合作状态">
<el-select v-model="q.status" placeholder="请选择" clearable>
<el-option label="正常/合作中" value="0" />
<el-option label="合作中" value="合作中" />
<el-option label="禁用" value="禁用" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column label="类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.orgType === 'sponsor' ? 'success' : 'primary'" disable-transitions>
{{ row.orgType === 'sponsor' ? '赞助方' : '执行方' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="orgName" label="公司名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="address" label="公司地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="taxNo" label="税号/统一社会信用代码" min-width="180" />
<el-table-column label="合作状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建日期" width="120" align="center">
<template #default="{ row }">{{ row.createTime ? String(row.createTime).slice(0,10) : '' }}</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="view(row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
</div>
@@ -129,20 +126,9 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-orgs { padding: 16px; }
.page-card { background: #fff; border-radius: 6px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.page-title { font-size: 20px; font-weight: 600; margin-bottom: 4px; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
.desc { color: #909399; font-size: 13px; margin-bottom: 12px; }
.page-toolbar { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
.page-toolbar :deep(.action-btn) {
height: 36px; padding: 0 20px; font-size: 14px; border-radius: 4px;
background: var(--brand-primary); color: #fff; border: 1px solid var(--brand-primary);
display: inline-flex; align-items: center; justify-content: center; white-space: nowrap;
transition: opacity .15s; margin-left: 0;
}
.page-toolbar :deep(.action-btn:hover) { opacity: .85; }
.page-toolbar :deep(.action-btn.secondary) {
background: #fff; color: var(--brand-primary); border: 1px solid var(--brand-primary);
}
.page-toolbar :deep(.action-btn.secondary:hover) { background: rgba(30,58,138,.06); }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+112 -112
View File
@@ -1,95 +1,92 @@
<template>
<div class="manager-plans">
<div class="page-card">
<div class="page-title">项目策划方案管理</div>
<div class="desc">审核 / 修改 / 结算 项目策划方案</div>
<el-form inline :model="q">
<!-- 按原型 top=171-178 的筛选区: 项目编号(52) / 项目编号(141) / 策划方案名称(254) / 项目方向(519) / 策划方案名称(591) / 状态(939) / 备注(1138) / 备注(1177) -->
<el-form-item label="项目编号:"><el-input v-model="q.projectNo" placeholder="输入项目编号" clearable /></el-form-item>
<el-form-item label="策划方案名称:"><el-input v-model="q.planName" placeholder="输入策划方案名称" clearable /></el-form-item>
<el-form-item label="项目方向:">
<el-select v-model="q.planDirection" placeholder="请选择" clearable>
<el-option label="专项计划一" value="专项计划一" /><el-option label="专项计划二" value="专项计划二" />
</el-select>
</el-form-item>
<el-form-item label="项目类别:">
<el-select v-model="q.planCategory" placeholder="请选择" clearable>
<el-option label="共识" value="共识" /><el-option label="对外交流" value="对外交流" />
<el-option label="学术项目" value="学术项目" /><el-option label="科研项目" value="科研项目" />
<el-option label="指南" value="指南" /><el-option label="会议项目" value="会议项目" />
</el-select>
</el-form-item>
<el-form-item label="状态:">
<el-select v-model="q.status" placeholder="请选择" clearable>
<el-option label="待审核" value="0" /><el-option label="审核通过" value="1" /><el-option label="已退回" value="2" />
</el-select>
</el-form-item>
<el-form-item label="是否结算:">
<el-select v-model="q.isSettled" placeholder="请选择" clearable>
<el-option label="已结算" value="1" /><el-option label="未结算" value="0" />
</el-select>
</el-form-item>
<el-form-item label="备注:"><el-input v-model="q.remark" placeholder="输入备注" clearable /></el-form-item>
<el-form-item>
<el-button class="action-btn" @click="load">查找</el-button>
<el-button class="action-btn secondary" @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="filter-tip">*批量审核策划案名称投稿ID审核结果审核意见只能审核不通过</div>
<div class="page-card manager-plans">
<div class="breadcrumb">首页 / 项目策划方案</div>
<div class="hint-list">
<p>*批量审核策划案名称投稿ID审核结果审核意见只能审核不通过</p>
</div>
<div class="page-card">
<!-- 批量按钮 (按原型 left: 批量审核 266 / 批量结算 432) -->
<div class="page-toolbar">
<el-button class="action-btn" :disabled="!selection.length" @click="batchAudit">
批量审核<span v-if="selection.length" class="badge">{{ selection.length }}</span>
</el-button>
<el-button class="action-btn secondary" :disabled="!selection.length" @click="batchSettle">
批量结算<span v-if="selection.length" class="badge">{{ selection.length }}</span>
</el-button>
</div>
<!-- 列头顺序严格按原型 top=294-300 left: 策划方案名称 342 / 项目方向 569 / 项目类别 739 / 项目形式 864 / 设计文件 999 / 状态 1133 / 是否结算 1246 / 项目编号 1348 / 备注 1492 / 操作 1640 -->
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSel">
<el-table-column type="selection" width="44" />
<el-table-column prop="planName" label="策划方案名称" min-width="200" fixed show-overflow-tooltip />
<el-table-column prop="planDirection" label="项目方向" min-width="220" show-overflow-tooltip />
<el-table-column prop="planCategory" label="项目类别" width="100" align="center" />
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
<el-table-column label="设计文件" width="160" align="center">
<template #default="{ row }">
<template v-if="row.designFileUrl">
<el-button link type="primary" @click="onFile(row)">查看</el-button>
<el-button link type="primary" @click="downloadFile(row)">下载</el-button>
</template>
<span v-else style="color:#c0c4cc">-</span>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="输入项目编号" clearable /></el-form-item>
<el-form-item label="策划方案名称"><el-input v-model="q.planName" placeholder="输入策划方案名称" clearable /></el-form-item>
<el-form-item label="项目方向">
<el-select v-model="q.planDirection" placeholder="请选择" clearable>
<el-option label="专项计划一" value="专项计划一" /><el-option label="专项计划二" value="专项计划二" />
</el-select>
</el-form-item>
<el-form-item label="项目类别">
<el-select v-model="q.planCategory" placeholder="请选择" clearable>
<el-option label="共识" value="共识" /><el-option label="对外交流" value="对外交流" />
<el-option label="学术项目" value="学术项目" /><el-option label="科研项目" value="科研项目" />
<el-option label="指南" value="指南" /><el-option label="会议项目" value="会议项目" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="请选择" clearable>
<el-option label="待审核" value="0" /><el-option label="审核通过" value="1" /><el-option label="已退回" value="2" />
</el-select>
</el-form-item>
<el-form-item label="是否结算">
<el-select v-model="q.isSettled" placeholder="请选择" clearable>
<el-option label="已结算" value="1" /><el-option label="未结算" value="0" />
</el-select>
</el-form-item>
<el-form-item label="备注"><el-input v-model="q.remark" placeholder="输入备注" clearable /></el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 批量按钮 ( People.vue 风格一致) ========== -->
<div class="batch-bar">
<el-button type="primary" :disabled="!selection.length" @click="batchAudit">批量审核</el-button>
<el-button type="primary" :disabled="!selection.length" @click="batchSettle">批量结算</el-button>
<span v-if="selection.length" class="filter-tip">已选 {{ selection.length }} </span>
</div>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSel">
<el-table-column type="selection" width="44" />
<el-table-column prop="planName" label="策划方案名称" min-width="200" fixed show-overflow-tooltip />
<el-table-column prop="planDirection" label="项目方向" min-width="220" show-overflow-tooltip />
<el-table-column prop="planCategory" label="项目类别" width="100" align="center" />
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
<el-table-column label="设计文件" width="160" align="center">
<template #default="{ row }">
<template v-if="row.designFileUrl">
<el-button link type="primary" @click="onFile(row)">查看</el-button>
<el-button link type="primary" @click="downloadFile(row)">下载</el-button>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.status==='1' ? 'success' : row.status==='2' ? 'danger' : 'warning'">
{{ row.status==='1' ? '审核通过' : row.status==='2' ? '已退回' : '待审核' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="是否结算" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.isSettled==='1' ? 'success' : 'warning'">
{{ row.isSettled==='1' ? '已结算' : '未结算' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="projectNo" label="项目编号" width="140" align="center" />
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
<!-- 操作列按原型 left 顺序: 审核 1586 / 修改 1640 / 结算 1693 -->
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onAudit(row)">审核</el-button>
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
<el-button link type="primary" @click="onSettle(row)" v-if="row.isSettled!=='1'">结算</el-button>
<el-button link type="primary" v-if="row.designFileUrl" @click="downloadFile(row)">下载</el-button>
</template>
</el-table-column>
</el-table>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.status==='1' ? 'success' : row.status==='2' ? 'danger' : 'warning'">
{{ row.status==='1' ? '审核通过' : row.status==='2' ? '已退回' : '待审核' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="是否结算" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.isSettled==='1' ? 'success' : 'warning'">
{{ row.isSettled==='1' ? '已结算' : '未结算' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="projectNo" label="项目编号" width="140" align="center" />
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onAudit(row)">审核</el-button>
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
<el-button link type="primary" @click="onSettle(row)" v-if="row.isSettled!=='1'">结算</el-button>
<el-button link type="primary" v-if="row.designFileUrl" @click="downloadFile(row)">下载</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
</div>
@@ -282,31 +279,34 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-plans { padding: 16px; }
.page-card { background: #fff; border-radius: 6px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.page-title { font-size: 20px; font-weight: 600; margin-bottom: 4px; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
.desc { color: #909399; font-size: 13px; margin-bottom: 12px; }
.filter-tip { color: #ff4d4f; font-size: 12px; margin-top: 8px; }
.page-toolbar { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
.page-toolbar :deep(.action-btn) {
height: 36px; padding: 0 20px; font-size: 14px; border-radius: 4px;
background: var(--brand-primary); color: #fff; border: 1px solid var(--brand-primary);
display: inline-flex; align-items: center; justify-content: center; white-space: nowrap;
transition: opacity .15s; margin-left: 0;
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.page-title {
font-size: 22px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 6px;
padding-left: 10px;
border-left: 3px solid var(--brand-primary);
}
.page-toolbar :deep(.action-btn:hover) { opacity: .85; }
.page-toolbar :deep(.action-btn.secondary) {
background: #fff; color: var(--brand-primary); border: 1px solid var(--brand-primary);
.page-sub { font-size: 13px; color: #909399; margin: 0 0 16px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.filter-tip { color: #909399; font-size: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
/* 红字提示 (与 People.vue 一致) */
.hint-list {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 12px 20px;
margin-bottom: 16px;
font-size: 12px;
color: #ff4d4f;
line-height: 1.8;
}
.page-toolbar :deep(.action-btn.secondary:hover) { background: rgba(30,58,138,.06); }
.page-toolbar :deep(.action-btn.is-disabled),
.page-toolbar :deep(.action-btn:disabled) { background: #f5f7fa; color: #c0c4cc; border-color: #e4e7ed; opacity: 1; cursor: not-allowed; }
.page-toolbar :deep(.action-btn.secondary.is-disabled),
.page-toolbar :deep(.action-btn.secondary:disabled) { background: #fff; color: #c0c4cc; border-color: #e4e7ed; }
.page-toolbar :deep(.action-btn .badge) {
display: inline-block; margin-left: 6px; padding: 0 6px; min-width: 18px; height: 18px;
line-height: 18px; font-size: 11px; text-align: center;
background: #fff; color: var(--brand-primary); border-radius: 9px;
}
.page-toolbar :deep(.action-btn.secondary .badge) { background: var(--brand-primary); color: #fff; }
.hint-list p { margin-bottom: 4px; }
.hint-list p:last-child { margin-bottom: 0; }
</style>
+92 -93
View File
@@ -1,89 +1,84 @@
<template>
<div class="page-card">
<!-- 面包屑 ( sponsor 端一致: 首页 / XXX / 当前页) -->
<!-- 面包屑 -->
<div class="breadcrumb">
首页 / 项目管理 / <span class="current">{{ isEdit ? '编辑项目' : '新建项目' }}</span>
</div>
<!-- 第一区块项目信息 -->
<el-card shadow="never" class="new-card">
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" class="project-form">
<!-- ========== 第一区块项目信息 ========== -->
<div class="new-card-title">项目信息</div>
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="请输入项目名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目编号" prop="projectNo">
<el-input v-model="form.projectNo" placeholder="请输入项目编号" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="项目形式" prop="projectForm">
<el-select v-model="form.projectForm" placeholder="请选择" style="width:100%">
<el-option label="线" value="线" />
<el-option label="线下" value="线下" />
<el-option label="线上+线下" value="线上+线下" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="总场次/总期数" prop="totalSessions">
<el-input-number v-model="form.totalSessions" :min="1" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="总金额(元)" prop="totalAmount">
<el-input-number v-model="form.totalAmount" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="管理费(元)">
<el-input-number v-model="form.manageFee" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker v-model="form.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间" prop="endTime">
<el-date-picker v-model="form.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<!-- 支持单位 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="提交截止(天)">
<el-input-number v-model="form.submitDeadlineDays" :min="0" controls-position="right" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="支持单位">
<el-select v-model="form.orgId" placeholder="请选择赞助公司" filterable clearable style="width:100%" @change="onSupporterPick">
<el-option v-for="u in supporterOptions" :key="u.userId"
:label="`${u.userName}${u.nickName ? ' (' + u.nickName + ')' : ''}`"
:value="u.userId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="请输入项目名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目编号" prop="projectNo">
<el-input v-model="form.projectNo" placeholder="请输入项目编号" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="项目形式" prop="projectForm">
<el-select v-model="form.projectForm" placeholder="请选择" style="width:100%">
<el-option label="线上" value="线上" />
<el-option label="线" value="线" />
<el-option label="线上+线下" value="线上+线下" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="总场次/总期数" prop="totalSessions">
<el-input-number v-model="form.totalSessions" :min="1" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="总金额(元)" prop="totalAmount">
<el-input-number v-model="form.totalAmount" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="管理费(元)">
<el-input-number v-model="form.manageFee" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker v-model="form.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间" prop="endTime">
<el-date-picker v-model="form.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="提交截止(天)">
<el-input-number v-model="form.submitDeadlineDays" :min="0" controls-position="right" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="支持单位">
<el-select v-model="form.orgId" placeholder="请选择赞助公司" filterable clearable style="width:100%" @change="onSupporterPick">
<el-option v-for="u in supporterOptions" :key="u.userId"
:label="`${u.userName}${u.nickName ? ' (' + u.nickName + ')' : ''}`"
:value="u.userId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 第二区块角色劳务设置 -->
<el-card shadow="never" class="new-card">
<!-- ========== 第二区块角色劳务设置 ========== -->
<div class="new-card-title">角色劳务设置</div>
<div class="hint-text">
<p>*角色劳务设置针对该项目所有会议生效</p>
@@ -100,17 +95,14 @@
</el-select>
<el-input v-if="r.role === '其他'" v-model="r.customName" placeholder="角色名" class="role-custom" />
<el-input-number v-model="r.amount" :min="0" :precision="2" placeholder="劳务金额" class="role-amount" />
<!-- 行尾按钮: 每行末尾 [增加角色] + [删除本行] (不靠右, 紧贴字段) -->
<div class="row-actions">
<el-button link type="primary" @click="addRoleRow">增加角色</el-button>
<el-button link type="danger" :disabled="form.roleRows.length<=1" @click="removeRoleRowAt(idx)">删除角色</el-button>
</div>
</div>
<p class="hint-text">*当选择其他时,需手动填写角色名称,显示填写框</p>
</el-card>
<!-- 第三区块公告文件 -->
<el-card shadow="never" class="new-card">
<!-- ========== 第三区块公告文件 ========== -->
<div class="new-card-title">公告文件</div>
<p class="hint-text">*勾选公告类型后上传对应文件发布公告时可同时发布多个类型</p>
<div class="notice-list">
@@ -143,9 +135,9 @@
</div>
</div>
<p class="hint-text">*名称规则:公告文件类型+取项目编号的后两段+项目名称</p>
</el-card>
</el-form>
<!-- 底部按钮 (与 sponsor 工具栏风格一致) -->
<!-- 底部按钮 -->
<div class="form-actions">
<el-button @click="confirmCancel">取消</el-button>
<el-button type="warning" :loading="submitting" @click="submit('publish')">保存并发布公告</el-button>
@@ -183,7 +175,7 @@ const form = reactive({
projectName: '', projectNo: '', projectForm: '',
totalSessions: 0, totalAmount: 0, manageFee: 0,
startTime: '', endTime: '',
// 赞助公司 (TODO: 下拉源应改为 biz_org,orgType='sponsor', 当前是 sys_user)
// 赞助公司 (TODO 后续: 下拉源应改为 biz_org,orgType='sponsor' 列表, 当前是 sys_user)
orgId: null,
orgName: '',
// 提交截止(天)
@@ -286,7 +278,7 @@ async function loadSupporters(query = '') {
const params = { roleType: 'sponsor', pageNum: 1, pageSize: 50 }
if (query) params.userName = query
const r = await listSupporters(params)
supporterOptions.value = r.rows || r.data || []
supporterOptions.value = (r.data && r.data.rows) || r.rows || []
} catch (e) { supporterOptions.value = [] }
}
@@ -454,18 +446,25 @@ async function submit(mode = 'save') {
</script>
<style scoped>
/* 1:1 抄 sponsor/SponsorProjects.vue */
/* 整页面板 (与 People.vue 风格一致, 1 层) */
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.breadcrumb .current { color: #262626; font-weight: 500; }
/* 卡片分块 (与 sponsor 一致: 左边竖线标识 + 标题) */
.new-card { margin-bottom: 14px; border: 1px solid #f0f0f0; border-radius: 4px; }
.new-card :deep(.el-card__body) { padding: 16px 20px; }
/* 表单 (整页一个 form) */
.project-form { max-width: 100%; }
/* 章节标题 (替换原 .new-card 容器, 用分隔线分割 3 块) */
.new-card-title {
font-size: 14px; font-weight: 600; color: #262626; margin-bottom: 12px;
padding-left: 8px; border-left: 3px solid var(--brand-primary, #1890ff); line-height: 1;
font-size: 14px;
font-weight: 600;
color: #262626;
margin: 16px 0 12px;
padding-left: 8px;
border-left: 3px solid var(--brand-primary);
line-height: 1;
}
.new-card-title:first-child { margin-top: 0; }
.hint-text { font-size: 12px; color: #909399; margin: 8px 0; line-height: 1.6; }
.hint-text p { margin-bottom: 4px; }
@@ -499,6 +498,6 @@ async function submit(mode = 'save') {
.notice-actions .el-button { padding: 2px 6px; font-size: 12px; }
.notice-disabled { flex: 1; color: #c0c4cc; font-size: 13px; padding: 8px 0; }
/* 底部按钮 (居中, 与 sponsor 风格一致) */
/* 底部按钮 */
.form-actions { display: flex; justify-content: center; gap: 16px; padding: 20px 0 4px; border-top: 1px solid #f0f0f0; margin-top: 8px; }
</style>
+120
View File
@@ -0,0 +1,120 @@
<template>
<!--
角色: 🟨 合规 支持单位查看 (manager/sponsor-orgs)
说明: manager/Orgs.vue 拆出 sponsor 一支, 仅查看
接口: bizList('org', q)
-->
<div class="page-card manager-sponsor-orgs">
<div class="breadcrumb">首页 / 支持单位管理</div>
<el-form inline :model="q" class="filter-form">
<el-form-item label="公司名称">
<el-input v-model="q.orgName" placeholder="输入公司名称" clearable />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="q.taxNo" placeholder="输入税号" clearable />
</el-form-item>
<el-form-item label="合作状态">
<el-select v-model="q.status" placeholder="请选择" clearable>
<el-option label="正常/合作中" value="0" />
<el-option label="合作中" value="合作中" />
<el-option label="禁用" value="禁用" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="orgName" label="公司名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="address" label="公司地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="taxNo" label="税号/统一社会信用代码" min-width="180" />
<el-table-column label="合作状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建日期" width="120" align="center">
<template #default="{ row }">{{ row.createTime ? String(row.createTime).slice(0,10) : '' }}</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="view(row)">查看</el-button>
<el-button link type="primary" @click="goPeople(row)">人员管理</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
</div>
<el-dialog v-model="detailOpen" title="支持单位详情" width="640px" v-if="currentRow">
<el-descriptions :column="1" border>
<el-descriptions-item label="公司类型">
<el-tag type="success">赞助方</el-tag>
</el-descriptions-item>
<el-descriptions-item label="公司名称">{{ currentRow.orgName }}</el-descriptions-item>
<el-descriptions-item label="公司地址">{{ currentRow.address }}</el-descriptions-item>
<el-descriptions-item label="税号/统一社会信用代码">{{ currentRow.taxNo }}</el-descriptions-item>
<el-descriptions-item label="合作状态">
<el-tag :type="statusTagType(currentRow.status)">{{ currentRow.status }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建日期">{{ currentRow.createTime ? String(currentRow.createTime).slice(0,10) : '' }}</el-descriptions-item>
<el-descriptions-item label="联系人">{{ currentRow.contactName || '-' }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ currentRow.contactPhone || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="detailOpen=false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { bizList } from '@/api/public'
const router = useRouter()
const q = ref({ orgType: 'sponsor', orgName: '', taxNo: '', status: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
const currentRow = ref(null)
const detailOpen = ref(false)
function statusTagType(s) {
if (s === '正常' || s === '0' || s === '合作中') return 'success'
if (s === '禁用' || s === '1') return 'danger'
return 'info'
}
async function load() {
loading.value = true
try {
const { data } = await bizList('org', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = data?.rows || []; page.total = data?.total || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() { q.value = { orgType: 'sponsor', orgName: '', taxNo: '', status: '' }; page.pageNum = 1; load() }
function view(row) { currentRow.value = row; detailOpen.value = true }
function goPeople(row) {
router.push({ path: '/manager/sponsor-people', query: { orgId: row.orgId, orgName: row.orgName } })
}
onMounted(load)
</script>
<style scoped>
.manager-sponsor-orgs { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+225
View File
@@ -0,0 +1,225 @@
<template>
<!--
角色: 🟨 合规 支持单位下人员 (manager/sponsor-people)
入口: org 表格行内人员管理按钮 (router.push orgId/orgName)
接口: bizList('person', q) / bizUpdate / bizAdd / sponsorImport / sponsorImportTemplate
-->
<div class="page-card manager-sponsor-people">
<div class="breadcrumb">
首页 / 支持单位管理 / 人员管理{{ orgName ? ' (' + orgName + ')' : '' }}
</div>
<el-form inline :model="q" class="filter-form">
<el-form-item label="姓名">
<el-input v-model="q.name" placeholder="姓名" clearable style="width:140px" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="q.phone" placeholder="手机号" clearable style="width:140px" />
</el-form-item>
<el-form-item label="企业名称">
<el-input v-model="q.orgName" placeholder="企业名称" clearable style="width:180px" />
</el-form-item>
<el-form-item label="部门">
<el-input v-model="q.department" placeholder="部门" clearable style="width:140px" />
</el-form-item>
<el-form-item label="职务">
<el-input v-model="q.position" placeholder="职务" clearable style="width:140px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="batch-bar">
<el-button type="primary" @click="goNew">新建人员</el-button>
<el-button @click="openImport">批量导入</el-button>
<span class="filter-tip">*批量导入模板与列表表项一致</span>
</div>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column prop="name" label="姓名" min-width="100" />
<el-table-column prop="phone" label="手机号" min-width="130" />
<el-table-column prop="orgName" label="企业名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" min-width="100" />
<el-table-column prop="position" label="职务" min-width="100" />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status || '正常' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
{{ isDisabled(row) ? '启用' : '禁用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
:total="page.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
<el-dialog v-model="importOpen" title="批量导入" width="520px" :close-on-click-modal="false">
<el-form label-width="100px">
<el-form-item label="选择文件">
<el-upload ref="uploadRef" :auto-upload="false" :limit="1" accept=".xls,.xlsx" :on-change="onFileChange" :on-remove="onFileRemove">
<el-button>选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="">
<el-button link type="primary" @click="downloadTemplate">批量导入模板下载</el-button>
</el-form-item>
<el-form-item><span class="filter-tip">*导入列: 姓名/手机号/所属公司/部门/职务/角色</span></el-form-item>
</el-form>
<template #footer>
<el-button @click="importOpen = false">取消</el-button>
<el-button type="primary" :loading="importing" @click="submitImport">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { bizList, bizUpdate, importPerson, downloadImportTemplate } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
const route = useRoute()
const router = useRouter()
const orgId = computed(() => route.query.orgId ? Number(route.query.orgId) : null)
const orgName = computed(() => route.query.orgName || '')
const q = reactive({ name: '', phone: '', orgName: '', department: '', position: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
function statusTagType(s) {
if (s === '禁用' || s === '1') return 'danger'
return 'success'
}
function isDisabled(row) {
return row.status === '禁用' || row.status === '1'
}
async function load() {
loading.value = true
try {
const params = {
...q,
orgType: 'sponsor',
...(orgId.value ? { orgId: orgId.value } : {}),
pageNum: page.pageNum,
pageSize: page.pageSize,
}
const { data } = await bizList('person', params)
rows.value = (data && data.rows) || []
page.total = (data && data.total) || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() {
Object.assign(q, { name: '', phone: '', orgName: '', department: '', position: '' })
page.pageNum = 1
load()
}
async function onToggleStatus(row) {
const disabled = isDisabled(row)
const newStatus = disabled ? '正常' : '禁用'
const label = disabled ? '启用' : '禁用'
try {
await ElMessageBox.confirm(
`确定要${label}${row.name}」吗?`,
'确认',
{ type: 'warning' }
)
} catch { return }
try {
await bizUpdate('person', { personId: row.personId, status: newStatus })
ElMessage.success('已' + label)
load()
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
}
// ========== 新建人员 ==========
function goNew() {
router.push({
path: '/manager/sponsor-people/new',
query: {
...(orgId.value ? { orgId: orgId.value } : {}),
...(orgName.value ? { orgName: orgName.value } : {})
}
})
}
// ========== 批量导入 ==========
const importOpen = ref(false)
const importing = ref(false)
const uploadRef = ref()
const importFile = ref(null)
function openImport() { importFile.value = null; importOpen.value = true }
function onFileChange(file) { importFile.value = file.raw }
function onFileRemove() { importFile.value = null }
async function downloadTemplate() {
try {
const res = await downloadImportTemplate('sponsor')
const blob = new Blob([res.data], { type: res.data.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '支持单位人员导入模板.xlsx'
a.click()
URL.revokeObjectURL(url)
} catch { ElMessage.error('模板下载失败') }
}
async function submitImport() {
if (!importFile.value) { ElMessage.warning('请先选择文件'); return }
importing.value = true
try {
const res = await importPerson('sponsor', importFile.value)
const payload = res.data || res
const ok = payload.ok ?? 0
const total = payload.total ?? 0
const failed = (payload.results || []).filter(r => !r.ok)
if (failed.length === 0) {
ElMessage.success(`导入成功 ${ok}/${total}`)
} else {
ElMessageBox.alert(
`成功 ${ok} 条, 失败 ${failed.length} 条:\n` +
failed.map(f => `${f.rowNo}${f.name || ''}: ${f.message}`).join('\n'),
'导入结果', { type: 'warning' }
)
}
importOpen.value = false
load()
} catch (e) {
ElMessage.error(e?.msg || '导入失败')
} finally {
importing.value = false
}
}
onMounted(load)
</script>
<style scoped>
.manager-sponsor-people { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.filter-tip { color: #909399; font-size: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+54 -49
View File
@@ -1,41 +1,43 @@
<template>
<div class="manager-support-intent">
<div class="page-card">
<div class="page-title">支持意向</div>
<!-- 筛选区严格按原型 top=169-217 left 顺序: 意向项目名称(276) / ZH-2026-658(371) / 姓名(541) / 工作单位(580) / 工作单位(750) / 工作单位(817) / 项目编号(996) / 提示(1113) -->
<el-form inline :model="q">
<el-form-item label="项目编号:"><el-input v-model="q.projectNo" placeholder="如 ZH-2026-658" clearable /></el-form-item>
<el-form-item label="意向项目名称:"><el-input v-model="q.projectName" placeholder="输入意向项目名称" clearable /></el-form-item>
<el-form-item label="姓名:"><el-input v-model="q.name" placeholder="输入姓名" clearable /></el-form-item>
<el-form-item label="工作单位:"><el-input v-model="q.workUnit" placeholder="输入工作单位" clearable /></el-form-item>
<el-form-item>
<el-button class="action-btn" @click="load">查找</el-button>
<el-button class="action-btn secondary" @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- 提示语严格按原型 top=217-243 -->
<div class="filter-tip">*项目公示中点击支持意向后显示在此</div>
<div class="filter-tip">*支持意向不需要注册但是如果已经注册账号则显示账号状态</div>
<div class="filter-tip">*根据手机号与系统账号进行匹配</div>
<div class="page-card manager-support-intent">
<div class="breadcrumb">首页 / 支持意向</div>
<!-- 提示语 (按原型 top=217-243) -->
<div class="hint-list">
<p>*项目公示中点击支持意向后显示在此</p>
<p>*支持意向不需要注册但是如果已经注册账号则显示账号状态</p>
<p>*根据手机号与系统账号进行匹配</p>
</div>
<div class="page-card">
<!-- 列头严格按原型 top=296 left 顺序: 支持意向项目编号 272 / 支持意向项目名称 437 / 姓名 610 / 工作单位名称 685 / 部门 815 / 职务 925 / 手机号 1054 / 账号状态 1164 -->
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="projectNo" label="支持意向项目编号" width="140" align="center" />
<el-table-column prop="projectName" label="支持意向项目名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="name" label="姓名" width="80" align="center" />
<el-table-column prop="workUnit" label="工作单位名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" width="120" align="center" />
<el-table-column prop="position" label="职务" width="120" align="center" />
<el-table-column prop="phone" label="手机号" width="130" align="center" />
<el-table-column label="账号状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.accountStatus==='存在' ? 'success' : 'warning'">{{ row.accountStatus || '不存在' }}</el-tag>
</template>
</el-table-column>
</el-table>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="如 ZH-2026-658" clearable /></el-form-item>
<el-form-item label="意向项目名称"><el-input v-model="q.projectName" placeholder="输入意向项目名称" clearable /></el-form-item>
<el-form-item label="姓名"><el-input v-model="q.name" placeholder="输入姓名" clearable /></el-form-item>
<el-form-item label="工作单位"><el-input v-model="q.workUnit" placeholder="输入工作单位" clearable /></el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="projectNo" label="支持意向项目编号" width="140" align="center" />
<el-table-column prop="projectName" label="支持意向项目名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="name" label="姓名" width="80" align="center" />
<el-table-column prop="workUnit" label="工作单位名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" width="120" align="center" />
<el-table-column prop="position" label="职务" width="120" align="center" />
<el-table-column prop="phone" label="手机号" width="130" align="center" />
<el-table-column label="账号状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.accountStatus==='存在' ? 'success' : 'warning'">{{ row.accountStatus || '不存在' }}</el-tag>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
</div>
</div>
@@ -64,20 +66,23 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-support-intent { padding: 16px; }
.page-card { background: #fff; border-radius: 6px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.page-title { font-size: 20px; font-weight: 600; margin-bottom: 4px; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
.filter-tip { color: #909399; font-size: 12px; line-height: 1.8; }
.page-toolbar { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
.page-toolbar :deep(.action-btn) {
height: 36px; padding: 0 20px; font-size: 14px; border-radius: 4px;
background: var(--brand-primary); color: #fff; border: 1px solid var(--brand-primary);
display: inline-flex; align-items: center; justify-content: center; white-space: nowrap;
transition: opacity .15s; margin-left: 0;
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
/* 红字提示 (与 People.vue 一致) */
.hint-list {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 12px 20px;
margin-bottom: 16px;
font-size: 12px;
color: #ff4d4f;
line-height: 1.8;
}
.page-toolbar :deep(.action-btn:hover) { opacity: .85; }
.page-toolbar :deep(.action-btn.secondary) {
background: #fff; color: var(--brand-primary); border: 1px solid var(--brand-primary);
}
.page-toolbar :deep(.action-btn.secondary:hover) { background: rgba(30,58,138,.06); }
.hint-list p { margin-bottom: 4px; }
.hint-list p:last-child { margin-bottom: 0; }
</style>
+50 -14
View File
@@ -1,10 +1,8 @@
<template>
<div class="manager-workbench">
<div class="page-card manager-workbench">
<div class="breadcrumb">首页 / 工作台</div>
<!-- 我的项目策划方案 标题 ( top=142) -->
<h2 class="page-title">我的项目策划方案</h2>
<!-- 5 KPI ( top=165 已结算/已结题 + top=458 已执行/未执行) -->
<!-- 5 KPI -->
<div class="stats-grid">
<router-link class="stat-card" to="/manager/projects">
<div class="stat-label">项目总数量</div>
@@ -30,7 +28,7 @@
<p class="hint-text">*点击数字跳转到相应的列表页</p>
<!-- 消息通知 (新增) -->
<!-- 消息通知 -->
<section class="section">
<h2 class="section-title">消息通知<a class="more" href="javascript:void(0)">更多 </a></h2>
<ul class="notice-list">
@@ -100,23 +98,61 @@ onMounted(() => { loadStats(); loadNotices() })
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-workbench { padding: 16px; }
.page-title { font-size: 20px; font-weight: 600; color: #1a1a1a; margin-bottom: 16px; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
/* 5 个 KPI 卡 (跟 executor/Overview 一致) */
.stats-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; margin-bottom: 12px; }
.stat-card {
background: #fff; border-radius: 4px; padding: 24px;
border: 1px solid #ebeef5; cursor: pointer; text-decoration: none; color: inherit;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 24px;
cursor: pointer;
text-decoration: none;
color: inherit;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: opacity .15s, border-color .15s;
min-height: 120px;
}
.stat-card:hover { border-color: var(--brand-primary); opacity: .9; }
.stat-label { font-size: 14px; color: #606266; margin-bottom: 16px; }
.stat-value { font-size: 32px; font-weight: 600; color: var(--brand-primary); line-height: 1.2; }
.stat-label { font-size: 14px; color: #8c8c8c; margin-bottom: 16px; }
.stat-value {
font-size: 32px;
font-weight: 600;
line-height: 1.2;
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-primary-deep) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hint-text { font-size: 12px; color: #ff4d4f; margin: 12px 0 20px; text-align: center; }
.section { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.section-title { font-size: 15px; font-weight: 600; color: #1a1a1a; margin-bottom: 16px; padding-left: 10px; border-left: 3px solid var(--brand-primary); display: flex; justify-content: space-between; }
/* 消息通知 */
.section {
background: #fff;
border-radius: 8px;
padding: 20px 24px;
border: 1px solid #f0f0f0;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 16px;
padding-left: 10px;
border-left: 3px solid var(--brand-primary);
line-height: 1;
display: flex;
justify-content: space-between;
}
.more { font-size: 12px; color: var(--brand-primary); text-decoration: none; }
.more:hover { text-decoration: underline; }
.notice-list { list-style: none; border-top: 1px solid #f0f0f0; }
.notice-item { padding: 12px 0; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; gap: 12px; }
.notice-item:last-child { border-bottom: none; }
+1 -1
View File
@@ -59,7 +59,7 @@
<el-input
v-if="myPersonId"
v-model="form.orgName"
placeholder="请填写所属公司 (TODO: 改为 biz_org 下拉)"
placeholder="请填写所属公司"
/>
<el-input
v-else
+26 -7
View File
@@ -5,7 +5,8 @@
筛选区 (top 165-205, left): 项目策划方案管理 / 项目总数量 / 已结算的会议 / 已结题的项目
列头 (top 285-335, left): 工作台 / 会议管理
-->
<div class="sponsor-home">
<div class="page-card sponsor-home">
<div class="breadcrumb">首页</div>
<!-- KPI: 5 个数字面板, 1 行展示 -->
<div class="stats-grid stats-grid-5">
@@ -100,7 +101,9 @@ onMounted(() => {
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.sponsor-home { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.stats-grid { display: grid; gap: 16px; margin-bottom: 16px; }
.stats-grid-5 { grid-template-columns: repeat(5, 1fr); }
@@ -109,28 +112,44 @@ onMounted(() => {
.stat-card {
background: #fff;
border-radius: 4px;
padding: 24px;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 24px;
cursor: pointer;
text-decoration: none;
color: inherit;
display: block;
transition: border-color 0.2s, box-shadow 0.2s;
}
.stat-card:hover { border-color: var(--brand-primary); box-shadow: 0 2px 8px rgba(30,58,138,0.15); }
.stat-label { font-size: 14px; color: #8c8c8c; margin-bottom: 16px; }
.stat-value { font-size: 32px; font-weight: 700; color: var(--brand-primary); line-height: 1; margin-bottom: 8px; }
.stat-value {
font-size: 32px;
font-weight: 700;
line-height: 1;
margin-bottom: 8px;
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-primary-deep) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.stat-desc { font-size: 12px; color: #bfbfbf; }
.hint-text { font-size: 12px; color: #ff4d4f; margin: 12px 0; text-align: center; }
.section { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #f0f0f0; }
.section {
background: #fff;
border-radius: 8px;
padding: 20px 24px;
border: 1px solid #f0f0f0;
}
.section-title {
font-size: 15px; font-weight: 600; color: #1a1a1a;
margin-bottom: 16px; padding-left: 10px;
border-left: 3px solid var(--brand-primary);
line-height: 1;
display: flex; justify-content: space-between;
}
.more { font-size: 12px; color: var(--brand-primary); text-decoration: none; }
.more:hover { text-decoration: underline; }
.notice-list { list-style: none; border-top: 1px solid #f0f0f0; }
.notice-item {
padding: 12px 0; border-bottom: 1px solid #f0f0f0;
+2 -2
View File
@@ -196,8 +196,8 @@ async function loadList() {
}
delete params.startTimeRange
const r = await bizList('meeting', params)
list.value = r.rows || []
page.total = r.total || 0
list.value = (r.data && r.data.rows) || r.rows || []
page.total = (r.data && r.data.total) || r.total || 0
} catch (e) { list.value = []; page.total = 0 }
finally { loading.value = false }
}
+4
View File
@@ -14,6 +14,10 @@ export default defineConfig({
resolve: {
alias: { '@': path.resolve(__dirname, 'src') }
},
build: {
// 老浏览器兼容: 把 ?. / ?? / 箭头函数等 ES2020+ 语法降级到 es2015
target: 'es2015'
},
server: {
port: 5173,
host: '0.0.0.0',
+32 -39
View File
@@ -60,17 +60,19 @@ SELECT meeting_id, 'ZH-2026-658', '小牛血清创新应用研讨会', '北京
SELECT meeting_id, 'ZH-2026-658', '小牛血清创新应用研讨会', '北京会务服务公司', '其他费用', 1000.00, 1, 1000.00, '茶水等', '已审核', 'labor' FROM biz_meeting WHERE business_id='5643145673';
-- 专家
-- audit_status: 0=未提交 1=待审核 2=通过 3=拒绝 (BizAuditStatusEnum)
-- status: Y=正常 N=禁用
INSERT INTO biz_expert(name, phone, work_unit, department, title, region, id_card, audit_status, audit_by, audit_time, status, create_by, create_time)
VALUES
('张三', '13534621147', '北京XXXXXXX医院', '呼吸科', '主任医师', '北京', '110101198501011234', '待审核', 'admin', NOW(), '0', 'admin', NOW()),
('李四', '13534621148', '北京XXXXXXX医院', '血液科', '副主任医师', '北京', '110101198801012345', '审核通过', 'admin', NOW(), '0', 'admin', NOW()),
('王五', '13534621149', '北京XXXXXXX医院', '心血管科', '主治医师', '上海', '310101198901013456', '待审核', 'admin', NOW(), '0', 'admin', NOW()),
('赵六', '13534621150', '北京XXXXXXX医院', '神经科', '副主任医师', '广东', '440101198701014567', '已退回', 'admin', NOW(), '0', 'admin', NOW()),
('钱七', '13534621151', '北京XXXXXXX医院', '内分泌科', '主任医师', '北京', '110101199001015678', '审核通过', 'admin', NOW(), '0', 'admin', NOW()),
('陈教授', '13534621152', '北京XXXXXXX医院', '消化科', '副主任医师', '北京', '110101198601016789', '审核通过', 'admin', NOW(), '0', 'admin', NOW()),
('王教授', '13534621153', '北京XXXXXXX医院', '心血管科', '主任医师', '上海', '310101198401017890', '审核通过', 'admin', NOW(), '0', 'admin', NOW()),
('李教授', '13534621154', '北京XXXXXXX医院', '神经科', '主治医师', '北京', '110101199201018901', '审核通过', 'admin', NOW(), '0', 'admin', NOW()),
('张教授', '13534621155', '北京XXXXXXX医院', '骨科', '主任医师', '北京', '110101198501019012', '已驳回', 'admin', NOW(), '0', 'admin', NOW());
('张三', '13534621147', '北京XXXXXXX医院', '呼吸科', '主任医师', '北京', '110101198501011234', '1', 'admin', NOW(), 'Y', 'admin', NOW()),
('李四', '13534621148', '北京XXXXXXX医院', '血液科', '副主任医师', '北京', '110101198801012345', '2', 'admin', NOW(), 'Y', 'admin', NOW()),
('王五', '13534621149', '北京XXXXXXX医院', '心血管科', '主治医师', '上海', '310101198901013456', '1', 'admin', NOW(), 'Y', 'admin', NOW()),
('赵六', '13534621150', '北京XXXXXXX医院', '神经科', '副主任医师', '广东', '440101198701014567', '3', 'admin', NOW(), 'Y', 'admin', NOW()),
('钱七', '13534621151', '北京XXXXXXX医院', '内分泌科', '主任医师', '北京', '110101199001015678', '2', 'admin', NOW(), 'Y', 'admin', NOW()),
('陈教授', '13534621152', '北京XXXXXXX医院', '消化科', '副主任医师', '北京', '110101198601016789', '2', 'admin', NOW(), 'Y', 'admin', NOW()),
('王教授', '13534621153', '北京XXXXXXX医院', '心血管科', '主任医师', '上海', '310101198401017890', '2', 'admin', NOW(), 'Y', 'admin', NOW()),
('李教授', '13534621154', '北京XXXXXXX医院', '神经科', '主治医师', '北京', '110101199201018901', '2', 'admin', NOW(), 'Y', 'admin', NOW()),
('张教授', '13534621155', '北京XXXXXXX医院', '骨科', '主任医师', '北京', '110101198501019012', '3', 'admin', NOW(), 'Y', 'admin', NOW());
-- 公司表 (赞助方/执行方共用)
INSERT INTO biz_org(org_name, org_type, address, tax_no, contact_name, contact_phone, intent_count, status, create_by, create_time)
@@ -97,23 +99,23 @@ VALUES
-- 人员(执行方会务执行, org_id 对应 biz_execution_unit 那 8 条, 取前 5 个 org_id)
INSERT INTO biz_person(name, phone, org_id, department, position, role, unit_type, status, create_by, create_time)
VALUES
('张三', '13534621147', 1, '会务部', '经理', '会务执行', 'execution', '0', 'admin', NOW()),
('李四', '13534621148', 1, '会务部', '专员', '会务执行', 'execution', '0', 'admin', NOW()),
('王五', '13534621149', 2, '会务部', '主管', '会务执行', 'execution', '0', 'admin', NOW()),
('赵六', '13534621150', 2, '会务部', '专员', '会务执行', 'execution', '0', 'admin', NOW()),
('钱七', '13534621151', 3, '会务部', '经理', '会务执行', 'execution', '0', 'admin', NOW()),
('孙八', '13534621152', 3, '会务部', '专员', '会务执行', 'execution', '0', 'admin', NOW()),
('周九', '13534621153', 4, '会务部', '主管', '会务执行', 'execution', '1', 'admin', NOW()),
('吴十', '13534621154', 4, '会务部', '专员', '会务执行', 'execution', '0', 'admin', NOW());
('张三', '13534621147', 1, '会务部', '经理', '会务执行', 'executor', '0', 'admin', NOW()),
('李四', '13534621148', 1, '会务部', '专员', '会务执行', 'executor', '0', 'admin', NOW()),
('王五', '13534621149', 2, '会务部', '主管', '会务执行', 'executor', '0', 'admin', NOW()),
('赵六', '13534621150', 2, '会务部', '专员', '会务执行', 'executor', '0', 'admin', NOW()),
('钱七', '13534621151', 3, '会务部', '经理', '会务执行', 'executor', '0', 'admin', NOW()),
('孙八', '13534621152', 3, '会务部', '专员', '会务执行', 'executor', '0', 'admin', NOW()),
('周九', '13534621153', 4, '会务部', '主管', '会务执行', 'executor', '1', 'admin', NOW()),
('吴十', '13534621154', 4, '会务部', '专员', '会务执行', 'executor', '0', 'admin', NOW());
-- 人员(合规方项目负责人/会议执行/监察员, org_id 对应 sponsor 类型 biz_org 前 5 个)
INSERT INTO biz_person(name, phone, org_id, department, position, role, unit_type, status, create_by, create_time)
VALUES
('张三', '13800138001', 1, '合规部', '经理', '项目负责人', 'execution', '0', 'admin', NOW()),
('李四', '13800138002', 2, '合规部', '专员', '会议执行', 'execution', '0', 'admin', NOW()),
('王五', '13800138003', 3, '监察组', '组长', '监察员', 'execution', '0', 'admin', NOW()),
('赵六', '13800138004', 4, '监察组', '专员', '监察员', 'execution', '0', 'admin', NOW()),
('钱七', '13800138005', 5, '合规部', '主管', '项目负责人', 'execution', '0', 'admin', NOW());
('张三', '13800138001', 1, '合规部', '经理', '项目负责人', 'executor', '0', 'admin', NOW()),
('李四', '13800138002', 2, '合规部', '专员', '会议执行', 'executor', '0', 'admin', NOW()),
('王五', '13800138003', 3, '监察组', '组长', '监察员', 'executor', '0', 'admin', NOW()),
('赵六', '13800138004', 4, '监察组', '专员', '监察员', 'executor', '0', 'admin', NOW()),
('钱七', '13800138005', 5, '合规部', '主管', '项目负责人', 'executor', '0', 'admin', NOW());
-- 支持意向
INSERT INTO biz_support_intent(project_no, project_name, name, work_unit, department, position, phone, account_status, create_time)
@@ -192,27 +194,18 @@ VALUES
('LV-2026-003', 3, '智慧医院建设项目方案论证', '李教授', '440101********9012', '6222****3456', 4000.00, '2026-03-16 10:00:00', '已审核'),
('LV-2026-004', 4, '基层医疗改革试点中期评估', '张教授', '320101********3456', '6222****7890', 3500.00, '2026-02-02 10:00:00', '已驳回');
-- 业务角色绑定(为 sys_user 建立业务角色映射)
-- 业务角色绑定 (2026-08-16 重构: 统一存 sys_user.role_type, 不再使用 biz_user_role_bind)
-- admin: 1=后台管理员
INSERT INTO biz_user_role_bind(user_id, role_type, unit_type, create_time)
SELECT user_id, 'admin', NULL, NOW() FROM sys_user WHERE user_name='admin';
UPDATE sys_user SET role_type='admin' WHERE user_name='admin';
-- 创建 5 个业务角色用户 (密码 123456, RuoYi 加密后)
INSERT INTO sys_user(user_name, nick_name, user_type, email, phonenumber, sex, avatar, password, status, del_flag, login_ip, create_by, create_time, remark)
INSERT INTO sys_user(user_name, nick_name, user_type, email, phonenumber, sex, avatar, password, status, del_flag, login_ip, create_by, create_time, remark, role_type)
VALUES
('leader01', '项目负责人张三', '00', 'leader01@bahim.org.cn', '13900139001', '0', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', 'admin', NOW(), '项目负责人'),
('manager01', '合规员李四', '00', 'manager01@bahim.org.cn', '13900139002', '0', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', 'admin', NOW(), '合规人员'),
('doctor01', '专家王五', '00', 'doctor01@bahim.org.cn', '13900139003', '0', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', 'admin', NOW(), '专家'),
('executor01', '执行负责人赵六', '00', 'executor01@bahim.org.cn', '13900139004', '0', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', 'admin', NOW(), '执行方'),
('sponsor01', '赞助方钱七', '00', 'sponsor01@bahim.org.cn', '13900139005', '0', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', 'admin', NOW(), '赞助方');
-- 绑定业务角色
INSERT INTO biz_user_role_bind(user_id, role_type, unit_type, create_time)
SELECT user_id, 'leader', NULL, NOW() FROM sys_user WHERE user_name='leader01' UNION ALL
SELECT user_id, 'manager', NULL, NOW() FROM sys_user WHERE user_name='manager01' UNION ALL
SELECT user_id, 'doctor', NULL, NOW() FROM sys_user WHERE user_name='doctor01' UNION ALL
SELECT user_id, 'executor', 'execution', NOW() FROM sys_user WHERE user_name='executor01' UNION ALL
SELECT user_id, 'sponsor', 'sponsor', NOW() FROM sys_user WHERE user_name='sponsor01';
('leader01', '项目负责人张三', '00', 'leader01@bahim.org.cn', '13900139001', '0', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', 'admin', NOW(), '项目负责人', 'leader'),
('manager01', '合规员李四', '00', 'manager01@bahim.org.cn', '13900139002', '0', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', 'admin', NOW(), '合规人员', 'manager'),
('doctor01', '专家王五', '00', 'doctor01@bahim.org.cn', '13900139003', '0', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', 'admin', NOW(), '专家', 'doctor'),
('executor01', '执行负责人赵六', '00', 'executor01@bahim.org.cn', '13900139004', '0', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', 'admin', NOW(), '执行方', 'executor'),
('sponsor01', '赞助方钱七', '00', 'sponsor01@bahim.org.cn', '13900139005', '0', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', 'admin', NOW(), '赞助方', 'sponsor');
-- 给所有业务用户配项目负责人角色(ruoyi role_id=2)
INSERT INTO sys_user_role(user_id, role_id)
+4 -13
View File
@@ -174,11 +174,11 @@ CREATE TABLE biz_expert (
id_card_back_url VARCHAR(500) DEFAULT NULL COMMENT '身份证反面URL',
practice_cert_url VARCHAR(500) DEFAULT NULL COMMENT '执业证书URL',
title_cert_url VARCHAR(500) DEFAULT NULL COMMENT '职称证明URL',
audit_status VARCHAR(20) DEFAULT '0' COMMENT '审核状态 待审核/审核通过/已退回',
audit_status VARCHAR(20) DEFAULT '1' COMMENT '审核状态 0未提交 1待审核 2通过 3拒绝',
audit_opinion VARCHAR(500) DEFAULT NULL COMMENT '审核意见',
audit_by VARCHAR(64) DEFAULT NULL COMMENT '审核人',
audit_time DATETIME DEFAULT NULL COMMENT '审核时间',
status CHAR(1) DEFAULT '0' COMMENT '状态 0正常 1禁用',
status CHAR(1) DEFAULT 'Y' COMMENT '状态 Y正常 N禁用',
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
@@ -392,17 +392,8 @@ CREATE TABLE biz_labor_voucher (
UNIQUE KEY uk_voucher_no (voucher_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='劳务凭证表';
DROP TABLE IF EXISTS biz_user_role_bind;
CREATE TABLE biz_user_role_bind (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
role_type VARCHAR(20) NOT NULL COMMENT '业务角色 admin/leader/manager/doctor/executor/sponsor (注: 原 supplier 角色已统一为 executor, 见 2026-08-15 重构)',
unit_id BIGINT DEFAULT NULL COMMENT '所属单位ID',
unit_type VARCHAR(20) DEFAULT NULL COMMENT '所属单位类型',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uk_urb_user (user_id, role_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户业务角色绑定';
-- 注: 2026-08-16 重构, 删 biz_user_role_bind 表, 业务角色统一存 sys_user.role_type
-- sys_user 表在 RuoYi 标准 schema 里已含 role_type 列 (admin/leader/manager/doctor/executor/sponsor)
SET FOREIGN_KEY_CHECKS = 1;
@@ -0,0 +1,45 @@
-- ============================================================
-- Migration: biz_expert.audit_status 0/1/2/中文 → 0/1/2/3 (语义扩展)
-- Date: 2026-08-16
-- Author: Claude
-- 影响范围: biz_expert 表的 audit_status 字段
-- 旧语义: '0' = 待审核, '1' = 通过, '2' = 退回 (还有中文脏数据: '审核通过'/'待审核'/'已退回'/'已驳回')
-- 新语义: '0' = 未提交, '1' = 待审核, '2' = 通过, '3' = 拒绝
-- 注意: status 字段已改成 Y/N (见 migration_status_YN_2026_08_16.sql)
-- ============================================================
-- 0. 备份 (必须做, 用于回滚)
CREATE TABLE biz_expert_audit_status_bak_20260816 AS
SELECT expert_id, name, audit_status FROM biz_expert;
-- 1. 单条 CASE UPDATE, 一次搞定 (避免 cascading: '0'→'1' 后被 '1'→'2' 误改)
UPDATE biz_expert
SET audit_status = CASE audit_status
WHEN '0' THEN '1' -- 待审核 (旧) → 待审核 (新)
WHEN '1' THEN '2' -- 通过 (旧) → 通过 (新)
WHEN '2' THEN '3' -- 退回 (旧) → 拒绝 (新)
WHEN '审核通过' THEN '2'
WHEN '待审核' THEN '1'
WHEN '已退回' THEN '3'
WHEN '已驳回' THEN '3'
ELSE audit_status -- 未识别的值不动, 人工排查
END
WHERE audit_status IN ('0','1','2','审核通过','待审核','已退回','已驳回');
-- 2. ALTER 默认值 + comment
ALTER TABLE biz_expert
MODIFY COLUMN audit_status VARCHAR(20) DEFAULT '1' COMMENT '审核状态 0未提交 1待审核 2通过 3拒绝';
-- 3. 验证 (执行后应全部为 1/2/3)
-- SELECT audit_status, COUNT(*) FROM biz_expert GROUP BY audit_status ORDER BY audit_status;
-- 预期结果 (取决于生产数据):
-- 1 | 待审核
-- 2 | 通过
-- 3 | 拒绝
-- 4. 若有数据未转换 (上面 ELSE 分支), 用 SELECT 排查:
-- SELECT expert_id, name, audit_status FROM biz_expert
-- WHERE audit_status NOT IN ('0','1','2','3') OR audit_status IS NULL;
-- 5. 确认无误后, 可删除备份表:
-- DROP TABLE biz_expert_audit_status_bak_20260816;
+27
View File
@@ -0,0 +1,27 @@
-- ============================================================
-- Migration: biz_expert.status 0/1 → Y/N
-- Date: 2026-08-16
-- Author: Claude
-- 影响范围: biz_expert 表的 status 字段
-- 旧语义: '0' = 正常, '1' = 禁用
-- 新语义: 'Y' = 正常, 'N' = 禁用
-- 注意: audit_status (0/1/2) 不变
-- ============================================================
-- 0. 备份 (可选, 已上生产前一定要做)
-- CREATE TABLE biz_expert_status_bak_20260816 AS
-- SELECT expert_id, status FROM biz_expert;
-- 1. 数据迁移 (先转换旧值, 避免默认值变化后插入冲突)
UPDATE biz_expert SET status = 'Y' WHERE status = '0';
UPDATE biz_expert SET status = 'N' WHERE status = '1';
-- 2. 表结构变更 (默认值 + 注释)
ALTER TABLE biz_expert
MODIFY COLUMN status CHAR(1) DEFAULT 'Y' COMMENT '状态 Y正常 N禁用';
-- 3. 验证 (执行后应全部为 Y/N)
-- SELECT status, COUNT(*) FROM biz_expert GROUP BY status;
-- 预期结果:
-- Y | 10
-- N | 0