From b5b3e3a21323a55dfc576435f4c25a46f7c29b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=AD=E5=BA=86=E6=B3=B0?= <12369755+htcloud1@user.noreply.gitee.com> Date: Wed, 26 Aug 2026 00:41:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=9A=E8=AE=AE=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8=20+=20=E5=85=AC=E7=A4=BA=E6=8C=89=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E6=97=B6=E9=97=B4=E5=80=92=E5=BA=8F=20+=20=E7=8E=B0?= =?UTF-8?q?=E5=9C=BA=E7=85=A7=E7=89=87=20dialog=20=E9=A2=84=E8=A7=88=20+?= =?UTF-8?q?=20=E9=87=8D=E5=A4=8D=E6=8A=A5=E9=94=99=E7=B2=BE=E7=A1=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 会议列表 manager 批量合规审核 (批量通过/退回, best-effort 逐条回执) - 公示列表按 publish_time 倒序, 发布/开通写本地 publish_time (修 toISOString UTC 时区 bug) - 签到表/前全景/后全景「查看照片」改 dialog 预览 (复用 Preview.vue) - 手机号/用户名重复报错精确提示 (后端 fieldMap + 前端 duplicateTip) - 医生首页欢迎语改「欢迎使用合规系统」 --- .../controller/BizMeetingController.java | 77 +++++- .../controller/BizPublicController.java | 5 +- .../business/mapper/BizProjectMapper.java | 2 + .../business/service/IBizProjectService.java | 2 + .../service/impl/BizProjectServiceImpl.java | 3 + .../mapper/business/BizProjectMapper.xml | 16 +- .../web/exception/GlobalExceptionHandler.java | 1 + ry-vue3/src/components/CameraQrUpload.vue | 19 +- ry-vue3/src/views/doctor/Home.vue | 2 +- ry-vue3/src/views/executor/NewPerson.vue | 10 +- ry-vue3/src/views/manager/ProjectsNew.vue | 8 +- ry-vue3/src/views/meetings/Meetings.vue | 84 +++++- ry-vue3/src/views/portal/Publicity.vue | 253 +++++++++++------- ry-vue3/src/views/sponsor/NewPerson.vue | 10 +- ry-vue3/src/views/sponsor/Projects.vue | 10 +- 15 files changed, 387 insertions(+), 115 deletions(-) diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java index 0985412..4dbdf4d 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java @@ -1,7 +1,9 @@ package com.ruoyi.business.controller; +import java.util.ArrayList; import java.util.Calendar; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; import com.ruoyi.business.domain.BizMeetingMaterial; @@ -283,22 +285,72 @@ public class BizMeetingController extends BaseController { @Log(title = "会议审核", businessType = BusinessType.UPDATE) @PostMapping("/{meetingId}/audit-compliance") public AjaxResult auditCompliance(@PathVariable("meetingId") Long meetingId, @RequestBody AuditBody body) { - String roleType = SecurityUtils.getLoginUser().getUser().getRoleType(); - if (!"manager".equals(roleType)) throw new ServiceException("仅合规人员可操作"); - + if (!"manager".equals(SecurityUtils.getLoginUser().getUser().getRoleType())) { + throw new ServiceException("仅合规人员可操作"); + } BizMeeting m = bizMeetingService.getById(meetingId); if (m == null) throw new ServiceException("会议不存在"); + boolean approved = Boolean.TRUE.equals(body.getApproved()); + if (!approved && (body.getOpinion() == null || body.getOpinion().isEmpty())) { + throw new ServiceException("拒绝时意见不能为空"); + } + return success(doComplianceAudit(m, approved, body.getOpinion())); + } + /** + * 批量合规审核 (role_type=manager): 一次对多个会议执行材料一级审核. + *

+ * body: { "meetingIds": [..], "approved": true|false, "opinion": "..." } + *

best-effort: 逐条审核, 状态不符的会议跳过并回执失败原因, 不整批回滚. + */ + @Log(title = "会议审核", businessType = BusinessType.UPDATE) + @PostMapping("/batch-audit-compliance") + public AjaxResult batchAuditCompliance(@RequestBody BatchAuditBody body) { + if (!"manager".equals(SecurityUtils.getLoginUser().getUser().getRoleType())) { + throw new ServiceException("仅合规人员可操作"); + } + List meetingIds = body.getMeetingIds(); + if (meetingIds == null || meetingIds.isEmpty()) { + throw new ServiceException("请选择会议"); + } boolean approved = Boolean.TRUE.equals(body.getApproved()); if (!approved && (body.getOpinion() == null || body.getOpinion().isEmpty())) { throw new ServiceException("拒绝时意见不能为空"); } + List successIds = new ArrayList<>(); + List> failures = new ArrayList<>(); + for (Long meetingId : meetingIds) { + if (meetingId == null) continue; + try { + BizMeeting m = bizMeetingService.getById(meetingId); + if (m == null) throw new ServiceException("会议不存在"); + doComplianceAudit(m, approved, body.getOpinion()); + successIds.add(meetingId); + } catch (ServiceException e) { + Map f = new HashMap<>(); + f.put("meetingId", meetingId); + f.put("reason", e.getMessage()); + failures.add(f); + } + } + + Map data = new HashMap<>(); + data.put("total", meetingIds.size()); + data.put("successCount", successIds.size()); + data.put("failCount", failures.size()); + data.put("failures", failures); + return success(data); + } + + /** + * 内部: 合规审核核心流转 (阶段校验 + 状态写入 + audit_log). 单条/批量共用. + */ + private String doComplianceAudit(BizMeeting m, boolean approved, String opinion) { Integer compliance = m.getMaterialComplianceApproved(); if (!"SUBMITTED".equals(m.getMaterialAuditStage()) || (compliance != null && compliance == 1)) { throw new ServiceException("当前阶段不允许合规审核"); } - String result = approved ? "APPROVED" : "REJECTED"; if (approved) { m.setMaterialComplianceApproved(1); @@ -310,8 +362,8 @@ public class BizMeetingController extends BaseController { m.setMaterialAuditTime(new Date()); m.setCurrentStage(stageDeriver.derivePhysicalStage(m)); bizMeetingService.updateByPrimaryKey(m); - appendAuditLog(m, "MATERIAL", result, body.getOpinion()); - return success(result); + appendAuditLog(m, "MATERIAL", result, opinion); + return result; } /** @@ -533,4 +585,17 @@ public class BizMeetingController extends BaseController { public void setOpinion(String opinion) { this.opinion = opinion; } } + /** request body for batch audit endpoint */ + public static class BatchAuditBody { + private List meetingIds; + private Boolean approved; // true=通过 false=拒绝 + private String opinion; // 意见 + public List getMeetingIds() { return meetingIds; } + public void setMeetingIds(List meetingIds) { this.meetingIds = meetingIds; } + public Boolean getApproved() { return approved; } + public void setApproved(Boolean approved) { this.approved = approved; } + public String getOpinion() { return opinion; } + public void setOpinion(String opinion) { this.opinion = opinion; } + } + } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java index 236a170..92ba2a6 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java @@ -82,9 +82,8 @@ public class BizPublicController extends BaseController { @GetMapping("/announcements") public AjaxResult announcements() { - BizProject query = new BizProject(); - query.setIsPublished("1"); - List list = projectService.selectList(query); + // 公示列表按发布时间倒序 (后端排序), 只返回已发布项目 + List list = projectService.selectPublicAnnouncements(); return success(list); } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectMapper.java index 65e4256..c2f0d18 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectMapper.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectMapper.java @@ -9,6 +9,8 @@ public interface BizProjectMapper { BizProject selectByPrimaryKey(Long projectId); List selectList(BizProject entity); + /** 公开门户公示列表: is_published='1' 且未删除, 按 publish_time 倒序 (最新在前, 空值排最后) */ + List selectPublicAnnouncements(); /** sponsor 端专属: projectIds + LEFT JOIN 当前 login 用户评分, 用于评分回显 */ List selectSponsorList(BizProject entity); /** executor 端专属: JOIN biz_project_assign 过滤, 只返回分给当前 user 的项目 */ diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectService.java index ec292fc..62a2781 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectService.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectService.java @@ -10,6 +10,8 @@ public interface IBizProjectService { BizProject getById(Long projectId); List selectList(BizProject entity); + /** 公开门户公示列表: 已发布, 按发布时间倒序 */ + List selectPublicAnnouncements(); /** sponsor 端专属: LEFT JOIN 当前 login 用户评分回显 */ List selectSponsorList(BizProject entity); /** executor 端专属: JOIN biz_project_assign 过滤, 只返回分给当前 user 的项目 */ diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectServiceImpl.java index f0e9f4c..4cd35e9 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectServiceImpl.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectServiceImpl.java @@ -43,6 +43,9 @@ public class BizProjectServiceImpl implements IBizProjectService public List selectList(BizProject entity) { return bizProjectMapper.selectList(entity); } @Override + public List selectPublicAnnouncements() + { return bizProjectMapper.selectPublicAnnouncements(); } + @Override public List selectSponsorList(BizProject entity) { return bizProjectMapper.selectSponsorList(entity); } @Override diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml index a0d6721..7f4db12 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml @@ -28,6 +28,7 @@ + @@ -56,7 +57,7 @@ (p.total_amount - ifnull(p.manage_fee, 0) - (select ifnull(sum(m.labor_fee), 0) from biz_meeting m where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0) - (select ifnull(sum(m.meeting_fee), 0) from biz_meeting m where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0)) as available_amount, - p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled, p.is_published, p.sponsor_org_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, 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, p.schedule_url, p.open_deadline, p.open_status, p.is_deleted, + p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled, p.is_published, p.publish_time, p.sponsor_org_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, 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, p.schedule_url, p.open_deadline, p.open_status, p.is_deleted, o.org_name as sponsor_org_name, su.user_name as sponsor_admin_user_name, lu.user_name as lead_user_name, @@ -81,7 +82,7 @@ (p.total_amount - ifnull(p.manage_fee, 0) - (select ifnull(sum(m.labor_fee), 0) from biz_meeting m where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0) - (select ifnull(sum(m.meeting_fee), 0) from biz_meeting m where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0)) as available_amount, - p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled, p.is_published, + p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled, p.is_published, p.publish_time, p.sponsor_org_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, @@ -356,6 +357,14 @@ order by project_id desc + + + + insert into biz_project @@ -372,6 +381,7 @@ is_finished, is_settled, is_published, + publish_time, sponsor_org_id, lead_user_id, is_bid_project, @@ -411,6 +421,7 @@ #{isFinished}, #{isSettled}, #{isPublished}, + #{publishTime}, #{sponsorOrgId}, #{leadUserId}, #{isBidProject}, @@ -462,6 +473,7 @@ is_finished = #{isFinished}, is_settled = #{isSettled}, is_published = #{isPublished}, + publish_time = #{publishTime}, sponsor_org_id = #{sponsorOrgId}, lead_user_id = #{leadUserId}, is_bid_project = #{isBidProject}, diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java index f228ca0..6778c5b 100644 --- a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java @@ -184,6 +184,7 @@ public class GlobalExceptionHandler fieldMap.put("expert_id_card", "身份证号"); fieldMap.put("unit_name", "单位名称"); fieldMap.put("org_name", "机构名称"); + fieldMap.put("person_phone", "手机号"); if (key != null && key.startsWith("uk_")) { String field = key.substring(3); diff --git a/ry-vue3/src/components/CameraQrUpload.vue b/ry-vue3/src/components/CameraQrUpload.vue index 91b09e3..042e9ad 100644 --- a/ry-vue3/src/components/CameraQrUpload.vue +++ b/ry-vue3/src/components/CameraQrUpload.vue @@ -21,11 +21,14 @@  扫码拍照 - 查看照片 + 查看照片 未上传 等待手机回传… + + + @@ -151,8 +166,6 @@ onBeforeUnmount(stopPolling) display: flex; align-items: center; gap: 12px; min-height: 36px; } -.cam-link { color: var(--brand-primary); text-decoration: none; font-size: 13px; } -.cam-link:hover { text-decoration: underline; } .cam-empty { color: #c0c4cc; font-size: 13px; } .cam-polling { color: #e6a23c; font-size: 12px; } diff --git a/ry-vue3/src/views/doctor/Home.vue b/ry-vue3/src/views/doctor/Home.vue index 26dee46..264a31d 100644 --- a/ry-vue3/src/views/doctor/Home.vue +++ b/ry-vue3/src/views/doctor/Home.vue @@ -6,7 +6,7 @@

下午好,{{ displayName }}专家

-

欢迎使用项目管理系统

+

欢迎使用合规系统

{{ nowTime }}
diff --git a/ry-vue3/src/views/executor/NewPerson.vue b/ry-vue3/src/views/executor/NewPerson.vue index 3b68db1..bbb7fbb 100644 --- a/ry-vue3/src/views/executor/NewPerson.vue +++ b/ry-vue3/src/views/executor/NewPerson.vue @@ -213,6 +213,12 @@ function isDuplicateError(msg) { return /已存在|重复|duplicate|uk_person_phone/i.test(msg || '') } +// 按后端原因精确提示: 登录账号(用户名) vs 手机号 (后端分别抛 "登录账号已存在" / DB uk_person_phone) +function duplicateTip(msg) { + if (/登录账号|用户名|login/i.test(msg || '')) return '登录账号已存在,请更换' + return '该手机号已存在,请更换' +} + async function onSave() { try { await formRef.value.validate() @@ -248,12 +254,12 @@ async function onSave() { } } else { const msg = res?.msg || '保存失败' - if (isDuplicateError(msg)) ElMessage.warning('该手机号已存在,请更换') + if (isDuplicateError(msg)) ElMessage.warning(duplicateTip(msg)) else ElMessage.error(msg) } } catch (e) { const msg = e?.msg || e?.message || '保存失败' - if (isDuplicateError(msg)) ElMessage.warning('该手机号已存在,请更换') + if (isDuplicateError(msg)) ElMessage.warning(duplicateTip(msg)) else ElMessage.error(msg) } finally { saving.value = false diff --git a/ry-vue3/src/views/manager/ProjectsNew.vue b/ry-vue3/src/views/manager/ProjectsNew.vue index 9a54d64..97482c9 100644 --- a/ry-vue3/src/views/manager/ProjectsNew.vue +++ b/ry-vue3/src/views/manager/ProjectsNew.vue @@ -145,6 +145,12 @@ const fileInputRef = ref(null) function defaultRoleRow() { return { role: '', customName: '', amount: 0 } } function defaultNotice(key, label) { return { key, label, url: '', name: '' } } +// 当前本地时间 "yyyy-MM-dd HH:mm:ss" (发布用, 与 el-date-picker 的本地时间口径一致; 不能用 toISOString 的 UTC) +function nowLocalDatetime() { + const d = new Date() + const p = n => String(n).padStart(2, '0') + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}` +} // 项目负责人候选 (合规管理员, sys_user.role_type='manager') const managerOptions = ref([]) @@ -333,7 +339,7 @@ async function submit(mode = 'save') { // 发布时设置 is_published='1' + publish_time (同步后端字段) if (mode === 'publish') { payload.isPublished = '1' - payload.publishTime = new Date().toISOString().slice(0, 19).replace('T', ' ') + payload.publishTime = nowLocalDatetime() } // 多执行方分配已移至"项目分配"页面单独管理 try { diff --git a/ry-vue3/src/views/meetings/Meetings.vue b/ry-vue3/src/views/meetings/Meetings.vue index 3e364a9..7dc1a41 100644 --- a/ry-vue3/src/views/meetings/Meetings.vue +++ b/ry-vue3/src/views/meetings/Meetings.vue @@ -35,9 +35,9 @@
- 批量删除 批量下载会务 批量下载劳务 + 批量审核 已选 {{ selectedIds.length }} 条
@@ -119,6 +119,28 @@ 确定 + + + + + + 已选 {{ selectedIds.length }} 个会议, 其中待合规审核 {{ batchEligibleIds.length }} 个 + + + + 通过 + 退回 + + + + + + + +
@@ -364,6 +386,66 @@ async function onApprovalConfirm() { } } +// ========== 合规人员(manager) 批量审核 ========== +const batchAuditOpen = ref(false) +const batchAuditSaving = ref(false) +const batchAuditForm = reactive({ approved: true, opinion: '' }) +const batchEligibleIds = ref([]) + +// 待合规审核判据: material_audit_stage=SUBMITTED 且 compliance_approved≠1 (与详情页 canComplianceAudit 一致) +function isCompliancePending(row) { + return row.materialAuditStage === 'SUBMITTED' && !isOneVal(row.materialComplianceApproved) +} + +function onBatchAudit() { + if (!selectedIds.value.length) return ElMessage.warning('请先勾选会议') + // 从当前页 rows 里过滤出「待合规审核」的选中会议 (selection 只来自当前页) + const eligible = rows.value.filter(r => selectedIds.value.includes(r.meetingId) && isCompliancePending(r)) + batchEligibleIds.value = eligible.map(r => r.meetingId) + if (!batchEligibleIds.value.length) { + ElMessage.warning('所选会议中没有处于「待合规审核」状态的, 无法批量审核') + return + } + batchAuditForm.approved = true + batchAuditForm.opinion = '' + batchAuditOpen.value = true +} + +async function onBatchAuditConfirm() { + if (!batchAuditForm.approved && !batchAuditForm.opinion.trim()) { + ElMessage.warning('退回时意见不能为空') + return + } + batchAuditSaving.value = true + try { + const r = await request.post('/business/meeting/batch-audit-compliance', { + meetingIds: batchEligibleIds.value, + approved: batchAuditForm.approved, + opinion: batchAuditForm.opinion + }) + const d = r?.data || {} + const successCount = d.successCount ?? 0 + const failCount = d.failCount ?? 0 + const failures = d.failures || [] + if (failCount > 0) { + const names = failures.map(f => `会议ID ${f.meetingId}: ${f.reason}`).join('\n') + ElMessageBox.alert( + `批量审核完成\n成功 ${successCount} 个, 失败 ${failCount} 个\n\n失败明细:\n${names}`, + '批量审核结果', + { type: successCount > 0 ? 'warning' : 'error', confirmButtonText: '知道了' } + ) + } else { + ElMessage.success(`批量审核完成, 成功 ${successCount} 个`) + } + batchAuditOpen.value = false + load() + } catch (e) { + ElMessage.error(e?.msg || e?.message || '批量审核失败') + } finally { + batchAuditSaving.value = false + } +} + // ========== 会务下载 (OSS 端打 zip: 后端 staging copy + 阿里云 FC 打包) ========== async function onDownloadService(row) { try { diff --git a/ry-vue3/src/views/portal/Publicity.vue b/ry-vue3/src/views/portal/Publicity.vue index fd01657..47116aa 100644 --- a/ry-vue3/src/views/portal/Publicity.vue +++ b/ry-vue3/src/views/portal/Publicity.vue @@ -42,30 +42,48 @@
- +
+ + + + + +
- +
@@ -121,8 +139,7 @@ async function load() { allRows.value = list.map(r => { return { annId: r.projectId, - date: r.startTime, - endTime: r.endTime, + publishTime: r.publishTime, type: '公示', projectNo: r.projectNo || '', projectName: r.projectName, @@ -430,30 +447,47 @@ main.container { /* ========== 筛选条 ========== */ .filter-bar { - padding: 12px 0; - border-bottom: 1px solid #f3f4f6; - margin-bottom: 0; + padding: 14px 0; + border-bottom: 1px solid #f0f2f5; display: flex; align-items: center; - gap: 14px; + gap: 12px; font-size: 13px; } +.filter-input-wrap { + position: relative; + display: flex; + align-items: center; +} + +.filter-icon { + position: absolute; + left: 13px; + top: 50%; + transform: translateY(-50%); + color: #9ca3af; + pointer-events: none; +} + .filter-input { - height: 30px; - padding: 0 10px; - border: 1px solid #d1d5db; - background: #fff; + height: 34px; + padding: 0 12px 0 34px; + border: 1px solid #dde1e6; + border-radius: 17px; + background: #f7f8fa; font-size: 13px; color: #1f2937; font-family: inherit; outline: none; - width: 220px; - transition: border-color 0.2s; + width: 240px; + transition: border-color 0.2s, background 0.2s, box-shadow 0.2s; } .filter-input:focus { border-color: var(--brand-primary); + background: #fff; + box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.05); } .filter-input::placeholder { @@ -461,20 +495,33 @@ main.container { } .filter-btn { - height: 30px; - padding: 0 18px; + height: 34px; + padding: 0 20px; background: var(--brand-primary); color: #fff; - border: none; - font-size: 12px; + border: 1px solid var(--brand-primary); + border-radius: 17px; + font-size: 13px; cursor: pointer; font-family: inherit; letter-spacing: 1px; - transition: background 0.2s; + transition: background 0.2s, border-color 0.2s, color 0.2s; } .filter-btn:hover { background: var(--brand-primary-deep); + border-color: var(--brand-primary-deep); +} + +.filter-btn-plain { + background: #fff; + color: var(--brand-primary); +} + +.filter-btn-plain:hover { + background: #f7f8fa; + border-color: var(--brand-primary); + color: var(--brand-primary); } /* ========== 公示列表 ========== */ @@ -482,40 +529,14 @@ main.container { padding: 0; } -.notice-item { +.notice-empty { display: flex; - align-items: flex-start; - padding: 14px 0; - border-bottom: 1px solid #f3f4f6; - cursor: pointer; - transition: background 0.2s; - gap: 24px; -} - -.notice-item:hover { - background: #fafbfc; -} - -.notice-item:hover .notice-title { - color: var(--brand-primary); -} - -.notice-date { - flex-shrink: 0; - width: 100px; - font-size: 13px; - color: #6b7280; - font-family: ui-monospace, "Courier New", monospace; - padding-top: 2px; -} - -.notice-title { - flex: 1; + flex-direction: column; + align-items: center; + gap: 12px; + padding: 64px 0; + color: #c0c6cf; font-size: 14px; - color: #1f2937; - line-height: 1.7; - transition: color 0.2s; - min-width: 0; } /* 表头: 与 notice-item 相同的 flex + gap + 列宽, 保证列对齐 */ @@ -523,23 +544,12 @@ main.container { display: flex; align-items: center; gap: 24px; - padding: 12px 0; - border-bottom: 1px solid #e5e7eb; - background: #f9fafb; - font-size: 13px; + padding: 14px 20px; + border-bottom: 1px solid #eef0f3; + font-size: 12px; font-weight: 600; - color: #4b5563; -} - -.notice-header-no { - flex-shrink: 0; - width: 32px; - text-align: center; -} - -.notice-header-date { - flex-shrink: 0; - width: 100px; + color: #9ca3af; + letter-spacing: 1px; } .notice-header-title { @@ -547,29 +557,88 @@ main.container { min-width: 0; } -.notice-header-end { +.notice-header-date { flex-shrink: 0; - width: 100px; + width: 130px; + text-align: right; } -/* 右侧截止时间: 等宽数字 + 灰色, 与日期列同宽对齐 */ -.notice-end { - flex-shrink: 0; - width: 100px; - font-size: 13px; - color: #6b7280; - font-family: ui-monospace, "Courier New", monospace; - padding-top: 2px; +.notice-item { + position: relative; + display: flex; + align-items: center; + gap: 24px; + padding: 16px 20px; + border-bottom: 1px solid #f3f4f6; + cursor: pointer; + transition: background 0.2s; } -.notice-item-no { +.notice-item::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 52%; + border-radius: 2px; + background: var(--brand-primary); + opacity: 0; + transition: opacity 0.2s ease; +} + +.notice-item:hover { + background: #f7f9fb; +} + +.notice-item:hover::before { + opacity: 1; +} + +.notice-item:hover .notice-title { + color: var(--brand-primary); +} + +.notice-title { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 8px; + font-size: 15px; + font-weight: 500; + color: #1f2937; + line-height: 1.6; + transition: color 0.2s; +} + +.notice-arrow { flex-shrink: 0; - width: 32px; - text-align: center; + color: var(--brand-primary); + opacity: 0; + transform: translateX(-6px); + transition: opacity 0.2s ease, transform 0.2s ease; +} + +.notice-item:hover .notice-arrow { + opacity: 1; + transform: translateX(0); +} + +.notice-date { + flex-shrink: 0; + width: 130px; + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 6px; font-size: 12px; - color: #9ca3af; - font-family: ui-monospace, "Courier New", monospace; - padding-top: 2px; + color: #8a919c; +} + +.notice-date-icon { + color: #b6bcc6; } /* ========== 分页 ========== */ diff --git a/ry-vue3/src/views/sponsor/NewPerson.vue b/ry-vue3/src/views/sponsor/NewPerson.vue index 1bcaad6..4adcfef 100644 --- a/ry-vue3/src/views/sponsor/NewPerson.vue +++ b/ry-vue3/src/views/sponsor/NewPerson.vue @@ -212,6 +212,12 @@ function isDuplicateError(msg) { return /已存在|重复|duplicate|uk_person_phone/i.test(msg || '') } +// 按后端原因精确提示: 登录账号(用户名) vs 手机号 (后端分别抛 "登录账号已存在" / DB uk_person_phone) +function duplicateTip(msg) { + if (/登录账号|用户名|login/i.test(msg || '')) return '登录账号已存在,请更换' + return '该手机号已存在,请更换' +} + async function onSave() { try { await formRef.value.validate() @@ -247,12 +253,12 @@ async function onSave() { } } else { const msg = res?.msg || '保存失败' - if (isDuplicateError(msg)) ElMessage.warning('该手机号已存在,请更换') + if (isDuplicateError(msg)) ElMessage.warning(duplicateTip(msg)) else ElMessage.error(msg) } } catch (e) { const msg = e?.msg || e?.message || '保存失败' - if (isDuplicateError(msg)) ElMessage.warning('该手机号已存在,请更换') + if (isDuplicateError(msg)) ElMessage.warning(duplicateTip(msg)) else ElMessage.error(msg) } finally { saving.value = false diff --git a/ry-vue3/src/views/sponsor/Projects.vue b/ry-vue3/src/views/sponsor/Projects.vue index 94cd707..bebfa97 100644 --- a/ry-vue3/src/views/sponsor/Projects.vue +++ b/ry-vue3/src/views/sponsor/Projects.vue @@ -241,6 +241,12 @@ const detail = ref({}) const detailOpen = ref(false) function formatMoney(v) { return Number(v || 0).toFixed(2) } +// 当前本地时间 "yyyy-MM-dd HH:mm:ss" (发布用, 与后端 GMT+8 一致; 不能用 toISOString 的 UTC) +function nowLocalDatetime() { + const d = new Date() + const p = n => String(n).padStart(2, '0') + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}` +} async function load() { loading.value = true @@ -387,7 +393,7 @@ async function onBatchOpen() { try { await ElMessageBox.confirm(`确定对选中的 ${selected.value.length} 个项目批量开通吗?`, '批量开通', { type: 'warning' }) } catch { return } let ok = 0 for (const r of selected.value) { - try { await bizUpdate('project', { projectId: r.projectId, isPublished: '1' }); ok++ } catch {} + try { await bizUpdate('project', { projectId: r.projectId, isPublished: '1', publishTime: nowLocalDatetime() }); ok++ } catch {} } ElMessage.success(`已批量开通 ${ok} 项`) load() @@ -405,7 +411,7 @@ async function onFinish(row) { async function onOpen(row) { try { await ElMessageBox.confirm(`确定开通「${row.projectName}」吗?`, '开通', { type: 'warning' }) } catch { return } try { - await bizUpdate('project', { projectId: row.projectId, isPublished: '1' }) + await bizUpdate('project', { projectId: row.projectId, isPublished: '1', publishTime: nowLocalDatetime() }) ElMessage.success('已开通') load() } catch (e) { ElMessage.error(e?.msg || '操作失败') }