chore: biz_org 合并 / BizProject 重构 / 各角色页面调整 (上次重构遗留)

涵盖改动范围:
- 后端 BizOrg / BizProject / BizProjectRating / BizSpecialPlan controller + domain + mapper + service
- 前端 admin (BizSpecialPlanAdmin, ProjectCategory) / executor / leader / manager / sponsor 多页调整
- api/dict.js + AdminLayout.vue 同步
This commit is contained in:
郭庆泰
2026-08-18 00:30:34 +08:00
parent c9fc6ebdb8
commit b52a334184
32 changed files with 744 additions and 454 deletions
@@ -11,6 +11,7 @@ import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.business.domain.BizOrg;
import com.ruoyi.business.service.IBizOrgService;
import com.ruoyi.common.utils.SecurityUtils;
/**
* 公司Controller (支持方 + 执行方 共用)
@@ -62,6 +63,18 @@ public class BizOrgController extends BaseController {
return success(bizOrgService.getById(orgId));
}
/**
* 当前登录 sponsor 的所属公司 (供 /sponsor/account 页面回显)
* 返回 { orgId, orgName, isOwner: 1|0 }
* 主账号 isOwner=1 可改名, 子账号 isOwner=0 只读
*/
@GetMapping("/myCompany")
public AjaxResult myCompany() {
Long uid = SecurityUtils.getUserId();
Map<String, Object> data = bizOrgService.selectMySponsorCompany(uid);
return success(data);
}
@Log(title = "公司管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody BizOrg bizOrg) {
@@ -83,6 +83,37 @@ public class BizProjectController extends BaseController
List<BizProject> list = bizProjectService.selectList(bizProject);
return getDataTable(list);
}
/**
* sponsor 专属项目列表 (按当前登录 sponsor 的 user_id 过滤)
* GET /business/project/sponsorList
* 注: biz_project.sponsor_admin_user_id 永远是主账号 user_id, 子账号登录也应能看主账号的项目 — 简化: 直接用当前 user_id 过滤
* 若需要子账号看主账号项目, 改 SQL 改为 (sponsor_admin_user_id = uid OR sponsor_admin_user_id IN (parent_user_id=uid 的子账号所属主账号))
*/
@GetMapping("/sponsorList")
public TableDataInfo sponsorList(BizProject bizProject)
{
bizProject.getParams().put("sponsorAdminUserId", SecurityUtils.getUserId());
startPage();
List<BizProject> list = bizProjectService.selectSponsorList(bizProject);
return getDataTable(list);
}
/**
* executor 专属项目列表 (按当前 user_id + 其 executor biz_org.org_id 过滤 biz_project_assign)
* GET /business/project/executorList
* 匹配规则 (任一):
* 1) biz_project_assign.exec_user_id = uid
* 2) biz_project_assign.execution_unit_id = uid 对应的 executor biz_org.org_id
*/
@GetMapping("/executorList")
public TableDataInfo executorList(BizProject bizProject)
{
bizProject.getParams().put("executorUserId", SecurityUtils.getUserId());
startPage();
List<BizProject> list = bizProjectService.selectExecutorList(bizProject);
return getDataTable(list);
}
@GetMapping("/{projectId}")
public AjaxResult getInfo(@PathVariable("projectId") String projectId)
{
@@ -144,9 +175,11 @@ public class BizProjectController extends BaseController
@PostMapping("/rate")
public AjaxResult rate(@RequestBody BizProjectRating body)
{
if (body.getProjectId() == null || body.getRaterId() == null || body.getRaterRole() == null) {
return error("projectId / raterId / raterRole 必填");
if (body.getProjectId() == null || body.getRaterRole() == null) {
return error("projectId / raterRole 必填");
}
// 评分人以当前登录用户为准, 不信任前端传入的 raterId
body.setRaterId(SecurityUtils.getUserId());
body.setCreateBy(SecurityUtils.getUsername());
body.setUpdateBy(SecurityUtils.getUsername());
int rows = bizProjectRatingService.upsertRating(body);
@@ -47,9 +47,11 @@ public class BizProjectRatingController extends BaseController
@PostMapping("/rate")
public AjaxResult rate(@RequestBody BizProjectRating body)
{
if (body.getProjectId() == null || body.getRaterId() == null || body.getRaterRole() == null) {
return error("projectId / raterId / raterRole 必填");
if (body.getProjectId() == null || body.getRaterRole() == null) {
return error("projectId / raterRole 必填");
}
// 评分人以当前登录用户为准
body.setRaterId(getUserId());
body.setCreateBy(getUsername());
body.setUpdateBy(getUsername());
int rows = bizProjectRatingService.upsertRating(body);
@@ -32,6 +32,26 @@ public class BizSpecialPlanController extends BaseController
return getDataTable(list);
}
/**
* 专项计划下拉选项 (供 manager/Plans 等页面填充 "项目方向" 下拉)
* 返回 [{id, title}], 仅启用项 (status='0'), 按 sort_order 升序
* 注: 不加 @PreAuthorize, 与 BizProjectController 一致 — manager 是业务角色, 不走 RuoYi RBAC 权限
*/
@GetMapping("/options")
public AjaxResult options()
{
List<BizSpecialPlan> all = specialPlanService.selectList(new BizSpecialPlan());
java.util.List<java.util.Map<String, Object>> opts = new java.util.ArrayList<>();
for (BizSpecialPlan p : all) {
if (p.getStatus() != null && !"0".equals(p.getStatus())) continue;
java.util.Map<String, Object> m = new java.util.LinkedHashMap<>();
m.put("id", p.getId());
m.put("title", p.getTitle());
opts.add(m);
}
return success(opts);
}
@PreAuthorize("@ss.hasPermi('business:specialPlan:query')")
@GetMapping("/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id)
@@ -20,6 +20,10 @@ public class BizProject extends BaseEntity {
/** total_sessions */
@Excel(name = "total_sessions")
private Long totalSessions;
/** 分配给当前执行方的场次 (biz_project_assign.sessions 之和, 仅 executor 端使用) */
private Long assignedSessions;
/** 分配给当前执行方的金额 (biz_project_assign.amount 之和, 仅 executor 端使用) */
private java.math.BigDecimal assignedAmount;
/** done_sessions */
@Excel(name = "done_sessions")
private Long doneSessions;
@@ -38,17 +42,12 @@ public class BizProject extends BaseEntity {
/** paid_meeting_amount */
@Excel(name = "paid_meeting_amount")
private BigDecimal paidMeetingAmount;
/** rating_score (均分) */
@Excel(name = "rating_score")
private BigDecimal ratingScore;
/** 评分维度: 履约质量 */
private BigDecimal ratingQ1;
/** 评分维度: 时效响应 */
private BigDecimal ratingQ2;
/** 评分维度: 配合度 */
private BigDecimal ratingQ3;
/** 评分维度: 合规安全 */
private BigDecimal ratingQ4;
/** manager_score (合规管理员评均分) */
@Excel(name = "manager_score")
private BigDecimal managerScore;
/** sponsor_score (支持方评均分) */
@Excel(name = "sponsor_score")
private BigDecimal sponsorScore;
/** 支持方负责人用户名(冗余) — 原 org_name */
@Excel(name = "sponsor_admin_user_name")
private String sponsorAdminUserName;
@@ -138,6 +137,10 @@ public class BizProject extends BaseEntity {
public void setProjectName(String projectName) { this.projectName = projectName; }
public Long getTotalSessions() { return totalSessions; }
public void setTotalSessions(Long totalSessions) { this.totalSessions = totalSessions; }
public Long getAssignedSessions() { return assignedSessions; }
public void setAssignedSessions(Long assignedSessions) { this.assignedSessions = assignedSessions; }
public java.math.BigDecimal getAssignedAmount() { return assignedAmount; }
public void setAssignedAmount(java.math.BigDecimal assignedAmount) { this.assignedAmount = assignedAmount; }
public Long getDoneSessions() { return doneSessions; }
public void setDoneSessions(Long doneSessions) { this.doneSessions = doneSessions; }
public Long getTodoSessions() { return todoSessions; }
@@ -150,16 +153,10 @@ public class BizProject extends BaseEntity {
public void setPaidLaborAmount(BigDecimal paidLaborAmount) { this.paidLaborAmount = paidLaborAmount; }
public BigDecimal getPaidMeetingAmount() { return paidMeetingAmount; }
public void setPaidMeetingAmount(BigDecimal paidMeetingAmount) { this.paidMeetingAmount = paidMeetingAmount; }
public BigDecimal getRatingScore() { return ratingScore; }
public void setRatingScore(BigDecimal ratingScore) { this.ratingScore = ratingScore; }
public BigDecimal getRatingQ1() { return ratingQ1; }
public void setRatingQ1(BigDecimal ratingQ1) { this.ratingQ1 = ratingQ1; }
public BigDecimal getRatingQ2() { return ratingQ2; }
public void setRatingQ2(BigDecimal ratingQ2) { this.ratingQ2 = ratingQ2; }
public BigDecimal getRatingQ3() { return ratingQ3; }
public void setRatingQ3(BigDecimal ratingQ3) { this.ratingQ3 = ratingQ3; }
public BigDecimal getRatingQ4() { return ratingQ4; }
public void setRatingQ4(BigDecimal ratingQ4) { this.ratingQ4 = ratingQ4; }
public BigDecimal getManagerScore() { return managerScore; }
public void setManagerScore(BigDecimal managerScore) { this.managerScore = managerScore; }
public BigDecimal getSponsorScore() { return sponsorScore; }
public void setSponsorScore(BigDecimal sponsorScore) { this.sponsorScore = sponsorScore; }
public String getSponsorAdminUserName() { return sponsorAdminUserName; }
public void setSponsorAdminUserName(String sponsorAdminUserName) { this.sponsorAdminUserName = sponsorAdminUserName; }
public String getSponsorOrgName() { return sponsorOrgName; }
@@ -25,7 +25,7 @@ public class BizProjectRating extends BaseEntity {
private String raterRole;
/** quality_score */
@Excel(name = "quality_score")
private String qualityScore;
private Long qualityScore;
/** response_score */
@Excel(name = "response_score")
private Long responseScore;
@@ -35,9 +35,6 @@ public class BizProjectRating extends BaseEntity {
/** compliance_score */
@Excel(name = "compliance_score")
private Long complianceScore;
/** total_score */
@Excel(name = "total_score")
private BigDecimal totalScore;
/** remark */
@Excel(name = "remark")
private String remark;
@@ -71,16 +68,14 @@ public class BizProjectRating extends BaseEntity {
public void setRaterName(String raterName) { this.raterName = raterName; }
public String getRaterRole() { return raterRole; }
public void setRaterRole(String raterRole) { this.raterRole = raterRole; }
public String getQualityScore() { return qualityScore; }
public void setQualityScore(String qualityScore) { this.qualityScore = qualityScore; }
public Long getQualityScore() { return qualityScore; }
public void setQualityScore(Long qualityScore) { this.qualityScore = qualityScore; }
public Long getResponseScore() { return responseScore; }
public void setResponseScore(Long responseScore) { this.responseScore = responseScore; }
public Long getCooperationScore() { return cooperationScore; }
public void setCooperationScore(Long cooperationScore) { this.cooperationScore = cooperationScore; }
public Long getComplianceScore() { return complianceScore; }
public void setComplianceScore(Long complianceScore) { this.complianceScore = complianceScore; }
public BigDecimal getTotalScore() { return totalScore; }
public void setTotalScore(BigDecimal totalScore) { this.totalScore = totalScore; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public String getCreateBy() { return createBy; }
@@ -18,4 +18,10 @@ public interface BizOrgMapper {
* 注意: value 必须 MAIN user_id (写入 biz_project_assign.exec_user_id);
* label 必须 org_name, 不用 user_name 以避免把同公司的普通员工带出来 */
List<Map<String, Object>> selectExecutorOrgOptions(BizOrg entity);
/** 当前登录 sponsor 的所属公司
* 主账号 (sys_user.parent_user_id IS NULL): biz_org.user_id = #{userId}
* 子账号 (有 biz_person 记录): biz_org.org_id = biz_person.org_id
* 返回 Map: orgId / orgName / isOwner (1=主账号, 0=子账号)
* 用于 /sponsor/account 页面回显 + 主账号修改 org_name */
Map<String, Object> selectMySponsorCompany(Long userId);
}
@@ -11,6 +11,8 @@ public interface BizProjectMapper
List<BizProject> selectList(BizProject entity);
/** sponsor 端专属: projectIds + LEFT JOIN 当前 login 用户评分, 用于评分回显 */
List<BizProject> selectSponsorList(BizProject entity);
/** executor 端专属: JOIN biz_project_assign 过滤, 只返回分给当前 user 的项目 */
List<BizProject> selectExecutorList(BizProject entity);
int insert(BizProject entity);
int updateByPrimaryKey(BizProject entity);
int deleteByPrimaryKey(String projectId);
@@ -15,4 +15,6 @@ public interface IBizOrgService {
/** 执行方下拉选项 (userId/orgName/userName), 用于 manager 项目分配弹窗
* 只返回 biz_org.org_type='executor' 对应的 MAIN 账号 (parent_user_id IS NULL) */
List<Map<String, Object>> selectExecutorOrgOptions(BizOrg entity);
/** 当前登录 sponsor 的所属公司 (主账号可改, 子账号只读) */
Map<String, Object> selectMySponsorCompany(Long userId);
}
@@ -12,6 +12,8 @@ public interface IBizProjectService
List<BizProject> selectList(BizProject entity);
/** sponsor 端专属: LEFT JOIN 当前 login 用户评分回显 */
List<BizProject> selectSponsorList(BizProject entity);
/** executor 端专属: JOIN biz_project_assign 过滤, 只返回分给当前 user 的项目 */
List<BizProject> selectExecutorList(BizProject entity);
int insert(BizProject entity);
int updateByPrimaryKey(BizProject entity);
int deleteByPrimaryKey(String projectId);
@@ -45,4 +45,9 @@ public class BizOrgServiceImpl implements IBizOrgService {
public List<Map<String, Object>> selectExecutorOrgOptions(BizOrg entity) {
return bizOrgMapper.selectExecutorOrgOptions(entity);
}
@Override
public Map<String, Object> selectMySponsorCompany(Long userId) {
return bizOrgMapper.selectMySponsorCompany(userId);
}
}
@@ -23,6 +23,9 @@ public class BizProjectServiceImpl implements IBizProjectService
public List<BizProject> selectSponsorList(BizProject entity)
{ return bizProjectMapper.selectSponsorList(entity); }
@Override
public List<BizProject> selectExecutorList(BizProject entity)
{ return bizProjectMapper.selectExecutorList(entity); }
@Override
public int insert(BizProject entity) { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "projectId"); return bizProjectMapper.insert(entity); }
@Override
public int updateByPrimaryKey(BizProject entity)
@@ -142,4 +142,23 @@
<if test="orgName != null and orgName != ''">and o.org_name like concat('%', #{orgName}, '%')</if>
order by o.org_id desc
</select>
<!--
当前登录 sponsor 的所属公司 (供 /sponsor/account 页面回显 + 主账号改名)
主账号 (sys_user.parent_user_id IS NULL): 用 own (biz_org.user_id = #{userId})
子账号 (有 biz_person.user_id = #{userId}): 用 biz_person.org_id → biz_org
COALESCE(own.org_id, p.org_id) 二选一, isOwner 标识主账号
返回 Map: orgId / orgName / isOwner (1=主账号, 0=子账号)
-->
<select id="selectMySponsorCompany" parameterType="Long" resultType="java.util.LinkedHashMap">
select o.org_id as orgId,
o.org_name as orgName,
case when p.user_id is null then 1 else 0 end as isOwner
from sys_user u
left join biz_person p on p.user_id = u.user_id and p.unit_type = 'sponsor'
left join biz_org own on own.user_id = u.user_id and own.org_type = 'sponsor'
left join biz_org o on o.org_id = COALESCE(own.org_id, p.org_id)
where u.user_id = #{userId}
limit 1
</select>
</mapper>
@@ -6,13 +6,16 @@
<result property="projectNo" column="project_no" />
<result property="projectName" column="project_name" />
<result property="totalSessions" column="total_sessions" />
<result property="assignedSessions" column="assigned_sessions" />
<result property="assignedAmount" column="assigned_amount" />
<result property="doneSessions" column="done_sessions" />
<result property="todoSessions" column="todo_sessions" />
<result property="totalAmount" column="total_amount" />
<result property="availableAmount" column="available_amount" />
<result property="paidLaborAmount" column="paid_labor_amount" />
<result property="paidMeetingAmount" column="paid_meeting_amount" />
<result property="ratingScore" column="rating_score" />
<result property="managerScore" column="manager_score" />
<result property="sponsorScore" column="sponsor_score" />
<result property="sponsorAdminUserName" column="sponsor_admin_user_name" />
<result property="sponsorAdminUserId" column="sponsor_admin_user_id" />
<result property="leadUserId" column="lead_user_id" />
@@ -38,7 +41,7 @@
<result property="publishUrl" column="publish_url" />
</resultMap>
<sql id="selectFields">
select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount, p.rating_score, p.sponsor_admin_user_name, p.project_form, p.is_finished, p.is_settled, p.sponsor_admin_user_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_time, p.update_by, p.update_time, p.manage_fee, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url,
select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount, p.manager_score, p.sponsor_score, p.sponsor_admin_user_name, p.project_form, p.is_finished, p.is_settled, p.sponsor_admin_user_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_time, p.update_by, p.update_time, p.manage_fee, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url,
o.org_name as sponsor_org_name,
lu.user_name as lead_user_name,
(select group_concat(distinct o2.org_name separator ',')
@@ -54,7 +57,7 @@
<sql id="selectFieldsForSponsor">
select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions,
p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount,
p.rating_score, p.sponsor_admin_user_name, p.project_form, p.is_finished, p.is_settled,
p.manager_score, p.sponsor_score, p.sponsor_admin_user_name, p.project_form, p.is_finished, p.is_settled,
p.sponsor_admin_user_id, p.lead_user_id, p.is_bid_project,
p.create_by, p.create_time, p.update_by, p.update_time, p.manage_fee,
p.start_time, p.end_time, p.submit_deadline_days,
@@ -84,6 +87,48 @@
#{id}
</foreach>
</if>
<if test="params.sponsorAdminUserId != null">and p.sponsor_admin_user_id = #{params.sponsorAdminUserId}</if>
<if test="projectNo != null and projectNo != ''">and p.project_no like concat('%', #{projectNo}, '%')</if>
<if test="projectName != null and projectName != ''">and p.project_name like concat('%', #{projectName}, '%')</if>
<if test="startTime != null and startTime != ''">and p.start_time &gt;= #{startTime}</if>
<if test="endTime != null and endTime != ''">and p.end_time &lt;= #{endTime}</if>
<if test="isFinished != null and isFinished != ''">and p.is_finished = #{isFinished}</if>
</where>
order by p.project_id desc
</select>
<!--
executor 专属列表: 只查 biz_project_assign 里分给当前用户的项目
匹配规则 (任一):
1) a.exec_user_id = 当前 user_id (子账号直接被指派)
2) a.execution_unit_id = 当前 user_id 对应的 executor biz_org.org_id (主账号整公司被指派)
注: executor 角色无评分/聚合分需求, 复用 selectFields (含 sponsor_score / manager_score)
-->
<select id="selectExecutorList" resultMap="BizProjectResult" parameterType="BizProject">
select distinct p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount, p.manager_score, p.sponsor_score, p.sponsor_admin_user_name, p.project_form, p.is_finished, p.is_settled, p.sponsor_admin_user_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_time, p.update_by, p.update_time, p.manage_fee, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url,
o.org_name as sponsor_org_name,
lu.user_name as lead_user_name,
(select group_concat(distinct o2.org_name separator ',')
from biz_project_assign bpa2
join biz_org o2 on o2.org_id = bpa2.execution_unit_id and o2.org_type = 'executor'
where bpa2.project_id = p.project_id) as exec_org_names,
(select coalesce(sum(bpa3.sessions), 0)
from biz_project_assign bpa3
where bpa3.project_id = p.project_id
and (bpa3.exec_user_id = #{params.executorUserId}
or bpa3.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor'))) as assigned_sessions,
(select coalesce(sum(bpa4.amount), 0)
from biz_project_assign bpa4
where bpa4.project_id = p.project_id
and (bpa4.exec_user_id = #{params.executorUserId}
or bpa4.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor'))) as assigned_amount
from biz_project p
join biz_project_assign a on a.project_id = p.project_id
left join biz_org o on o.user_id = p.sponsor_admin_user_id and o.org_type = 'sponsor'
left join sys_user lu on lu.user_id = p.lead_user_id
<where>
(a.exec_user_id = #{params.executorUserId}
or a.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor'))
<if test="projectNo != null and projectNo != ''">and p.project_no like concat('%', #{projectNo}, '%')</if>
<if test="projectName != null and projectName != ''">and p.project_name like concat('%', #{projectName}, '%')</if>
<if test="startTime != null and startTime != ''">and p.start_time &gt;= #{startTime}</if>
@@ -149,7 +194,8 @@
<if test="availableAmount != null">available_amount,</if>
<if test="paidLaborAmount != null">paid_labor_amount,</if>
<if test="paidMeetingAmount != null">paid_meeting_amount,</if>
<if test="ratingScore != null">rating_score,</if>
<if test="managerScore != null">manager_score,</if>
<if test="sponsorScore != null">sponsor_score,</if>
<if test="sponsorAdminUserName != null and sponsorAdminUserName != ''">sponsor_admin_user_name,</if>
<if test="projectForm != null and projectForm != ''">project_form,</if>
<if test="isFinished != null and isFinished != ''">is_finished,</if>
@@ -182,7 +228,8 @@
<if test="availableAmount != null">#{availableAmount},</if>
<if test="paidLaborAmount != null">#{paidLaborAmount},</if>
<if test="paidMeetingAmount != null">#{paidMeetingAmount},</if>
<if test="ratingScore != null">#{ratingScore},</if>
<if test="managerScore != null">#{managerScore},</if>
<if test="sponsorScore != null">#{sponsorScore},</if>
<if test="sponsorAdminUserName != null and sponsorAdminUserName != ''">#{sponsorAdminUserName},</if>
<if test="projectForm != null and projectForm != ''">#{projectForm},</if>
<if test="isFinished != null and isFinished != ''">#{isFinished},</if>
@@ -226,7 +273,8 @@
<if test="availableAmount != null">available_amount = #{availableAmount},</if>
<if test="paidLaborAmount != null">paid_labor_amount = #{paidLaborAmount},</if>
<if test="paidMeetingAmount != null">paid_meeting_amount = #{paidMeetingAmount},</if>
<if test="ratingScore != null">rating_score = #{ratingScore},</if>
<if test="managerScore != null">manager_score = #{managerScore},</if>
<if test="sponsorScore != null">sponsor_score = #{sponsorScore},</if>
<if test="sponsorAdminUserName != null and sponsorAdminUserName != ''">sponsor_admin_user_name = #{sponsorAdminUserName},</if>
<if test="projectForm != null and projectForm != ''">project_form = #{projectForm},</if>
<if test="isFinished != null and isFinished != ''">is_finished = #{isFinished},</if>
@@ -11,11 +11,13 @@
<result property="responseScore" column="response_score" />
<result property="cooperationScore" column="cooperation_score" />
<result property="complianceScore" column="compliance_score" />
<result property="totalScore" column="total_score" />
<result property="remark" column="remark" />
<result property="projectId" column="project_id" />
<result property="raterId" column="rater_id" />
<result property="ratingTime" column="rating_time" />
</resultMap>
<sql id="selectFields">
select rating_id, project_no, project_name, rater_name, rater_role, quality_score, response_score, cooperation_score, compliance_score, total_score, remark, create_by, create_time, update_by, update_time
select rating_id, project_id, project_no, project_name, rater_id, rater_name, rater_role, quality_score, response_score, cooperation_score, compliance_score, remark, rating_time, create_by, create_time, update_by, update_time
from biz_project_rating
</sql>
<select id="selectByPrimaryKey" resultMap="BizProjectRatingResult" parameterType="String">
@@ -25,6 +27,9 @@
<select id="selectList" resultMap="BizProjectRatingResult" parameterType="BizProjectRating">
<include refid="selectFields"/>
<where>
<if test="projectId != null"> and project_id = #{projectId}</if>
<if test="raterId != null"> and rater_id = #{raterId}</if>
<if test="raterRole != null and raterRole != ''"> and rater_role = #{raterRole}</if>
<if test="remark != null and remark != ''"> and remark = #{remark}</if>
</where>
order by rating_id desc
@@ -49,11 +54,10 @@
<if test="raterId != null and raterId != ''">rater_id = #{raterId},</if>
<if test="raterName != null and raterName != ''">rater_name = #{raterName},</if>
<if test="raterRole != null and raterRole != ''">rater_role = #{raterRole},</if>
<if test="qualityScore != null and qualityScore != ''">quality_score = #{qualityScore},</if>
<if test="responseScore != null and responseScore != ''">response_score = #{responseScore},</if>
<if test="cooperationScore != null and cooperationScore != ''">cooperation_score = #{cooperationScore},</if>
<if test="complianceScore != null and complianceScore != ''">compliance_score = #{complianceScore},</if>
<if test="totalScore != null and totalScore != ''">total_score = #{totalScore},</if>
<if test="qualityScore != null">quality_score = #{qualityScore},</if>
<if test="responseScore != null">response_score = #{responseScore},</if>
<if test="cooperationScore != null">cooperation_score = #{cooperationScore},</if>
<if test="complianceScore != null">compliance_score = #{complianceScore},</if>
<if test="ratingTime != null and ratingTime != ''">rating_time = #{ratingTime},</if>
<if test="remark != null">remark = #{remark},</if>
</trim>
@@ -73,11 +77,11 @@
<insert id="upsertRating" parameterType="BizProjectRating">
INSERT INTO biz_project_rating
(project_id, project_no, project_name, rater_id, rater_name, rater_role,
quality_score, response_score, cooperation_score, compliance_score, total_score,
quality_score, response_score, cooperation_score, compliance_score,
remark, create_by, create_time, update_by, update_time)
VALUES
(#{projectId}, #{projectNo}, #{projectName}, #{raterId}, #{raterName}, #{raterRole},
#{qualityScore}, #{responseScore}, #{cooperationScore}, #{complianceScore}, #{totalScore},
#{qualityScore}, #{responseScore}, #{cooperationScore}, #{complianceScore},
#{remark}, #{createBy}, sysdate(), #{updateBy}, sysdate())
ON DUPLICATE KEY UPDATE
project_no = VALUES(project_no),
@@ -87,7 +91,6 @@
response_score = VALUES(response_score),
cooperation_score= VALUES(cooperation_score),
compliance_score = VALUES(compliance_score),
total_score = VALUES(total_score),
remark = VALUES(remark),
update_by = VALUES(update_by),
update_time = sysdate()
+34 -7
View File
@@ -2,19 +2,29 @@ import request from '@/utils/request'
// 字典类型常量 (前端统一引用,避免拼写错误)
export const DICT_TYPE = {
DEPARTMENT: 'department', // 科室
TITLE: 'title' // 医生职称
DEPARTMENT: 'department', // 科室 (biz 表)
TITLE: 'title', // 医生职称 (biz 表)
PROJECT_CATEGORY: 'biz_project_category' // 项目类别 (sys_dict_data)
}
// 走 RuoYi sys_dict_type + sys_dict_data 的字典类型 (管理员可维护)
const SYS_DICT_TYPES = new Set([
DICT_TYPE.PROJECT_CATEGORY
])
/**
* 拉取某类型字典的启用项 (匿名可访问)
* @param {string} type - DICT_TYPE.*
* @returns {Promise<{value: number, label: string, raw: object}[]>}
* - value: deptId/titleId
* - label: name
* - raw: 完整字段 (含 sort/status/...)
* @returns {Promise<{value: number|string, label: string, raw: object}[]>}
*/
export async function getActiveDict(type) {
if (SYS_DICT_TYPES.has(type)) {
// 走 RuoYi sys_dict_data, 仅取启用项
const r = await request({ url: '/system/dict/data/list', method: 'get', params: { dictType: type, status: '0' } })
const raw = (r.data && r.data.rows) || r.rows || []
return raw.map(item => ({ value: item.dictValue, label: item.dictLabel, raw: item }))
}
// biz 表字典
const url = `/business/dict/${type}/active`
const r = await request({ url, method: 'get' })
const raw = r.data || r || []
@@ -26,7 +36,7 @@ export async function getActiveDict(type) {
}))
}
// ===== 管理员维护接口 =====
// ===== 管理员维护接口 (biz 表字典) =====
export function listDepartments(query) {
return request({ url: '/business/dict/department/list', method: 'get', params: query })
}
@@ -51,4 +61,21 @@ export function updateTitle(data) {
}
export function deleteTitles(ids) {
return request({ url: `/business/dict/title/${ids}`, method: 'delete' })
}
// ===== 管理员维护接口 (sys_dict_data, 例如项目类别) =====
export function listSysDictItems(query) {
return request({ url: '/system/dict/data/list', method: 'get', params: query })
}
export function addSysDictItem(data) {
return request({ url: '/system/dict/data', method: 'post', data })
}
export function updateSysDictItem(data) {
return request({ url: '/system/dict/data', method: 'put', data })
}
export function deleteSysDictItems(ids) {
return request({ url: `/system/dict/data/${ids}`, method: 'delete' })
}
export function getSysDictItem(dictCode) {
return request({ url: `/system/dict/data/${dictCode}`, method: 'get' })
}
+4 -3
View File
@@ -55,7 +55,7 @@ import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/store/user'
import { logout as logoutApi } from '@/api/auth'
import { House, Document, Calendar, User, List, OfficeBuilding, Setting, Bell, EditPen, DataAnalysis, Tickets, CaretBottom, Folder, Medal, UserFilled, Connection, Box, Star, Grid, Files, Compass } from '@element-plus/icons-vue'
import { House, Document, Calendar, User, List, OfficeBuilding, Setting, Bell, EditPen, DataAnalysis, Tickets, CaretBottom, Folder, Medal, UserFilled, Connection, Box, Star, Grid, Files, Compass, Collection } from '@element-plus/icons-vue'
const route = useRoute()
const router = useRouter()
@@ -77,11 +77,12 @@ const MENU = {
{ path: '/admin/sponsor-orgs', title: '支持单位管理', icon: Star },
{ path: '/admin/executor-orgs', title: '服务机构管理', icon: OfficeBuilding },
{ path: '/admin/department', title: '科室管理', icon: Grid },
{ path: '/admin/title', title: '职称管理', icon: Medal }
{ path: '/admin/title', title: '职称管理', icon: Medal },
{ path: '/admin/project-category', title: '项目类别管理', icon: Collection }
]},
{ path: '/admin/manage', title: '网站管理', icon: Setting, children: [
{ path: '/admin/article', title: '协议管理', icon: Files },
{ path: '/admin/special-plan', title: '七大专项计划', icon: Compass }
{ path: '/admin/special-plan', title: '专项计划管理', icon: Compass }
]},
{ path: '/admin/account', title: '账号信息', icon: User }
],
@@ -1,6 +1,6 @@
<template>
<div class="page-card admin-plan">
<div class="breadcrumb">首页 / 网站管理 / 七大专项计划</div>
<div class="breadcrumb">首页 / 网站管理 / 专项计划管理</div>
<!-- 筛选 -->
<el-form inline :model="q" class="filter-form">
+187
View File
@@ -0,0 +1,187 @@
<template>
<div class="page-card admin-project-category">
<div class="breadcrumb">首页 / 资料库管理 / 项目类别管理</div>
<el-form inline :model="q" class="filter-form">
<el-form-item label="类别名称"><el-input v-model="q.dictLabel" placeholder="输入名称模糊搜索" clearable /></el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部状态" clearable style="width:140px">
<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 type="index" label="序号" width="60" align="center" />
<el-table-column prop="dictLabel" label="类别名称" min-width="200" />
<el-table-column prop="dictSort" 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="240" 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
v-model:current-page="q.pageNum"
v-model:page-size="q.pageSize"
:total="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="480px">
<el-form :model="form" label-width="100px">
<el-form-item label="类别名称">
<el-input v-model="form.dictLabel" placeholder="请输入显示名称 (如 共识)" maxlength="32" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.dictSort" :min="0" :max="999" />
</el-form-item>
<el-form-item v-if="isEdit" label="状态">
<el-radio-group v-model="form.status">
<el-radio value="0">启用</el-radio>
<el-radio value="1">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { listSysDictItems, addSysDictItem, updateSysDictItem, deleteSysDictItems, DICT_TYPE } from '@/api/dict'
const q = reactive({ dictLabel: '', status: '', pageNum: 1, pageSize: 20 })
const rows = ref([])
const total = ref(0)
const loading = ref(false)
async function load() {
loading.value = true
try {
const r = await listSysDictItems({ ...q, dictType: DICT_TYPE.PROJECT_CATEGORY })
rows.value = (r.data && r.data.rows) || r.rows || []
total.value = (r.data && r.data.total) || r.total || 0
} finally { loading.value = false }
}
function reset() {
Object.assign(q, { dictLabel: '', status: '', pageNum: 1 })
load()
}
const dialogVisible = ref(false)
const isEdit = ref(false)
const saving = ref(false)
const form = reactive({ dictCode: null, dictLabel: '', dictSort: 0, status: '0' })
function openAdd() {
isEdit.value = false
Object.assign(form, { dictCode: null, dictLabel: '', dictSort: 0, status: '0' })
dialogVisible.value = true
}
function openEdit(row) {
isEdit.value = true
Object.assign(form, {
dictCode: row.dictCode,
dictLabel: row.dictLabel,
dictSort: row.dictSort || 0,
status: row.status
})
dialogVisible.value = true
}
async function save() {
if (!form.dictLabel.trim()) return ElMessage.warning('请输入类别名称')
saving.value = true
try {
const label = form.dictLabel.trim()
const payload = {
dictType: DICT_TYPE.PROJECT_CATEGORY,
dictLabel: label,
dictValue: label, // 类别值始终与名称一致
dictSort: form.dictSort
}
if (isEdit.value) {
payload.dictCode = form.dictCode
payload.status = form.status
await updateSysDictItem(payload)
} else {
await addSysDictItem(payload)
}
ElMessage.success('保存成功')
dialogVisible.value = false
load()
} catch (e) {
ElMessage.error(e?.msg || '保存失败')
} finally { saving.value = false }
}
async function toggleStatus(row) {
const newStatus = row.status === '0' ? '1' : '0'
try {
await updateSysDictItem({
dictCode: row.dictCode,
dictType: row.dictType,
dictLabel: row.dictLabel,
dictValue: row.dictLabel, // 与名称保持一致
dictSort: row.dictSort,
status: newStatus
})
ElMessage.success('已' + (newStatus === '0' ? '启用' : '停用'))
load()
} catch (e) {
ElMessage.error(e?.msg || '操作失败')
}
}
async function removeOne(row) {
try {
await ElMessageBox.confirm(`确定删除项目类别"${row.dictLabel}"? 该操作不可恢复`, '删除确认', { type: 'warning' })
} catch { return }
try {
await deleteSysDictItems(row.dictCode)
ElMessage.success('删除成功')
load()
} catch (e) {
ElMessage.error(e?.msg || '删除失败')
}
}
onMounted(load)
</script>
<style scoped>
.admin-project-category { 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>
+1 -1
View File
@@ -51,7 +51,7 @@ async function loadStats() {
try {
const ps = await bizList('project', { pageNum: 1, pageSize: 1 })
stats.value.totalProjects = ps.total || 0
const settled = await bizList('project', { isSettled: '1', pageNum: 1, pageSize: 1 })
const settled = await bizList('project', { isSettled: 'Y', pageNum: 1, pageSize: 1 })
stats.value.settledProjects = settled.total || 0
const finished = await bizList('project', { isFinished: '1', pageNum: 1, pageSize: 1 })
stats.value.finishedProjects = finished.total || 0
+25 -68
View File
@@ -25,7 +25,6 @@
<!-- ========== 批量按钮 ( 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>
@@ -35,12 +34,12 @@
<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>
<template #default="{ row }">{{ row.assignedSessions || 0 }}/{{ row.assignedSessions || 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>
<template #default="{ row }">¥ {{ formatNum(row.assignedAmount) }}</template>
</el-table-column>
<el-table-column label="可用金额" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.availableAmount) }}</template>
@@ -51,9 +50,16 @@
<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">
<el-table-column prop="managerScore" label="执行单位得分(合规)" width="140" align="center">
<template #default="{ row }">
<el-rate v-model="row.ratingScore" disabled show-score :max="5" />
<span v-if="row.managerScore != null">{{ row.managerScore }}</span>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column prop="sponsorScore" label="执行单位得分(支持)" width="140" align="center">
<template #default="{ row }">
<span v-if="row.sponsorScore != null">{{ row.sponsorScore }}</span>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column prop="isFinished" label="是否结题" width="100" align="center">
@@ -65,11 +71,10 @@
</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">
<el-table-column label="操作" width="180" 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>
@@ -97,27 +102,7 @@
</template>
</el-dialog>
<!-- 批量评价 弹窗 -->
<el-dialog v-model="rateOpen" :title="rateRow ? '项目评价' : '批量评价'" width="520px">
<el-form label-width="100px">
<el-form-item v-if="rateRow" label="项目名称">
<span>{{ rateRow.projectName }}</span>
</el-form-item>
<el-form-item v-else label="已选项目">
<span>{{ selected.length }} </span>
</el-form-item>
<el-form-item label="评分">
<el-rate v-model="rateForm.score" :max="5" show-text />
</el-form-item>
<el-form-item label="评价说明">
<el-input v-model="rateForm.comment" type="textarea" :rows="3" placeholder="请输入评价说明" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="rateOpen=false">取消</el-button>
<el-button type="primary" @click="onSaveRate">确定</el-button>
</template>
</el-dialog>
<!-- 评分弹窗已移除: executor 不可评分 -->
<el-dialog v-model="detailOpen" title="项目详情" width="720px">
<el-descriptions :column="2" border>
@@ -143,7 +128,14 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { bizList, bizUpdate } from '@/api/public'
import request from '@/utils/request'
import { bizUpdate } from '@/api/public'
// executor 专属: 只查分给当前用户 (主账号 biz_org.org_id 或 子账号 exec_user_id) 的项目
async function listExecutorProjects(params) {
const { data } = await request.get('/business/project/executorList', { params })
return data || { rows: [], total: 0 }
}
const list = ref([])
const loading = ref(false)
@@ -159,10 +151,6 @@ const assignOpen = ref(false)
const assignRow = ref({})
const assignForm = reactive({ assignee: '' })
const rateOpen = ref(false)
const rateRow = ref(null)
const rateForm = reactive({ score: 5, comment: '' })
function formatNum(n) {
if (n == null) return '0.00'
return Number(n).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
@@ -176,9 +164,9 @@ async function loadList() {
params.startTime = dateRange.value[0]
params.endTime = dateRange.value[1]
}
const r = await bizList('project', params)
list.value = (r.data && r.data.rows) || r.rows || []
page.total = (r.data && r.data.total) || r.total || 0
const data = await listExecutorProjects(params)
list.value = data.rows || []
page.total = data.total || 0
} catch (e) { list.value = []; page.total = 0 }
finally { loading.value = false }
}
@@ -217,38 +205,7 @@ async function onSaveAssign() {
} catch (e) { ElMessage.error(e?.msg || '分配失败') }
}
function onRate(row) {
rateRow.value = row
rateForm.score = row?.ratingScore || 5
rateForm.comment = row?.ratingComment || ''
rateOpen.value = true
}
function onBatchRate() {
if (!selected.value.length) return
rateRow.value = null
rateForm.score = 5
rateForm.comment = ''
rateOpen.value = true
}
async function onSaveRate() {
try {
let ok = 0
const payload = { ratingScore: rateForm.score, ratingComment: rateForm.comment }
if (rateRow.value) {
await bizUpdate('project', { projectId: rateRow.value.projectId, ...payload })
ok = 1
} else {
for (const r of selected.value) {
try { await bizUpdate('project', { projectId: r.projectId, ...payload }); ok++ } catch {}
}
}
ElMessage.success(rateRow.value ? '评价成功' : `已评价 ${ok} 个项目`)
rateOpen.value = false
loadList()
} catch (e) { ElMessage.error(e?.msg || '评价失败') }
}
// 评价相关函数已移除: executor 不可评分
onMounted(() => { loadList(); loadPeople() })
</script>
+1 -1
View File
@@ -14,7 +14,7 @@
<div class="stat-value">{{ stats.totalProjects }}</div>
<div class="stat-desc">查看全部项目</div>
</router-link>
<router-link class="stat-card" to="/leader/projects?isSettled=1">
<router-link class="stat-card" to="/leader/projects?isSettled=Y">
<div class="stat-label">已结算数量</div>
<div class="stat-value">{{ stats.settledProjects }}</div>
<div class="stat-desc">查看已结算项目</div>
+2 -2
View File
@@ -50,8 +50,8 @@
<el-table-column prop="currentStage" label="当前阶段" width="100" align="center" />
<el-table-column label="是否结算" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.isSettled === '1' ? 'success' : 'info'" size="small">
{{ row.isSettled === '1' ? '已结算' : '未结算' }}
<el-tag :type="row.isSettled === 'Y' ? 'success' : 'info'" size="small">
{{ row.isSettled === 'Y' ? '已结算' : '未结算' }}
</el-tag>
</template>
</el-table-column>
+10 -86
View File
@@ -38,8 +38,8 @@
</el-form-item>
<el-form-item label="是否结算">
<el-select v-model="q.isSettled" placeholder="全部" clearable style="width:100px">
<el-option label="已结算" value="1" />
<el-option label="未结算" value="0" />
<el-option label="已结算" value="Y" />
<el-option label="未结算" value="N" />
</el-select>
</el-form-item>
<el-form-item>
@@ -53,7 +53,7 @@
<!-- 批量按钮 (按原型 left 顺序): 项目分配 / 批量评价 -->
<div class="batch-bar">
<el-button type="primary" @click="openBatchAssign">项目分配</el-button>
<el-button type="primary" :disabled="!selected.length" @click="onBatchRate">批量评价</el-button>
<!-- 评价入口仅 manager / sponsor 可见, leader 不可评分 -->
<span class="hint">已选 {{ selected.length }} </span>
</div>
@@ -80,7 +80,7 @@
<el-table-column label="已支付会务费" width="120" align="right">
<template #default="{ row }">¥{{ formatMoney(row.paidMeetingAmount) }}</template>
</el-table-column>
<el-table-column prop="ratingScore" label="执行单位评价(支持方)" width="150" align="center" />
<el-table-column prop="managerScore" label="执行单位评价(合规)" width="150" align="center" />
<el-table-column label="是否结题" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.isFinished === '1' ? 'success' : 'info'" size="small">
@@ -90,12 +90,11 @@
</el-table-column>
<el-table-column prop="startTime" label="项目开始时间" min-width="160" />
<el-table-column prop="endTime" label="项目结束时间" min-width="160" />
<!-- 操作列按原型 left 顺序: 查看 / 分配 / 项目评价 -->
<el-table-column label="操作" width="240" fixed="right">
<!-- 操作列按原型 left 顺序: 查看 / 分配 -->
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openDetail(row)">查看</el-button>
<el-button size="small" link type="primary" @click="openAssign(row)">分配</el-button>
<el-button size="small" link type="primary" @click="openRate(row)">项目评价</el-button>
</template>
</el-table-column>
</el-table>
@@ -118,7 +117,7 @@
<el-descriptions-item label="可用金额">¥{{ formatMoney(detail.availableAmount) }}</el-descriptions-item>
<el-descriptions-item label="已支付劳务费">¥{{ formatMoney(detail.paidLaborAmount) }}</el-descriptions-item>
<el-descriptions-item label="已支付会务费">¥{{ formatMoney(detail.paidMeetingAmount) }}</el-descriptions-item>
<el-descriptions-item label="执行单位评价(支持方)">{{ detail.ratingScore || '-' }}</el-descriptions-item>
<el-descriptions-item label="执行单位评价(合规)">{{ detail.managerScore || '-' }}</el-descriptions-item>
<el-descriptions-item label="是否结题">
<el-tag :type="detail.isFinished === '1' ? 'success' : 'info'">
{{ detail.isFinished === '1' ? '已结题' : '未结题' }}
@@ -154,31 +153,7 @@
</template>
</el-dialog>
<!-- 项目评价 dialog -->
<el-dialog v-model="rateOpen" :title="rateMode==='batch' ? '批量项目评价' : '项目评价'" width="520px">
<el-form :model="rateForm" label-width="100px">
<el-form-item v-if="rateMode==='single'" label="项目编号 / 名称">
<span>{{ rateForm.projectNo }} / {{ rateForm.projectName }}</span>
</el-form-item>
<el-form-item v-else label="评价项目">
<span> {{ rateForm.projectIds.length }} 个项目</span>
</el-form-item>
<el-form-item label="评价维度">
<div class="score-row"><span style="width:90px">履约质量:</span><el-rate v-model="rateForm.q1" :max="5" /></div>
<div class="score-row"><span style="width:90px">时效响应:</span><el-rate v-model="rateForm.q2" :max="5" /></div>
<div class="score-row"><span style="width:90px">配合度:</span><el-rate v-model="rateForm.q3" :max="5" /></div>
<div class="score-row"><span style="width:90px">合规安全:</span><el-rate v-model="rateForm.q4" :max="5" /></div>
</el-form-item>
<el-form-item label="评价备注">
<el-input v-model="rateForm.remark" type="textarea" rows="3" placeholder="可选" />
</el-form-item>
</el-form>
<p class="dialog-hint">*评价标准与供应商系统保持一致</p>
<template #footer>
<el-button @click="rateOpen = false">取消</el-button>
<el-button type="primary" @click="submitRate">提交评价</el-button>
</template>
</el-dialog>
<!-- 项目评价 dialog 已移除: leader 不可评分 -->
</div>
</template>
@@ -205,13 +180,7 @@ const assignForm = reactive({
})
const personList = ref([])
// 评价
const rateOpen = ref(false)
const rateMode = ref('single') // 'single' | 'batch'
const rateForm = reactive({
projectId: null, projectNo: '', projectName: '',
projectIds: [], q1: 0, q2: 0, q3: 0, q4: 0, remark: ''
})
// 评价相关状态已移除: leader 不可评分
function formatMoney(v) { return Number(v || 0).toFixed(2) }
@@ -289,52 +258,7 @@ async function submitAssign() {
load()
}
// ─── 项目评价 ───────────────────────────────────────────
function openRate(row) {
rateMode.value = 'single'
Object.assign(rateForm, {
projectId: row.projectId,
projectNo: row.projectNo,
projectName: row.projectName,
projectIds: [],
q1: 0, q2: 0, q3: 0, q4: 0,
remark: ''
})
rateOpen.value = true
}
async function onBatchRate() {
if (!selected.value.length) return
rateMode.value = 'batch'
Object.assign(rateForm, {
projectId: null, projectNo: '', projectName: '',
projectIds: selected.value.map(r => r.projectId),
q1: 3, q2: 3, q3: 3, q4: 3,
remark: '批量评价'
})
rateOpen.value = true
}
async function submitRate() {
if (!rateForm.q1 && !rateForm.q2 && !rateForm.q3 && !rateForm.q4) {
ElMessage.warning('请至少完成一项评分'); return
}
const items = rateMode.value === 'single'
? [{ ...rateForm }]
: rateForm.projectIds.map(pid => ({
projectId: pid,
q1: rateForm.q1, q2: rateForm.q2,
q3: rateForm.q3, q4: rateForm.q4,
remark: rateForm.remark
}))
let ok = 0
for (const it of items) {
try { await bizAdd('projectRating', it); ok++ } catch (e) { /* 跳过失败项 */ }
}
ElMessage.success(`已提交评价 ${ok}`)
rateOpen.value = false
load()
}
// 项目评价相关函数已移除: leader 不可评分
onMounted(() => { load(); loadPersons() })
</script>
@@ -30,7 +30,7 @@
</el-row>
<el-row :gutter="12">
<el-col :span="12"><el-form-item label="支持公司"><span class="info-value">{{ row.sponsorAdminUserName || '-' }}</span></el-form-item></el-col>
<el-col :span="12"><el-form-item label="项目评价"><a class="op-link" href="javascript:void(0)" @click="openScoreDialog">{{ fmtScore(row.ratingScore) }}</a></el-form-item></el-col>
<el-col :span="12"><el-form-item label="项目评价"><a class="op-link" href="javascript:void(0)" @click="openScoreDialog">{{ fmtScore(row.managerScore) }}</a></el-form-item></el-col>
</el-row>
</el-form>
@@ -360,7 +360,7 @@ async function submitScore() {
scoreSpeed: scoreForm.scoreSpeed,
scoreCoordination: scoreForm.scoreCoordination,
scoreCompliance: scoreForm.scoreCompliance,
ratingScore: totalScore.value
managerScore: totalScore.value
})
ElMessage.success('评分已保存')
scoreOpen.value = false
+52 -77
View File
@@ -1,34 +1,27 @@
<template>
<div class="page-card manager-plans">
<div class="breadcrumb">首页 / 项目策划方案</div>
<div class="hint-list">
<p>*批量审核策划案名称投稿ID审核结果审核意见只能审核不通过</p>
</div>
<!-- ========== 筛选区 ( 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 v-model="q.planDirectionId" placeholder="请选择" clearable style="width:200px">
<el-option v-for="opt in planDirectionOptions" :key="opt.id" :label="opt.title" :value="opt.id" />
</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>
<dict-select v-model="q.planCategory" dict-type="biz_project_category" value-field="label" style="width:160px" />
</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 v-model="q.status" placeholder="请选择" clearable style="width:140px">
<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.isSettled" placeholder="请选择" clearable>
<el-option label="已结算" value="1" /><el-option label="未结算" value="0" />
<el-select v-model="q.isSettled" placeholder="请选择" clearable style="width:140px">
<el-option label="已结算" value="Y" /><el-option label="未结算" value="N" />
</el-select>
</el-form-item>
<el-form-item label="备注"><el-input v-model="q.remark" placeholder="输入备注" clearable /></el-form-item>
@@ -40,7 +33,6 @@
<!-- ========== 批量按钮 ( 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>
@@ -49,7 +41,7 @@
<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="planDirectionTitle" 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">
@@ -63,15 +55,13 @@
</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>
<audit-status-tag :status="row.status" />
</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 size="small" :type="row.isSettled==='Y' ? 'success' : 'warning'">
{{ row.isSettled==='Y' ? '已结算' : '未结算' }}
</el-tag>
</template>
</el-table-column>
@@ -81,7 +71,7 @@
<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" @click="onSettle(row)" v-if="row.isSettled!=='Y'">结算</el-button>
<el-button link type="primary" v-if="row.designFileUrl" @click="downloadFile(row)">下载</el-button>
</template>
</el-table-column>
@@ -90,19 +80,17 @@
<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>
<!-- 审核 dialog (按原型: 通过/退回 radio + 项目编号通过时必填 + 审核意见 + 确认/取消) -->
<!-- 审核 dialog: 策划案名称 + 投稿ID + 审核结果(固定 退回) + 审核意见 -->
<el-dialog v-model="auditOpen" title="审核策划方案" width="500px">
<el-form :model="auditForm" :rules="auditRules" ref="auditFormRef" label-width="100px">
<el-form-item label="案名称"><span>{{ auditForm.planName }}</span></el-form-item>
<el-form-item label="策划案名称"><span>{{ auditForm.planName }}</span></el-form-item>
<el-form-item label="投稿ID"><span>{{ auditForm.planId }}</span></el-form-item>
<el-form-item label="审核结果">
<el-radio-group v-model="auditForm.result">
<el-radio label="1">通过</el-radio><el-radio label="2">退回</el-radio>
</el-radio-group>
<span style="color:#f56c6c">退回审核不通过</span>
</el-form-item>
<el-form-item v-if="auditForm.result==='1'" label="项目编号" prop="projectNo">
<el-input v-model="auditForm.projectNo" placeholder="通过时项目编号为必填" />
<el-form-item label="审核意见" prop="opinion">
<el-input v-model="auditForm.opinion" type="textarea" rows="3" placeholder="请填写审核不通过原因" />
</el-form-item>
<el-form-item label="审核意见"><el-input v-model="auditForm.opinion" type="textarea" rows="3" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="auditOpen=false">取消</el-button>
@@ -116,16 +104,12 @@
<el-form-item label="方案名称" prop="planName"><el-input v-model="editForm.planName" /></el-form-item>
<el-form-item label="项目编号" prop="projectNo"><el-input v-model="editForm.projectNo" /></el-form-item>
<el-form-item label="项目方向">
<el-select v-model="editForm.planDirection" placeholder="请选择" style="width:100%">
<el-option label="专项计划一" value="专项计划一" /><el-option label="专项计划二" value="专项计划二" />
<el-select v-model="editForm.planDirectionId" placeholder="请选择" style="width:100%">
<el-option v-for="opt in planDirectionOptions" :key="opt.id" :label="opt.title" :value="opt.id" />
</el-select>
</el-form-item>
<el-form-item label="项目类别">
<el-select v-model="editForm.planCategory" 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>
<dict-select v-model="editForm.planCategory" dict-type="biz_project_category" value-field="label" style="width:100%" />
</el-form-item>
<el-form-item label="项目形式">
<el-select v-model="editForm.projectForm" placeholder="请选择" style="width:100%">
@@ -159,10 +143,22 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { bizList, bizUpdate } from '@/api/public'
import request from '@/utils/request'
import { ElMessage } from 'element-plus'
import Preview from '@/components/Preview.vue'
import DictSelect from '@/components/DictSelect.vue'
import AuditStatusTag from '@/components/AuditStatusTag.vue'
const q = ref({ planName: '', projectNo: '', planDirection: '', planCategory: '', status: '', isSettled: '', remark: '' })
// 专项计划下拉选项 (项目方向): [{id, title}]
const planDirectionOptions = ref([])
async function loadPlanDirectionOptions() {
try {
const { data } = await request.get('/business/specialPlan/options')
planDirectionOptions.value = (data && data.data) || data || []
} catch { planDirectionOptions.value = [] }
}
const q = ref({ planName: '', projectNo: '', planDirectionId: '', planCategory: '', status: '', isSettled: '', remark: '' })
const rows = ref([]); const selection = ref([])
const loading = ref(false)
const submitting = ref(false)
@@ -177,28 +173,15 @@ async function load() {
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() { q.value = { planName:'', projectNo:'', planDirection:'', planCategory:'', status:'', isSettled:'', remark:'' }; page.pageNum = 1; load() }
function reset() { q.value = { planName:'', projectNo:'', planDirectionId:'', planCategory:'', status:'', isSettled:'', remark:'' }; page.pageNum = 1; load() }
function onSel(rs) { selection.value = rs }
async function batchAudit() {
if (!selection.value.length) return ElMessage.warning('请先勾选行')
let ok = 0
submitting.value = true
for (const r of selection.value) {
try { await bizUpdate('projectPlan', { planId: r.planId, status: '2', auditOpinion: '批量审核不通过' }); ok++ } catch {}
}
submitting.value = false
ElMessage.success(`批量审核 ${ok}/${selection.value.length}`)
selection.value = []
load()
}
async function batchSettle() {
if (!selection.value.length) return ElMessage.warning('请先勾选行')
submitting.value = true
let ok = 0
for (const r of selection.value) {
try { await bizUpdate('projectPlan', { planId: r.planId, isSettled: '1' }); ok++ } catch {}
try { await bizUpdate('projectPlan', { planId: r.planId, isSettled: 'Y' }); ok++ } catch {}
}
submitting.value = false
ElMessage.success(`批量结算 ${ok}/${selection.value.length}`)
@@ -208,17 +191,23 @@ async function batchSettle() {
const auditOpen = ref(false)
const auditFormRef = ref(null)
const auditForm = reactive({ planId: '', planName: '', projectNo: '', result: '1', opinion: '' })
// 审核: 只能审核不通过, 固定写死 status='2'
const auditForm = reactive({ planId: '', planName: '', opinion: '' })
const auditRules = {
projectNo: [{ required: true, message: '通过时项目编号为必填', trigger: 'blur' }]
opinion: [{ required: true, message: '请填写审核不通过原因', trigger: 'blur' }]
}
function onAudit(row) {
auditForm.planId = row.planId
auditForm.planName = row.planName
auditForm.opinion = ''
auditOpen.value = true
}
function onAudit(row) { auditForm.planId = row.planId; auditForm.planName = row.planName; auditForm.projectNo = row.projectNo || ''; auditForm.result = '1'; auditForm.opinion = ''; auditOpen.value = true }
async function submitAudit() {
try {
await auditFormRef.value.validate()
submitting.value = true
await bizUpdate('projectPlan', { planId: auditForm.planId, status: auditForm.result, auditOpinion: auditForm.opinion, projectNo: auditForm.result === '1' ? auditForm.projectNo : undefined })
ElMessage.success('审核已确认')
await bizUpdate('projectPlan', { planId: auditForm.planId, status: '3', auditOpinion: auditForm.opinion })
ElMessage.success('已退回')
auditOpen.value = false
load()
} catch (e) { if (e?.message) ElMessage.error(e.message) }
@@ -227,17 +216,17 @@ async function submitAudit() {
const editOpen = ref(false)
const editFormRef = ref(null)
const editForm = reactive({ planId:'', planName:'', projectNo:'', planDirection:'', planCategory:'', projectForm:'', designFileUrl:'', remark:'' })
const editForm = reactive({ planId:'', planName:'', projectNo:'', planDirectionId:'', planCategory:'', projectForm:'', designFileUrl:'', remark:'' })
const editRules = { planName:[{required:true,message:'请输入方案名称',trigger:'blur'}] }
function onEdit(row) {
Object.assign(editForm, {
planId: row.planId, planName: row.planName, projectNo: row.projectNo,
planDirection: row.planDirection, planCategory: row.planCategory,
planDirectionId: row.planDirectionId ?? null, planCategory: row.planCategory,
projectForm: row.projectForm, designFileUrl: row.designFileUrl || '', remark: row.remark || ''
})
editOpen.value = true
}
function resetEditForm() { Object.assign(editForm, { planId:'', planName:'', projectNo:'', planDirection:'', planCategory:'', projectForm:'', designFileUrl:'', remark:'' }) }
function resetEditForm() { Object.assign(editForm, { planId:'', planName:'', projectNo:'', planDirectionId:'', planCategory:'', projectForm:'', designFileUrl:'', remark:'' }) }
async function submitEdit() {
await editFormRef.value.validate()
try {
@@ -256,7 +245,7 @@ function onSettle(row) { settleForm.planId = row.planId; settleForm.planName = r
async function submitSettle() {
try {
submitting.value = true
await bizUpdate('projectPlan', { planId: settleForm.planId, isSettled: '1' })
await bizUpdate('projectPlan', { planId: settleForm.planId, isSettled: 'Y' })
ElMessage.success('已结算')
settleOpen.value = false
load()
@@ -275,7 +264,7 @@ function downloadFile(row) {
a.click()
}
onMounted(load)
onMounted(() => { loadPlanDirectionOptions(); load() })
</script>
<style scoped>
@@ -295,18 +284,4 @@ onMounted(load)
.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;
}
.hint-list p { margin-bottom: 4px; }
.hint-list p:last-child { margin-bottom: 0; }
</style>
+90 -60
View File
@@ -25,10 +25,10 @@
</el-form-item>
<el-form-item label="是否结算:">
<el-select v-model="q.isSettled" placeholder="请选择" clearable style="width:120px">
<el-option label="已结算" value="1" /><el-option label="未结算" value="0" />
<el-option label="已结算" value="Y" /><el-option label="未结算" value="N" />
</el-select>
</el-form-item>
<el-form-item label="评价得分:"><el-input v-model="q.ratingScore" placeholder="输入评分" clearable /></el-form-item>
<el-form-item label="评价得分:"><el-input v-model="q.managerScore" 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>
@@ -58,9 +58,15 @@
<el-table-column prop="availableAmount" label="可用金额" width="130" align="right" :formatter="fmtMoney" />
<el-table-column prop="paidLaborAmount" label="已支付劳务费" width="140" align="right" :formatter="fmtMoney" />
<el-table-column prop="paidMeetingAmount" label="已支付会务费" width="140" align="right" :formatter="fmtMoney" />
<el-table-column prop="complianceScore" label="执行单位得分(合规)" width="140" align="center">
<el-table-column prop="managerScore" label="执行单位得分(合规)" width="140" align="center">
<template #default="{ row }">
<span v-if="row.ratingScore != null">{{ row.ratingScore }}</span>
<span v-if="row.managerScore != null">{{ row.managerScore }}</span>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column prop="sponsorScore" label="执行单位得分(支持)" width="140" align="center">
<template #default="{ row }">
<span v-if="row.sponsorScore != null">{{ row.sponsorScore }}</span>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
@@ -199,10 +205,10 @@
<th style="width:130px;padding:10px 12px;text-align:center">评分标准(1~5 分)</th>
</tr></thead>
<tbody>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">履约质量</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">服务/活动效果达标度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="batchScoreForm.q1" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">时效响应</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">执行 & 售后响应速度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="batchScoreForm.q2" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">配合度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">沟通配合 & 问题处理</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="batchScoreForm.q3" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">合规安全</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">流程合规 & 无安全事故</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="batchScoreForm.q4" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">履约质量</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">服务/活动效果达标度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="batchScoreForm.qualityScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">时效响应</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">执行 & 售后响应速度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="batchScoreForm.responseScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">配合度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">沟通配合 & 问题处理</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="batchScoreForm.cooperationScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">合规安全</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">流程合规 & 无安全事故</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="batchScoreForm.complianceScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
</tbody>
</table>
</template>
@@ -255,10 +261,10 @@
<th style="width:130px;padding:10px 12px;text-align:center">评分标准(1~5 分)</th>
</tr></thead>
<tbody>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">履约质量</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">服务/活动效果达标度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="singleScoreForm.q1" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">时效响应</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">执行 & 售后响应速度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="singleScoreForm.q2" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">配合度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">沟通配合 & 问题处理</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="singleScoreForm.q3" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">合规安全</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">流程合规 & 无安全事故</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="singleScoreForm.q4" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">履约质量</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">服务/活动效果达标度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="singleScoreForm.qualityScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">时效响应</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">执行 & 售后响应速度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="singleScoreForm.responseScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">配合度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">沟通配合 & 问题处理</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="singleScoreForm.cooperationScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">合规安全</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">流程合规 & 无安全事故</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="singleScoreForm.complianceScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
</tbody>
</table>
<template #footer>
@@ -277,10 +283,12 @@ import request from '@/utils/request'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowDown } from '@element-plus/icons-vue'
import Preview from '@/components/Preview.vue'
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
const router = useRouter()
const route = useRoute()
const q = ref({ sponsorOrgName: '', execOrgName: '', projectNo: '', projectName: '', projectForm: '', startTime: '', endTime: '', isFinished: '', isSettled: '', ratingScore: '' })
const q = ref({ sponsorOrgName: '', execOrgName: '', projectNo: '', projectName: '', projectForm: '', startTime: '', endTime: '', isFinished: '', isSettled: '', managerScore: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
@@ -333,7 +341,7 @@ async function doDelete(row) {
}
}
function reset() { q.value = { sponsorOrgName:'', execOrgName:'', projectNo:'', projectName:'', projectForm:'', startTime:'', endTime:'', isFinished:'', isSettled:'', ratingScore:'' }; page.pageNum = 1; load() }
function reset() { q.value = { sponsorOrgName:'', execOrgName:'', projectNo:'', projectName:'', projectForm:'', startTime:'', endTime:'', isFinished:'', isSettled:'', managerScore:'' }; page.pageNum = 1; load() }
// 导出
function exportProjects() { ElMessage.info('导出项目功能开发中') }
@@ -413,42 +421,59 @@ function confirmDeleteAnn() {
.catch(() => ElMessage.error('删除失败'))
}
function openSingleScore(row) {
async function openSingleScore(row) {
singleScoreTargetRow.value = row
// 反显各维度原值 (从后端 ratingQ1~Q4 字段); 无值才归零
singleScoreForm.q1 = row.ratingQ1 != null ? Number(row.ratingQ1) : 0
singleScoreForm.q2 = row.ratingQ2 != null ? Number(row.ratingQ2) : 0
singleScoreForm.q3 = row.ratingQ3 != null ? Number(row.ratingQ3) : 0
singleScoreForm.q4 = row.ratingQ4 != null ? Number(row.ratingQ4) : 0
singleScoreForm.qualityScore = 0
singleScoreForm.responseScore = 0
singleScoreForm.cooperationScore = 0
singleScoreForm.complianceScore = 0
singleScoreModalOpen.value = true
}
function confirmSingleScore() {
if (!singleScoreTargetRow.value) return
const dims = [singleScoreForm.q1, singleScoreForm.q2, singleScoreForm.q3, singleScoreForm.q4]
const labels = ['履约质量', '时效响应', '配合度', '合规安全']
// 4 个维度全部必填
for (let i = 0; i < dims.length; i++) {
if (dims[i] === 0 || dims[i] === '' || dims[i] == null) {
return ElMessage.error(`请填写「${labels[i]}」评分`)
// 反显当前登录管理员的历史明细
const uid = userStore.user?.userId
if (!uid) return
try {
const r = await request({ url: '/business/project/ratings', method: 'get', params: { projectId: row.projectId } })
const list = (r.data && (Array.isArray(r.data) ? r.data : r.data.rows)) || r.rows || []
const mine = list.find(x =>
String(x.projectId) === String(row.projectId) &&
String(x.raterId) === String(uid) &&
x.raterRole === 'manager'
)
if (mine) {
singleScoreForm.qualityScore = Number(mine.qualityScore) || 0
singleScoreForm.responseScore = Number(mine.responseScore) || 0
singleScoreForm.cooperationScore = Number(mine.cooperationScore) || 0
singleScoreForm.complianceScore = Number(mine.complianceScore) || 0
}
const v = Number(dims[i])
if (isNaN(v) || v < 1 || v > 5) return ElMessage.error(`${labels[i]}必须是 1~5 的数字`)
} catch (e) { /* 拉取失败不影响打开, 维持默认 0 */ }
}
async function confirmSingleScore() {
if (!singleScoreTargetRow.value) return
const f = singleScoreForm
if (!f.qualityScore || f.qualityScore < 1 || f.qualityScore > 5) return ElMessage.error('履约质量必须是 1~5 的数字')
if (!f.responseScore || f.responseScore < 1 || f.responseScore > 5) return ElMessage.error('时效响应必须是 1~5 的数字')
if (!f.cooperationScore || f.cooperationScore < 1 || f.cooperationScore > 5) return ElMessage.error('配合度必须是 1~5 的数字')
if (!f.complianceScore || f.complianceScore < 1 || f.complianceScore > 5) return ElMessage.error('合规安全必须是 1~5 的数字')
const avg = ((f.qualityScore + f.responseScore + f.cooperationScore + f.complianceScore) / 4).toFixed(1)
const projectId = singleScoreTargetRow.value.projectId
try {
// 1. 写聚合分到 biz_project.manager_score
await bizUpdate('project', { projectId, managerScore: Number(avg) })
// 2. 写 4 维度明细到 biz_project_rating (rater_role='manager', 后端自动填 raterId)
await request.post('/business/project/rate', {
projectId,
raterRole: 'manager',
qualityScore: f.qualityScore,
responseScore: f.responseScore,
cooperationScore: f.cooperationScore,
complianceScore: f.complianceScore
})
ElMessage.success(`评分完成,平均分:${avg}`)
singleScoreModalOpen.value = false
load()
} catch (e) {
ElMessage.error(e?.msg || '评分失败')
}
// 校验通过, 计算平均分
const scores = dims.map(v => Number(v))
const avg = (scores.reduce((s, v) => s + v, 0) / scores.length).toFixed(1)
// 构造 payload: 平均分 + 4 个维度分
const payload = {
projectId: singleScoreTargetRow.value.projectId,
ratingScore: Number(avg),
ratingQ1: scores[0],
ratingQ2: scores[1],
ratingQ3: scores[2],
ratingQ4: scores[3]
}
bizUpdate('project', payload)
.then(() => { ElMessage.success(`评分完成,平均分:${avg}`); singleScoreModalOpen.value = false; load() })
.catch(() => ElMessage.error('评分失败'))
}
function viewDetail(row) { router.push(`/manager/projects/detail/${row.projectId}`) }
@@ -474,7 +499,7 @@ const batchKind = ref('')
const batchSubmitting = ref(false)
const batchMap = { score: '批量评分', close: '批量结题', activate: '批量开通' }
const batchScoreRows = ref([]) // 保留兼容旧引用 (实际不再使用)
const batchScoreForm = reactive({ q1: 5, q2: 5, q3: 5, q4: 5 }) // 批量评分: 共享一份 4 维度
const batchScoreForm = reactive({ qualityScore: 5, responseScore: 5, cooperationScore: 5, complianceScore: 5 }) // 批量评分: 共享一份 4 维度
const activateDeadline = ref('')
// 5 个单行 modal 状态
@@ -486,7 +511,7 @@ const delAnnModalOpen = ref(false)
const delAnnTargetRow = ref(null)
const singleScoreModalOpen = ref(false)
const singleScoreTargetRow = ref(null)
const singleScoreForm = reactive({ q1: 0, q2: 0, q3: 0, q4: 0 })
const singleScoreForm = reactive({ qualityScore: 0, responseScore: 0, cooperationScore: 0, complianceScore: 0 })
// 新建项目已迁到独立页面 /manager/projects/new (ManagerProjectsNew.vue)
function openNewProject() {
@@ -518,7 +543,7 @@ function openBatch(kind) {
batchKind.value = kind
if (kind === 'score') {
// 共享评分: 重置 form 为默认值 (5 分)
Object.assign(batchScoreForm, { q1: 5, q2: 5, q3: 5, q4: 5 })
Object.assign(batchScoreForm, { qualityScore: 5, responseScore: 5, cooperationScore: 5, complianceScore: 5 })
}
if (kind === 'activate') {
activateDeadline.value = ''
@@ -554,24 +579,29 @@ async function confirmBatch() {
} catch { ElMessage.error('批量开通失败') }
finally { batchSubmitting.value = false }
} else if (batchKind.value === 'score') {
const q1 = Number(batchScoreForm.q1 || 0)
const q2 = Number(batchScoreForm.q2 || 0)
const q3 = Number(batchScoreForm.q3 || 0)
const q4 = Number(batchScoreForm.q4 || 0)
const total = ((q1 + q2 + q3 + q4) / 4).toFixed(1)
const f = batchScoreForm
if (!f.qualityScore || f.qualityScore < 1 || f.qualityScore > 5) return ElMessage.error('履约质量必须是 1~5 的数字')
if (!f.responseScore || f.responseScore < 1 || f.responseScore > 5) return ElMessage.error('时效响应必须是 1~5 的数字')
if (!f.cooperationScore || f.cooperationScore < 1 || f.cooperationScore > 5) return ElMessage.error('配合度必须是 1~5 的数字')
if (!f.complianceScore || f.complianceScore < 1 || f.complianceScore > 5) return ElMessage.error('合规安全必须是 1~5 的数字')
const total = ((f.qualityScore + f.responseScore + f.cooperationScore + f.complianceScore) / 4).toFixed(1)
try {
batchSubmitting.value = true
let ok = 0
// 共享评分: 所有项目用同一组 q1..q4
// 共享评分: 所有项目用同一组 4 维度
for (const r of selection.value) {
try {
await bizUpdate('project', {
projectId: r.projectId,
ratingScore: Number(total),
ratingQ1: q1,
ratingQ2: q2,
ratingQ3: q3,
ratingQ4: q4
managerScore: Number(total)
})
await request.post('/business/project/rate', {
projectId: r.projectId,
raterRole: 'manager',
qualityScore: f.qualityScore,
responseScore: f.responseScore,
cooperationScore: f.cooperationScore,
complianceScore: f.complianceScore
})
ok++
} catch {}
+1 -1
View File
@@ -70,7 +70,7 @@ async function loadStats() {
stats.value.executedMeetings = em.total || em.data?.total || 0
stats.value.pendingMeetings = meetingTotal - stats.value.executedMeetings
// 已结算项目
const sp = await bizList('project', { pageNum: 1, pageSize: 1, isSettled: '1' })
const sp = await bizList('project', { pageNum: 1, pageSize: 1, isSettled: 'Y' })
stats.value.settledProjects = sp.total || sp.data?.total || 0
} catch (e) {
console.warn('loadStats failed', e)
+22 -21
View File
@@ -57,15 +57,16 @@
</el-form-item>
<el-form-item label="所属公司">
<el-input
v-if="myPersonId"
v-if="myCompany.isOwner === 1"
v-model="form.orgName"
placeholder="请填写所属公司"
/>
<el-input
v-else
model-value="主账号无需设置 (请联系子账号在 [人员管理] 中维护)"
v-else-if="myCompany.orgName"
:model-value="myCompany.orgName"
disabled
/>
<span v-else style="color:#909399;font-size:13px">尚未分配所属公司</span>
</el-form-item>
</el-form>
@@ -175,8 +176,9 @@ const pwd = reactive({ old: '', new: '', confirm: '' })
const pwdError = ref('')
const saveError = ref('')
const saving = ref(false)
// 当前登录人绑定的 biz_person.personId (主账号为 null, 子账号有值)
const myPersonId = ref(null)
// 当前登录 sponsor 的所属公司 (主账号可改, 子账号只读)
// 从 /business/org/myCompany 取: { orgId, orgName, isOwner: 1|0 }
const myCompany = reactive({ orgId: null, orgName: '', isOwner: 0 })
const avatarUrl = ref(store.user?.avatar || '')
const avatarChar = computed(() => (form.name || store.user?.userName || '支').slice(0, 1))
@@ -247,19 +249,18 @@ async function loadUserProfile() {
form.licenseCertUrl = u.licenseCertUrl || ''
form.idCardFrontUrl = u.idCardFrontUrl || ''
form.idCardBackUrl = u.idCardBackUrl || ''
// orgName 不存在 sys_user 表, 改从 biz_person 读 (只有 SUB 子账号有 person 记录, 后端已 LEFT JOIN biz_org)
// 所属公司: 走 /business/org/myCompany, 主账号可改, 子账号只读
try {
const myPerson = await request({
url: '/business/person/list',
method: 'get',
params: { userId: u.userId, unitType: 'sponsor', pageNum: 1, pageSize: 1 }
})
const rows = (myPerson.data && myPerson.data.rows) || myPerson.rows || []
const me = rows[0]
myPersonId.value = me?.personId || null
form.orgName = me?.orgName || ''
const r = await request({ url: '/business/org/myCompany', method: 'get' })
const c = (r.data && (r.data.data || r.data)) || {}
myCompany.orgId = c.orgId || null
myCompany.orgName = c.orgName || ''
myCompany.isOwner = Number(c.isOwner) || 0
form.orgName = myCompany.orgName
} catch {
myPersonId.value = null
myCompany.orgId = null
myCompany.orgName = ''
myCompany.isOwner = 0
form.orgName = ''
}
} catch (e) {
@@ -278,8 +279,6 @@ async function onSave() {
saveError.value = '请填写姓名'
return ElMessage.warning(saveError.value)
}
// 主账号 (无 personId) 只保存 sys_user 字段; 子账号额外保存 biz_person.orgId
// TODO: orgName → orgId 需后端根据 orgName 查 biz_org 取 org_id
const sysPayload = {
nickName: form.name,
licenseCertUrl: form.licenseCertUrl,
@@ -289,12 +288,14 @@ async function onSave() {
saving.value = true
try {
await request({ url: '/system/user/profile', method: 'put', data: sysPayload })
if (myPersonId.value) {
// 主账号: 同步更新所属公司名称到 biz_org
if (myCompany.isOwner === 1 && myCompany.orgId && form.orgName !== myCompany.orgName) {
await request({
url: '/business/person',
url: '/business/org',
method: 'put',
data: { personId: myPersonId.value, orgName: form.orgName }
data: { orgId: myCompany.orgId, orgName: form.orgName }
})
myCompany.orgName = form.orgName
}
if (store.user) {
store.user.nickName = form.name
+1 -1
View File
@@ -15,7 +15,7 @@
<div class="stat-value">{{ stats.totalProjects }}</div>
<div class="stat-desc">查看全部项目</div>
</router-link>
<router-link class="stat-card" to="/sponsor/meetings?isSettled=1">
<router-link class="stat-card" to="/sponsor/meetings?isSettled=Y">
<div class="stat-label">已结算的会议</div>
<div class="stat-value">{{ stats.settledMeetings }}</div>
<div class="stat-desc">查看已结算会议</div>
+31 -12
View File
@@ -64,8 +64,18 @@
<el-table-column label="可用金额" width="120" align="right"><template #default="{ row }">¥{{ formatMoney(row.availableAmount) }}</template></el-table-column>
<el-table-column label="已支付劳务费" width="120" align="right"><template #default="{ row }">¥{{ formatMoney(row.paidLaborAmount) }}</template></el-table-column>
<el-table-column label="已支付会务费" width="120" align="right"><template #default="{ row }">¥{{ formatMoney(row.paidMeetingAmount) }}</template></el-table-column>
<el-table-column prop="complianceScore" label="执行单位得分(合规)" width="150" align="center" />
<el-table-column prop="ratingScore" label="执行单位评价(支持方)" width="160" align="center" />
<el-table-column prop="managerScore" label="执行单位得分(合规)" width="150" align="center">
<template #default="{ row }">
<span v-if="row.managerScore != null">{{ row.managerScore }}</span>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column prop="sponsorScore" label="执行单位评价(支持方)" width="160" align="center">
<template #default="{ row }">
<span v-if="row.sponsorScore != null">{{ row.sponsorScore }}</span>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column prop="orgName" label="支持公司" min-width="140" show-overflow-tooltip />
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
<el-table-column label="是否结题" width="90" align="center">
@@ -180,7 +190,7 @@
<el-descriptions-item label="可用金额">¥{{ formatMoney(detail.availableAmount) }}</el-descriptions-item>
<el-descriptions-item label="已支付劳务费">¥{{ formatMoney(detail.paidLaborAmount) }}</el-descriptions-item>
<el-descriptions-item label="已支付会务费">¥{{ formatMoney(detail.paidMeetingAmount) }}</el-descriptions-item>
<el-descriptions-item label="执行单位得分">{{ detail.ratingScore || '-' }}</el-descriptions-item>
<el-descriptions-item label="执行单位得分">{{ detail.managerScore || '-' }}</el-descriptions-item>
<el-descriptions-item label="是否结题">
<el-tag :type="detail.isFinished === '1' ? 'success' : 'info'" disable-transitions>
{{ detail.isFinished === '1' ? '已结题' : '未结题' }}
@@ -306,21 +316,30 @@ async function submitRate() {
if (!rateForm.q1 && !rateForm.q2 && !rateForm.q3 && !rateForm.q4) {
ElMessage.warning('请至少完成一项评分'); return
}
// 计算 4 维平均 (任意维度填了就参与平均)
const scores = [rateForm.q1, rateForm.q2, rateForm.q3, rateForm.q4].map(v => Number(v))
const filled = scores.filter(v => v > 0)
const avg = filled.length ? (filled.reduce((s, v) => s + v, 0) / filled.length).toFixed(1) : null
try {
// sponsor 专属: POST /business/project/rate (BizProjectSponsor 字段)
const data = {
// 1. 写明细到 biz_project_rating (rater_role='sponsor', 后端自动填 raterId)
await request.post('/business/project/rate', {
projectId: rateForm.projectId,
projectNo: rateForm.projectNo,
projectName: rateForm.projectName,
sponsorQ1: rateForm.q1,
sponsorQ2: rateForm.q2,
sponsorQ3: rateForm.q3,
sponsorQ4: rateForm.q4,
sponsorRemark: rateForm.remark
raterRole: 'sponsor',
qualityScore: rateForm.q1,
responseScore: rateForm.q2,
cooperationScore: rateForm.q3,
complianceScore: rateForm.q4,
remark: rateForm.remark
})
// 2. 写聚合分到 biz_project.sponsor_score
if (avg != null) {
await bizUpdate('project', { projectId: rateForm.projectId, sponsorScore: Number(avg) })
}
await request.post('/business/project/rate', data)
ElMessage.success('评分已提交')
ElMessage.success(`评分已提交, 平均分:${avg ?? '-'}`)
rateOpen.value = false
load()
} catch (e) { ElMessage.error(e?.msg || '提交失败') }
}
+81 -62
View File
@@ -43,7 +43,6 @@
<div class="toolbar">
<el-button type="primary" @click="onAssignBatch">项目分配</el-button>
<el-button @click="onEvalBatch">批量评价</el-button>
</div>
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelectionChange">
@@ -80,15 +79,15 @@
<span>{{ row.isFinished === '1' ? '已结题' : '未结题' }}</span>
</template>
</el-table-column>
<el-table-column label="执行单位评价" width="120" align="center">
<el-table-column label="执行单位评价(支持方)" width="150" align="center">
<template #default="{ row }">
<a v-if="row.ratingScore" class="link">{{ row.ratingScore }}</a>
<a v-if="row.sponsorScore" class="link">{{ row.sponsorScore }}</a>
<span v-else style="color:#909399"></span>
</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEval(row)">项目评价</el-button>
<el-button link type="primary" @click="openSingleScore(row)">项目评价</el-button>
<el-button link type="primary" @click="openAssign(row)">分配</el-button>
<el-button link type="primary" @click="onView(row)">查看</el-button>
</template>
@@ -106,37 +105,29 @@
@current-change="load"
/>
<!-- ============ Modal 1: 项目评价 ============ -->
<el-dialog v-model="evalVisible" title="执行单位评分" width="600px" append-to-body destroy-on-close>
<div class="modal-project-info">
<div><span class="lbl">项目编号</span><span>{{ evalForm.projectNo }}</span></div>
<div><span class="lbl">项目名称</span><span>{{ evalForm.projectName }}</span></div>
</div>
<el-form :model="evalForm" label-width="120px" class="modal-form">
<el-form-item label="履约质量">
<el-rate v-model="evalForm.q1" :max="5" :colors="['#99A9BF','#F7BA2A','#FF9900']" show-text :texts="['1', '2', '3', '4', '5']" />
</el-form-item>
<el-form-item label="时效响应">
<el-rate v-model="evalForm.q2" :max="5" :colors="['#99A9BF','#F7BA2A','#FF9900']" show-text :texts="['1', '2', '3', '4', '5']" />
</el-form-item>
<el-form-item label="配合度">
<el-rate v-model="evalForm.q3" :max="5" :colors="['#99A9BF','#F7BA2A','#FF9900']" show-text :texts="['1', '2', '3', '4', '5']" />
</el-form-item>
<el-form-item label="合规安全">
<el-rate v-model="evalForm.q4" :max="5" :colors="['#99A9BF','#F7BA2A','#FF9900']" show-text :texts="['1', '2', '3', '4', '5']" />
</el-form-item>
<el-form-item label="评价备注">
<el-input v-model="evalForm.remark" type="textarea" :rows="4" placeholder="请输入评价备注" />
</el-form-item>
</el-form>
<!-- ============ Modal 1: 项目评价 ( manager/Projects.vue 完全相同的样式) ============ -->
<el-dialog v-model="singleScoreModalOpen" title="执行单位评分" width="626px" :show-close="false">
<table class="score-table" style="width:100%;border-collapse:collapse;font-size:13px">
<thead><tr style="background:#fafafa">
<th style="width:110px;padding:10px 12px;text-align:left">评价维度</th>
<th style="padding:10px 12px;text-align:left">评价内容(简洁版)</th>
<th style="width:130px;padding:10px 12px;text-align:center">评分标准(1~5 )</th>
</tr></thead>
<tbody>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">履约质量</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">服务/活动效果达标度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="singleScoreForm.qualityScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">时效响应</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">执行 & 售后响应速度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="singleScoreForm.responseScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">配合度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">沟通配合 & 问题处理</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="singleScoreForm.cooperationScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">合规安全</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">流程合规 & 无安全事故</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center"><el-input-number v-model="singleScoreForm.complianceScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
</tbody>
</table>
<template #footer>
<el-button @click="evalVisible = false">取消</el-button>
<el-button type="primary" @click="saveEval">确定</el-button>
<el-button type="primary" @click="confirmSingleScore">确定</el-button>
<el-button @click="singleScoreModalOpen=false">取消</el-button>
</template>
</el-dialog>
<!-- ============ Modal 2: 项目分配 (单选 / 批量复用) ============ -->
<el-dialog :title="assignBatchMode ? `批量项目分配 (${assignBatchRows.length} 个项目)` : '项目分配'" width="600px" append-to-body destroy-on-close>
<el-dialog v-model="assignVisible" :title="assignBatchMode ? `批量项目分配 (${assignBatchRows.length} 个项目)` : '项目分配'" width="600px" append-to-body destroy-on-close>
<!-- 单选模式: 显示项目编号 / 名称 -->
<div v-if="!assignBatchMode" class="modal-project-info">
<div><span class="lbl">项目编号</span><span>{{ assignForm.projectNo }}</span></div>
@@ -175,7 +166,9 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/request'
import { listSponsorProjects, rateProject, sponsorAssignProject, sponsorAssignBatch } from '@/api/business/project'
import { bizUpdate } from '@/api/public'
import { listBizPerson } from '@/api/business/person'
import { useUserStore } from '@/store/user'
@@ -238,39 +231,65 @@ function onView(row) {
ElMessageBox.alert(`项目编号: ${row.projectNo}\n项目名称: ${row.projectName}`, '项目详情', { confirmButtonText: '确定' })
}
// ========== Modal 1: 项目评价 ==========
const evalVisible = ref(false)
const evalForm = reactive({ projectId: null, projectNo: '', projectName: '', q1: 5, q2: 5, q3: 5, q4: 5, remark: '' })
function openEval(row) {
evalForm.projectId = row.projectId
evalForm.projectNo = row.projectNo
evalForm.projectName = row.projectName
evalForm.q1 = row.sponsorQ1 || 5
evalForm.q2 = row.sponsorQ2 || 5
evalForm.q3 = row.sponsorQ3 || 5
evalForm.q4 = row.sponsorQ4 || 5
evalForm.remark = row.sponsorRemark || ''
evalVisible.value = true
// ========== 执行单位评分 (与 manager/Projects.vue 完全相同的样式和逻辑) ==========
const singleScoreModalOpen = ref(false)
const singleScoreTargetRow = ref(null)
const singleScoreForm = reactive({ qualityScore: 0, responseScore: 0, cooperationScore: 0, complianceScore: 0 })
async function openSingleScore(row) {
singleScoreTargetRow.value = row
singleScoreForm.qualityScore = 0
singleScoreForm.responseScore = 0
singleScoreForm.cooperationScore = 0
singleScoreForm.complianceScore = 0
singleScoreModalOpen.value = true
// 回显当前 sponsor 在该项目上的历史评分
const uid = userStore.user?.userId
if (!uid) return
try {
const r = await request({ url: '/business/project/ratings', method: 'get', params: { projectId: row.projectId } })
const list = (r.data && (Array.isArray(r.data) ? r.data : r.data.rows)) || r.rows || []
const mine = list.find(x =>
String(x.projectId) === String(row.projectId) &&
String(x.raterId) === String(uid) &&
x.raterRole === 'sponsor'
)
if (mine) {
singleScoreForm.qualityScore = Number(mine.qualityScore) || 0
singleScoreForm.responseScore = Number(mine.responseScore) || 0
singleScoreForm.cooperationScore = Number(mine.cooperationScore) || 0
singleScoreForm.complianceScore = Number(mine.complianceScore) || 0
}
} catch (e) { /* 拉取失败不影响打开, 维持默认 0 */ }
}
async function saveEval() {
const avg = ((evalForm.q1 + evalForm.q2 + evalForm.q3 + evalForm.q4) / 4).toFixed(2)
await rateProject({
projectId: evalForm.projectId,
sponsorRating: avg,
sponsorQ1: evalForm.q1,
sponsorQ2: evalForm.q2,
sponsorQ3: evalForm.q3,
sponsorQ4: evalForm.q4,
sponsorRemark: evalForm.remark
})
ElMessage.success('评价已保存')
evalVisible.value = false
load()
}
function onEvalBatch() {
if (!selected.value.length) return ElMessage.warning('请先选择项目')
if (selected.value.length > 1) return ElMessage.warning('批量评价一次仅可评价一个项目')
openEval(selected.value[0])
async function confirmSingleScore() {
if (!singleScoreTargetRow.value) return
const f = singleScoreForm
if (!f.qualityScore || f.qualityScore < 1 || f.qualityScore > 5) return ElMessage.error('履约质量必须是 1~5 的数字')
if (!f.responseScore || f.responseScore < 1 || f.responseScore > 5) return ElMessage.error('时效响应必须是 1~5 的数字')
if (!f.cooperationScore || f.cooperationScore < 1 || f.cooperationScore > 5) return ElMessage.error('配合度必须是 1~5 的数字')
if (!f.complianceScore || f.complianceScore < 1 || f.complianceScore > 5) return ElMessage.error('合规安全必须是 1~5 的数字')
const avg = ((f.qualityScore + f.responseScore + f.cooperationScore + f.complianceScore) / 4).toFixed(1)
const projectId = singleScoreTargetRow.value.projectId
try {
// 1. 写聚合分到 biz_project.sponsor_score
await bizUpdate('project', { projectId, sponsorScore: Number(avg) })
// 2. 写 4 维度明细到 biz_project_rating (rater_role='sponsor', 后端自动填 raterId)
await rateProject({
projectId,
raterRole: 'sponsor',
qualityScore: f.qualityScore,
responseScore: f.responseScore,
cooperationScore: f.cooperationScore,
complianceScore: f.complianceScore
})
ElMessage.success(`评分完成,平均分:${avg}`)
singleScoreModalOpen.value = false
load()
} catch (e) {
ElMessage.error(e?.msg || '评分失败')
}
}
// ========== Modal 2: 项目分配 (单选 / 批量) ==========