Browse Source

问题处置

main
wang 1 month ago
parent
commit
c51b5b5069
  1. 74
      docs/sql/业务脚本.sql
  2. 35
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/constant/WtczConst.java
  3. 102
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/controller/SurveySampleController.java
  4. 7
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/controller/WtczController.java
  5. 15
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/dao/SurveySampleDao.java
  6. 15
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/dao/SurveySampleGroupDao.java
  7. 15
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/dao/SurveyTaskDao.java
  8. 40
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/entity/SurveySampleEntity.java
  9. 31
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/entity/SurveySampleGroupEntity.java
  10. 55
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/entity/SurveyTaskEntity.java
  11. 3
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/entity/WtczYyEntity.java
  12. 26
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/SurveySampleAddForm.java
  13. 19
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/SurveySampleBatchAddForm.java
  14. 14
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/SurveySampleGroupAddForm.java
  15. 19
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/SurveySampleQueryForm.java
  16. 26
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/SurveyTaskCreateForm.java
  17. 19
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/SurveyTaskQueryForm.java
  18. 19
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/SurveyTaskSubmitAnswerForm.java
  19. 3
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/WtczAddForm.java
  20. 3
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/WtczQueryForm.java
  21. 15
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/vo/WtczVO.java
  22. 3
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/vo/WtczYyVO.java
  23. 221
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/service/SurveySampleService.java
  24. 1
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/service/SurveyService.java
  25. 86
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/service/WtczService.java
  26. 90
      smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/util/WtczUtil.java
  27. 115
      smart-flow-web/src/api/business/jwpy/survey-sample-api.js
  28. 7
      smart-flow-web/src/api/business/jwpy/wtcz-api.js
  29. 6
      smart-flow-web/src/router/flow/flow.js
  30. 2
      smart-flow-web/src/router/index.js
  31. 6
      smart-flow-web/src/views/flow/definition/definition-form.vue
  32. 47
      smart-flow-web/src/views/jwpy/jwpy/flow/components/BizClassificationForm.vue
  33. 47
      smart-flow-web/src/views/jwpy/jwpy/flow/components/CallerInfoForm.vue
  34. 73
      smart-flow-web/src/views/jwpy/jwpy/flow/components/InvolvedTargetForm.vue
  35. 194
      smart-flow-web/src/views/jwpy/jwpy/flow/index.vue
  36. 66
      smart-flow-web/src/views/jwpy/jwpy/flow/utils/jwpy-util.js
  37. 140
      smart-flow-web/src/views/jwpy/jwpy/flow/wtcz-detail.vue
  38. 58
      smart-flow-web/src/views/jwpy/jwpy/flow/wtcz-form.vue
  39. 277
      smart-flow-web/src/views/jwpy/mydc/my-survey/components/SurveyFormRenderer.vue
  40. 491
      smart-flow-web/src/views/jwpy/mydc/my-survey/fill.vue
  41. 493
      smart-flow-web/src/views/jwpy/mydc/my-survey/index.vue
  42. 215
      smart-flow-web/src/views/jwpy/mydc/questionnaire/designer.vue
  43. 163
      smart-flow-web/src/views/jwpy/mydc/questionnaire/index.vue
  44. 657
      smart-flow-web/src/views/jwpy/mydc/sample/index.vue

74
docs/sql/业务脚本.sql

@ -53,3 +53,77 @@ CREATE INDEX idx_phone_record_call_status ON t_phone_record(call_status);
-- 序列(用于主键自增)
CREATE SEQUENCE IF NOT EXISTS seq_phone_record_id START WITH 1 INCREMENT BY 1;
-- =============================================
-- 民意调查样本库与评议任务表结构(人大金仓 / PostgreSQL 兼容)
-- =============================================
-- 1. 样本分类分组表
CREATE TABLE IF NOT EXISTS t_survey_sample_group (
id BIGINT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE t_survey_sample_group IS '样本分类分组表';
COMMENT ON COLUMN t_survey_sample_group.id IS '主键ID';
COMMENT ON COLUMN t_survey_sample_group.name IS '分组名称 (如:高级精英人才库)';
COMMENT ON COLUMN t_survey_sample_group.create_time IS '创建时间';
-- 2. 样本人员名单表
CREATE TABLE IF NOT EXISTS t_survey_sample (
id BIGINT PRIMARY KEY,
group_id BIGINT NOT NULL,
name VARCHAR(50) NOT NULL,
phone VARCHAR(20) NOT NULL,
remarks VARCHAR(255),
import_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE t_survey_sample IS '样本人员表';
COMMENT ON COLUMN t_survey_sample.id IS '主键ID';
COMMENT ON COLUMN t_survey_sample.group_id IS '关联分组ID';
COMMENT ON COLUMN t_survey_sample.name IS '姓名';
COMMENT ON COLUMN t_survey_sample.phone IS '联系电话';
COMMENT ON COLUMN t_survey_sample.remarks IS '分类备注';
COMMENT ON COLUMN t_survey_sample.import_time IS '导入时间';
-- 3. 调查测评任务表
CREATE TABLE IF NOT EXISTS t_survey_task (
id BIGINT PRIMARY KEY,
title VARCHAR(200) NOT NULL,
name VARCHAR(50) NOT NULL,
phone VARCHAR(20) NOT NULL,
sjly_no VARCHAR(50),
call_count INTEGER DEFAULT 0,
status INTEGER DEFAULT 0,
survey_title VARCHAR(200),
survey_id BIGINT,
answers TEXT,
assign_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE t_survey_task IS '调查测评任务表';
COMMENT ON COLUMN t_survey_task.id IS '主键ID';
COMMENT ON COLUMN t_survey_task.title IS '任务名称';
COMMENT ON COLUMN t_survey_task.name IS '受访群众姓名';
COMMENT ON COLUMN t_survey_task.phone IS '联系电话';
COMMENT ON COLUMN t_survey_task.sjly_no IS '关联警务分类编码';
COMMENT ON COLUMN t_survey_task.call_count IS '呼叫次数';
COMMENT ON COLUMN t_survey_task.status IS '状态 (0待调查, 1调查成功, 2无人接听, 3明确拒访, 4已发送短信评议)';
COMMENT ON COLUMN t_survey_task.survey_title IS '关联问卷模板名称';
COMMENT ON COLUMN t_survey_task.survey_id IS '关联的问卷模板ID';
COMMENT ON COLUMN t_survey_task.answers IS '群众填报的答卷JSON详情';
COMMENT ON COLUMN t_survey_task.assign_time IS '生成时间';
-- 预置默认分组与测试样本数据
INSERT INTO t_survey_sample_group (id, name, create_time) VALUES
(1001, '高级精英人才库', CURRENT_TIMESTAMP),
(1002, '警务窗口服务评议库', CURRENT_TIMESTAMP),
(1003, '110接处警抽样库', CURRENT_TIMESTAMP)
ON CONFLICT (id) DO NOTHING;
INSERT INTO t_survey_sample (id, group_id, name, phone, remarks, import_time) VALUES
(2001, 1001, '张大胖', '18944445555', '高层次人才领军学者', CURRENT_TIMESTAMP),
(2002, 1001, '陈科研', '18655556666', '杰出青年科研人才', CURRENT_TIMESTAMP),
(2003, 1002, '韩梅梅', '13711112222', '户政大厅办事人员', CURRENT_TIMESTAMP),
(2004, 1002, '李雷', '13633334444', '出入境窗口满意度抽评', CURRENT_TIMESTAMP),
(2005, 1003, '王有才', '13899998888', '110处警受害者电话', CURRENT_TIMESTAMP)
ON CONFLICT (id) DO NOTHING;

35
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/constant/WtczConst.java

@ -0,0 +1,35 @@
package net.lab1024.sa.admin.module.business.jwpy.constant;
/**
* 警务评议工单业务常量定义类
*
* @Author Antigravity
* @Date 2026-06-14
*/
public class WtczConst {
/**
* 工单状态已录入/暂存待首访
*/
public static final String GDZT_STASH = "0";
/**
* 工单状态办结中/待二次回访反馈
*/
public static final String GDZT_PROCESSING = "1";
/**
* 工单状态已办结
*/
public static final String GDZT_COMPLETED = "2";
/**
* 回访阶段1首访通话
*/
public static final String XFJD_FIRST = "1";
/**
* 回访阶段3二次回访通话
*/
public static final String XFJD_SECOND = "3";
}

102
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/controller/SurveySampleController.java

@ -0,0 +1,102 @@
package net.lab1024.sa.admin.module.business.jwpy.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import net.lab1024.sa.admin.module.business.jwpy.domain.entity.SurveySampleEntity;
import net.lab1024.sa.admin.module.business.jwpy.domain.entity.SurveySampleGroupEntity;
import net.lab1024.sa.admin.module.business.jwpy.domain.entity.SurveyTaskEntity;
import net.lab1024.sa.admin.module.business.jwpy.domain.form.*;
import net.lab1024.sa.admin.module.business.jwpy.service.SurveySampleService;
import net.lab1024.sa.base.common.domain.PageResult;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 调查问卷样本库与评议任务 控制器
*
* @Author Antigravity
* @Date 2026-06-15
*/
@RestController
@Tag(name = "业务功能-调查样本与评议分发")
public class SurveySampleController {
@Resource
private SurveySampleService sampleService;
// ================= 分组管理 =================
@Operation(summary = "查询样本库分类列表")
@GetMapping("/mydc/sample/group/list")
public ResponseDTO<List<SurveySampleGroupEntity>> groupList() {
return ResponseDTO.ok(sampleService.groupList());
}
@Operation(summary = "创建新样本库分类")
@PostMapping("/mydc/sample/group/add")
public ResponseDTO<String> addGroup(@RequestBody @Valid SurveySampleGroupAddForm form) {
return sampleService.addGroup(form);
}
@Operation(summary = "移除样本库分类及其样本")
@GetMapping("/mydc/sample/group/delete/{id}")
public ResponseDTO<String> deleteGroup(@PathVariable Long id) {
return sampleService.deleteGroup(id);
}
// ================= 名单管理 =================
@Operation(summary = "分页筛选样本人员名单")
@PostMapping("/mydc/sample/queryPage")
public ResponseDTO<PageResult<SurveySampleEntity>> querySamplePage(@RequestBody @Valid SurveySampleQueryForm queryForm) {
return ResponseDTO.ok(sampleService.querySamplePage(queryForm));
}
@Operation(summary = "单条录入样本人员")
@PostMapping("/mydc/sample/add")
public ResponseDTO<String> addSample(@RequestBody @Valid SurveySampleAddForm form) {
return sampleService.addSample(form);
}
@Operation(summary = "粘贴多行文本批量解析导入")
@PostMapping("/mydc/sample/batchAdd")
public ResponseDTO<String> batchAddSamples(@RequestBody @Valid SurveySampleBatchAddForm form) {
return sampleService.batchAddSamples(form);
}
@Operation(summary = "移除样本库成员")
@GetMapping("/mydc/sample/delete/{id}")
public ResponseDTO<String> deleteSample(@PathVariable Long id) {
return sampleService.deleteSample(id);
}
// ================= 测评任务管理 =================
@Operation(summary = "分页查询测评任务列表")
@PostMapping("/mydc/task/queryPage")
public ResponseDTO<PageResult<SurveyTaskEntity>> queryTaskPage(@RequestBody @Valid SurveyTaskQueryForm queryForm) {
return ResponseDTO.ok(sampleService.queryTaskPage(queryForm));
}
@Operation(summary = "查询单个测评任务详情")
@GetMapping("/mydc/task/detail/{id}")
public ResponseDTO<SurveyTaskEntity> getTaskDetail(@PathVariable Long id) {
return ResponseDTO.ok(sampleService.getTaskDetail(id));
}
@Operation(summary = "新建调查任务并批量推送生成名单")
@PostMapping("/mydc/task/create")
public ResponseDTO<String> createSurveyTask(@RequestBody @Valid SurveyTaskCreateForm form) {
return sampleService.createSurveyTask(form);
}
@Operation(summary = "H5填报端提交群众测评答案")
@PostMapping("/mydc/task/submitAnswer")
public ResponseDTO<String> submitH5Answer(@RequestBody @Valid SurveyTaskSubmitAnswerForm form) {
return sampleService.submitH5Answer(form);
}
}

7
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/controller/WtczController.java

@ -15,6 +15,7 @@ import net.lab1024.sa.base.common.util.SmartRequestUtil;
import net.lab1024.sa.base.common.domain.PageResult;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.domain.ValidateList;
import net.lab1024.sa.admin.module.business.jwpy.domain.entity.WtczYyEntity;
import org.springframework.web.bind.annotation.*;
/**
@ -93,4 +94,10 @@ public class WtczController {
Long employeeId = SmartRequestUtil.getRequestUserId();
return wtczService.claimRandom(employeeId);
}
@Operation(summary = "保存外呼通话录音流水")
@PostMapping("/jwpy/wtcz/addYy")
public ResponseDTO<String> addYy(@RequestBody @Valid WtczYyEntity entity) {
return wtczService.addYy(entity);
}
}

15
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/dao/SurveySampleDao.java

@ -0,0 +1,15 @@
package net.lab1024.sa.admin.module.business.jwpy.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import net.lab1024.sa.admin.module.business.jwpy.domain.entity.SurveySampleEntity;
import org.apache.ibatis.annotations.Mapper;
/**
* 调查问卷样本人员 Dao 接口
*
* @Author Antigravity
* @Date 2026-06-15
*/
@Mapper
public interface SurveySampleDao extends BaseMapper<SurveySampleEntity> {
}

15
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/dao/SurveySampleGroupDao.java

@ -0,0 +1,15 @@
package net.lab1024.sa.admin.module.business.jwpy.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import net.lab1024.sa.admin.module.business.jwpy.domain.entity.SurveySampleGroupEntity;
import org.apache.ibatis.annotations.Mapper;
/**
* 调查问卷样本分组 Dao 接口
*
* @Author Antigravity
* @Date 2026-06-15
*/
@Mapper
public interface SurveySampleGroupDao extends BaseMapper<SurveySampleGroupEntity> {
}

15
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/dao/SurveyTaskDao.java

@ -0,0 +1,15 @@
package net.lab1024.sa.admin.module.business.jwpy.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import net.lab1024.sa.admin.module.business.jwpy.domain.entity.SurveyTaskEntity;
import org.apache.ibatis.annotations.Mapper;
/**
* 调查测评任务 Dao 接口
*
* @Author Antigravity
* @Date 2026-06-15
*/
@Mapper
public interface SurveyTaskDao extends BaseMapper<SurveyTaskEntity> {
}

40
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/entity/SurveySampleEntity.java

@ -0,0 +1,40 @@
package net.lab1024.sa.admin.module.business.jwpy.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 调查问卷样本人员 实体类
*
* @Author Antigravity
* @Date 2026-06-15
*/
@Data
@TableName("t_survey_sample")
@Schema(description = "调查问卷样本人员实体")
public class SurveySampleEntity {
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "样本人ID")
private Long id;
@Schema(description = "关联分组ID")
private Long groupId;
@Schema(description = "群众/人才姓名")
private String name;
@Schema(description = "手机号")
private String phone;
@Schema(description = "备注说明")
private String remarks;
@Schema(description = "录入时间")
private LocalDateTime importTime;
}

31
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/entity/SurveySampleGroupEntity.java

@ -0,0 +1,31 @@
package net.lab1024.sa.admin.module.business.jwpy.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 调查问卷样本分组 实体类
*
* @Author Antigravity
* @Date 2026-06-15
*/
@Data
@TableName("t_survey_sample_group")
@Schema(description = "调查问卷样本分组实体")
public class SurveySampleGroupEntity {
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "分组ID")
private Long id;
@Schema(description = "分类/分组名称")
private String name;
@Schema(description = "创建时间")
private LocalDateTime createTime;
}

55
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/entity/SurveyTaskEntity.java

@ -0,0 +1,55 @@
package net.lab1024.sa.admin.module.business.jwpy.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 调查测评任务 实体类
*
* @Author Antigravity
* @Date 2026-06-15
*/
@Data
@TableName("t_survey_task")
@Schema(description = "调查测评任务实体")
public class SurveyTaskEntity {
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "任务ID")
private Long id;
@Schema(description = "任务名称")
private String title;
@Schema(description = "受访群众姓名")
private String name;
@Schema(description = "联系电话")
private String phone;
@Schema(description = "关联警务分类编码")
private String sjlyNo;
@Schema(description = "呼叫次数")
private Integer callCount;
@Schema(description = "状态: 0待调查, 1调查成功, 2无人接听, 3明确拒访, 4已发送短信评议")
private Integer status;
@Schema(description = "关联问卷模板名称")
private String surveyTitle;
@Schema(description = "关联的问卷模板ID")
private Long surveyId;
@Schema(description = "群众填报的答卷JSON详情")
private String answers;
@Schema(description = "生成/分配时间")
private LocalDateTime assignTime;
}

3
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/entity/WtczYyEntity.java

@ -64,6 +64,9 @@ public class WtczYyEntity {
@Schema(description = "语音通话记录表ID")
private Long yyId;
@Schema(description = "回访阶段:1首访通话,3二次回访通话")
private String xfjd;
}

26
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/SurveySampleAddForm.java

@ -0,0 +1,26 @@
package net.lab1024.sa.admin.module.business.jwpy.domain.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
@Schema(description = "新增单个样本人员表单")
public class SurveySampleAddForm {
@Schema(description = "样本组ID")
@NotNull(message = "样本分组不能为空")
private Long groupId;
@Schema(description = "姓名")
@NotBlank(message = "姓名不能为空")
private String name;
@Schema(description = "联系电话")
@NotBlank(message = "电话不能为空")
private String phone;
@Schema(description = "分类备注/描述")
private String remarks;
}

19
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/SurveySampleBatchAddForm.java

@ -0,0 +1,19 @@
package net.lab1024.sa.admin.module.business.jwpy.domain.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
@Schema(description = "批量粘贴解析导入样本表单")
public class SurveySampleBatchAddForm {
@Schema(description = "样本组ID")
@NotNull(message = "样本分组不能为空")
private Long groupId;
@Schema(description = "粘贴的解析文本块")
@NotBlank(message = "粘贴的数据文本不能为空")
private String text;
}

14
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/SurveySampleGroupAddForm.java

@ -0,0 +1,14 @@
package net.lab1024.sa.admin.module.business.jwpy.domain.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "新增样本分组表单")
public class SurveySampleGroupAddForm {
@Schema(description = "分类/分组名称")
@NotBlank(message = "分组名称不能为空")
private String name;
}

19
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/SurveySampleQueryForm.java

@ -0,0 +1,19 @@
package net.lab1024.sa.admin.module.business.jwpy.domain.form;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import net.lab1024.sa.base.common.domain.PageParam;
@Data
@Schema(description = "样本名单分页查询表单")
public class SurveySampleQueryForm extends PageParam {
@Schema(description = "样本组ID")
private Long groupId;
@Schema(description = "姓名/模糊查询")
private String name;
@Schema(description = "手机号")
private String phone;
}

26
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/SurveyTaskCreateForm.java

@ -0,0 +1,26 @@
package net.lab1024.sa.admin.module.business.jwpy.domain.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
@Schema(description = "新建并推送调查测评任务表单")
public class SurveyTaskCreateForm {
@Schema(description = "测评任务名称")
@NotBlank(message = "任务名称不能为空")
private String title;
@Schema(description = "样本分组ID")
@NotNull(message = "请选择目标样本分组")
private Long groupId;
@Schema(description = "关联的问卷模板ID")
@NotNull(message = "请选择关联的问卷模板")
private Long surveyId;
@Schema(description = "推送通知方式: sms 短信, wx 微信")
private String pushType;
}

19
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/SurveyTaskQueryForm.java

@ -0,0 +1,19 @@
package net.lab1024.sa.admin.module.business.jwpy.domain.form;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import net.lab1024.sa.base.common.domain.PageParam;
@Data
@Schema(description = "测评任务分页查询表单")
public class SurveyTaskQueryForm extends PageParam {
@Schema(description = "受访群众姓名/模糊查询")
private String name;
@Schema(description = "任务状态: 0待调查, 1调查成功, 2无人接听, 3明确拒访, 4已发送")
private Integer status;
@Schema(description = "是否只查询待回访的列表")
private Boolean isTodoView;
}

19
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/SurveyTaskSubmitAnswerForm.java

@ -0,0 +1,19 @@
package net.lab1024.sa.admin.module.business.jwpy.domain.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
@Schema(description = "群众自助填报提交表单")
public class SurveyTaskSubmitAnswerForm {
@Schema(description = "任务明细ID")
@NotNull(message = "任务ID不能为空")
private Long taskId;
@Schema(description = "答卷反馈选项JSON内容")
@NotBlank(message = "答卷内容不能为空")
private String answers;
}

3
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/WtczAddForm.java

@ -15,8 +15,7 @@ import java.time.LocalDateTime;
@Data
public class WtczAddForm {
@Schema(description = "工单受理编号", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "工单受理编号 不能为空")
@Schema(description = "工单受理编号")
private String slbh;
@Schema(description = "地市4位数字编码")

3
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/WtczQueryForm.java

@ -40,4 +40,7 @@ public class WtczQueryForm extends PageParam {
@Schema(description = "分派认领状态: 0未认领, 1已认领")
private Integer assignStatus;
@Schema(description = "菜单类型:all全部, to_handle待办, transferred已流转, completed已办结, about_to_expire即将超期, expired已超期, invalid已作废")
private String tabType;
}

15
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/vo/WtczVO.java

@ -201,6 +201,21 @@ public class WtczVO {
@Schema(description = "首访通话语音列表")
private java.util.List<WtczYyVO> czzxWtczYyList;
@Schema(description = "二访通话语音列表")
private java.util.List<WtczYyVO> secondCallList;
@Schema(description = "二次回访记录列表")
private java.util.List<WtczHfhcVO> czzxWtczHfhcList;
@Schema(description = "涉及业务小类名称")
private String sjlyNoName;
@Schema(description = "诉求分类名称")
private String sqflName;
@Schema(description = "群众满意度名称")
private String qzmyqkName;
@Schema(description = "工单状态名称")
private String gdztName;
}

3
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/vo/WtczYyVO.java

@ -62,4 +62,7 @@ public class WtczYyVO {
@Schema(description = "语音通话记录表ID")
private Long yyId;
@Schema(description = "回访阶段:1首访通话,3二次回访通话")
private String xfjd;
}

221
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/service/SurveySampleService.java

@ -0,0 +1,221 @@
package net.lab1024.sa.admin.module.business.jwpy.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.admin.module.business.jwpy.dao.SurveyDao;
import net.lab1024.sa.admin.module.business.jwpy.dao.SurveySampleDao;
import net.lab1024.sa.admin.module.business.jwpy.dao.SurveySampleGroupDao;
import net.lab1024.sa.admin.module.business.jwpy.dao.SurveyTaskDao;
import net.lab1024.sa.admin.module.business.jwpy.domain.entity.SurveyEntity;
import net.lab1024.sa.admin.module.business.jwpy.domain.entity.SurveySampleEntity;
import net.lab1024.sa.admin.module.business.jwpy.domain.entity.SurveySampleGroupEntity;
import net.lab1024.sa.admin.module.business.jwpy.domain.entity.SurveyTaskEntity;
import net.lab1024.sa.admin.module.business.jwpy.domain.form.*;
import net.lab1024.sa.base.common.domain.PageResult;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.util.SmartBeanUtil;
import net.lab1024.sa.base.common.util.SmartPageUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* 调查问卷样本库与评议任务 业务服务类
*
* @Author Antigravity
* @Date 2026-06-15
*/
@Service
@Slf4j
public class SurveySampleService {
@Resource
private SurveySampleGroupDao groupDao;
@Resource
private SurveySampleDao sampleDao;
@Resource
private SurveyTaskDao surveyTaskDao;
@Resource
private SurveyDao surveyDao;
// ================= 分组管理 =================
public List<SurveySampleGroupEntity> groupList() {
return groupDao.selectList(Wrappers.<SurveySampleGroupEntity>lambdaQuery().orderByAsc(SurveySampleGroupEntity::getId));
}
public ResponseDTO<String> addGroup(SurveySampleGroupAddForm form) {
SurveySampleGroupEntity entity = new SurveySampleGroupEntity();
entity.setName(form.getName());
entity.setCreateTime(LocalDateTime.now());
groupDao.insert(entity);
return ResponseDTO.ok("分组创建成功");
}
public ResponseDTO<String> deleteGroup(Long id) {
groupDao.deleteById(id);
// 联动删除该分组下的所有人员
sampleDao.delete(Wrappers.<SurveySampleEntity>lambdaQuery().eq(SurveySampleEntity::getGroupId, id));
return ResponseDTO.ok("分类及下属样本删除成功");
}
// ================= 样本名单管理 =================
@SuppressWarnings("unchecked")
public PageResult<SurveySampleEntity> querySamplePage(SurveySampleQueryForm queryForm) {
Page<SurveySampleEntity> page = (Page<SurveySampleEntity>) SmartPageUtil.convert2PageQuery(queryForm);
LambdaQueryWrapper<SurveySampleEntity> wrapper = Wrappers.lambdaQuery();
if (queryForm.getGroupId() != null) {
wrapper.eq(SurveySampleEntity::getGroupId, queryForm.getGroupId());
}
if (StringUtils.isNotBlank(queryForm.getName())) {
wrapper.like(SurveySampleEntity::getName, queryForm.getName());
}
if (StringUtils.isNotBlank(queryForm.getPhone())) {
wrapper.eq(SurveySampleEntity::getPhone, queryForm.getPhone());
}
wrapper.orderByDesc(SurveySampleEntity::getImportTime);
sampleDao.selectPage(page, wrapper);
return SmartPageUtil.convert2PageResult(page, page.getRecords(), SurveySampleEntity.class);
}
public ResponseDTO<String> addSample(SurveySampleAddForm form) {
SurveySampleEntity entity = SmartBeanUtil.copy(form, SurveySampleEntity.class);
entity.setImportTime(LocalDateTime.now());
sampleDao.insert(entity);
return ResponseDTO.ok("录入样本成功");
}
@Transactional(rollbackFor = Exception.class)
public ResponseDTO<String> batchAddSamples(SurveySampleBatchAddForm form) {
String text = form.getText();
if (StringUtils.isBlank(text)) {
return ResponseDTO.userErrorParam("粘贴内容不能为空");
}
String[] lines = text.split("\n");
List<SurveySampleEntity> list = new ArrayList<>();
for (String line : lines) {
if (StringUtils.isBlank(line)) continue;
String[] parts = line.split("[,,\\s]+");
if (parts.length >= 2) {
String name = parts[0].trim();
String phone = parts[1].trim();
String remarks = parts.length >= 3 ? parts[2].trim() : "";
if (phone.matches("\\d{11}")) {
SurveySampleEntity s = new SurveySampleEntity();
s.setGroupId(form.getGroupId());
s.setName(name);
s.setPhone(phone);
s.setRemarks(remarks);
s.setImportTime(LocalDateTime.now());
list.add(s);
}
}
}
if (list.isEmpty()) {
return ResponseDTO.userErrorParam("解析失败,未提取到符合格式要求的人员信息");
}
for (SurveySampleEntity s : list) {
sampleDao.insert(s);
}
return ResponseDTO.ok("成功导入 " + list.size() + " 条样本数据");
}
public ResponseDTO<String> deleteSample(Long id) {
sampleDao.deleteById(id);
return ResponseDTO.ok("移除样本人员成功");
}
// ================= 调查评议任务管理 =================
@SuppressWarnings("unchecked")
public PageResult<SurveyTaskEntity> queryTaskPage(SurveyTaskQueryForm queryForm) {
Page<SurveyTaskEntity> page = (Page<SurveyTaskEntity>) SmartPageUtil.convert2PageQuery(queryForm);
LambdaQueryWrapper<SurveyTaskEntity> wrapper = Wrappers.lambdaQuery();
if (StringUtils.isNotBlank(queryForm.getName())) {
wrapper.like(SurveyTaskEntity::getName, queryForm.getName());
}
if (queryForm.getStatus() != null) {
wrapper.eq(SurveyTaskEntity::getStatus, queryForm.getStatus());
} else {
// 待办/已发推送等自适应过滤
if (queryForm.getIsTodoView() != null && queryForm.getIsTodoView()) {
wrapper.in(SurveyTaskEntity::getStatus, 0, 2, 4); // 待调查、未接听、已发送推送
}
}
wrapper.orderByDesc(SurveyTaskEntity::getAssignTime);
surveyTaskDao.selectPage(page, wrapper);
return SmartPageUtil.convert2PageResult(page, page.getRecords(), SurveyTaskEntity.class);
}
public SurveyTaskEntity getTaskDetail(Long id) {
return surveyTaskDao.selectById(id);
}
@Transactional(rollbackFor = Exception.class)
public ResponseDTO<String> createSurveyTask(SurveyTaskCreateForm form) {
// 1. 获取分组样本人员
List<SurveySampleEntity> samples = sampleDao.selectList(
Wrappers.<SurveySampleEntity>lambdaQuery().eq(SurveySampleEntity::getGroupId, form.getGroupId())
);
if (samples.isEmpty()) {
return ResponseDTO.userErrorParam("所选的分组下没有任何样本人员,请先录入名单");
}
// 2. 获取关联问卷详情
SurveyEntity survey = surveyDao.selectById(form.getSurveyId());
if (survey == null) {
return ResponseDTO.userErrorParam("绑定的问卷模板不存在");
}
if (survey.getStatus() == null || survey.getStatus() != 1) {
return ResponseDTO.userErrorParam("该问卷模板尚未启用,无法创建测评任务!");
}
// 3. 循环转换生成评议任务
for (SurveySampleEntity s : samples) {
SurveyTaskEntity task = new SurveyTaskEntity();
task.setTitle(form.getTitle());
task.setName(s.getName());
task.setPhone(s.getPhone());
task.setSjlyNo(form.getGroupId() == 1001L ? "crj" : (form.getGroupId() == 1002L ? "hz" : "110")); // 业务分类标签颜色映射
task.setCallCount(0);
task.setStatus(4); // 直接设置为已发推送评议状态
task.setSurveyTitle(survey.getTitle());
task.setSurveyId(survey.getSurveyId());
task.setAssignTime(LocalDateTime.now());
surveyTaskDao.insert(task);
}
return ResponseDTO.ok("成功为 " + samples.size() + " 位群众生成评议任务并下发通知!");
}
public ResponseDTO<String> submitH5Answer(SurveyTaskSubmitAnswerForm form) {
SurveyTaskEntity task = surveyTaskDao.selectById(form.getTaskId());
if (task == null) {
return ResponseDTO.userErrorParam("测评任务明细不存在");
}
task.setStatus(1); // 状态更新为“调查成功/已评议”
task.setAnswers(form.getAnswers());
surveyTaskDao.updateById(task);
return ResponseDTO.ok("提交成功,感谢您的测评反馈!");
}
}

1
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/service/SurveyService.java

@ -14,7 +14,6 @@ import net.lab1024.sa.admin.module.business.jwpy.domain.form.SurveyAddForm;
import net.lab1024.sa.admin.module.business.jwpy.domain.form.SurveyUpdateForm;
import net.lab1024.sa.admin.module.business.jwpy.domain.form.SurveyAnswerAddForm;
import net.lab1024.sa.admin.module.business.jwpy.domain.vo.SurveyVO;
import net.lab1024.sa.admin.module.business.jwpy.domain.vo.SurveyAnswerVO;
import net.lab1024.sa.base.common.domain.PageResult;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.util.SmartBeanUtil;

86
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/service/WtczService.java

@ -8,6 +8,8 @@ import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.admin.module.business.jwpy.dao.WtczDao;
import net.lab1024.sa.admin.module.business.jwpy.dao.WtczYyDao;
import net.lab1024.sa.admin.module.business.jwpy.dao.WtczHfhcDao;
import net.lab1024.sa.admin.module.business.jwpy.constant.WtczConst;
import net.lab1024.sa.admin.module.business.jwpy.util.WtczUtil;
import net.lab1024.sa.admin.module.business.jwpy.domain.entity.WtczEntity;
import net.lab1024.sa.admin.module.business.jwpy.domain.entity.WtczYyEntity;
import net.lab1024.sa.admin.module.business.jwpy.domain.entity.WtczHfhcEntity;
@ -76,6 +78,9 @@ public class WtczService {
@Resource
private FlowTaskMapper flowTaskMapper;
@Resource
private net.lab1024.sa.base.module.support.dict.service.DictService dictService;
/**
* 分页查询评议工单列表
*
@ -98,6 +103,9 @@ public class WtczService {
wrapper.eq(WtczEntity::getSjlyNo, queryForm.getSjlyNo());
}
// 根据菜单类型(如 待办、已流转、已办结)统一织入状态过滤条件
WtczUtil.buildTabTypeFilter(wrapper, queryForm.getTabType());
// 工单状态
if (StringUtils.isNotBlank(queryForm.getGdzt())) {
wrapper.eq(WtczEntity::getGdzt, queryForm.getGdzt());
@ -134,6 +142,7 @@ public class WtczService {
wtczDao.selectPage(page, wrapper);
List<WtczVO> voList = SmartBeanUtil.copyList(page.getRecords(), WtczVO.class);
fillFlowInfo(voList);
fillDictNames(voList);
PageResult<WtczVO> pageResult = SmartPageUtil.convert2PageResult(page, voList);
return ResponseDTO.ok(pageResult);
}
@ -164,6 +173,33 @@ public class WtczService {
}
}
private void fillDictNames(List<WtczVO> voList) {
if (CollectionUtils.isEmpty(voList)) {
return;
}
for (WtczVO vo : voList) {
if (StringUtils.isNotBlank(vo.getSjlyNo())) {
vo.setSjlyNoName(dictService.getDictDataLabel("BUSINESS_CLASSIFICATION", vo.getSjlyNo()));
}
if (StringUtils.isNotBlank(vo.getSqfl())) {
vo.setSqflName(dictService.getDictDataLabel("CZZX_SQFL", vo.getSqfl()));
}
if (StringUtils.isNotBlank(vo.getQzmyqk())) {
vo.setQzmyqkName(dictService.getDictDataLabel("CALLBACK_SATISFACTION", vo.getQzmyqk()));
}
if (StringUtils.isNotBlank(vo.getGdzt())) {
String gdzt = vo.getGdzt();
if ("0".equals(gdzt)) {
vo.setGdztName("待首访");
} else if ("1".equals(gdzt)) {
vo.setGdztName("办理中");
} else if ("2".equals(gdzt)) {
vo.setGdztName("已办结");
}
}
}
}
/**
* 检测若该小类暂无数据则自动往 t_czzx_wtczt_czzx_wtcz_yyt_czzx_wtcz_hfhc 中注入 3 Mock 数据方便流程联调与演示
*
@ -284,6 +320,10 @@ public class WtczService {
*/
@Transactional(rollbackFor = Exception.class)
public ResponseDTO<String> add(WtczAddForm addForm) {
if (StringUtils.isBlank(addForm.getSlbh())) {
addForm.setSlbh(WtczUtil.generateSlbh());
}
// 校验受理编号是否唯一
long count = wtczDao.selectCount(Wrappers.<WtczEntity>lambdaQuery().eq(WtczEntity::getSlbh, addForm.getSlbh()));
if (count > 0) {
@ -292,7 +332,7 @@ public class WtczService {
WtczEntity entity = SmartBeanUtil.copy(addForm, WtczEntity.class);
wtczDao.insert(entity);
return ResponseDTO.ok("添加成功");
return ResponseDTO.ok(entity.getId().toString());
}
/**
@ -365,16 +405,26 @@ public class WtczService {
WtczVO vo = SmartBeanUtil.copy(entity, WtczVO.class);
fillFlowInfo(List.of(vo));
fillDictNames(List.of(vo));
// 1. 查询并聚合首访通话语音列表
// 1. 查询并聚合首访通话语音列表与二次回访语音列表
if (StringUtils.isNotBlank(vo.getSlbh())) {
List<WtczYyEntity> yyEntities = wtczYyDao.selectList(Wrappers.<WtczYyEntity>lambdaQuery()
.eq(WtczYyEntity::getSlbh, vo.getSlbh())
.eq(WtczYyEntity::getXfjd, WtczConst.XFJD_FIRST)
.orderByDesc(WtczYyEntity::getId));
if (CollectionUtils.isNotEmpty(yyEntities)) {
vo.setCzzxWtczYyList(SmartBeanUtil.copyList(yyEntities, WtczYyVO.class));
}
List<WtczYyEntity> secondYyEntities = wtczYyDao.selectList(Wrappers.<WtczYyEntity>lambdaQuery()
.eq(WtczYyEntity::getSlbh, vo.getSlbh())
.eq(WtczYyEntity::getXfjd, WtczConst.XFJD_SECOND)
.orderByDesc(WtczYyEntity::getId));
if (CollectionUtils.isNotEmpty(secondYyEntities)) {
vo.setSecondCallList(SmartBeanUtil.copyList(secondYyEntities, WtczYyVO.class));
}
// 2. 查询并聚合二次回访核实记录
List<WtczHfhcEntity> hfhcEntities = wtczHfhcDao.selectList(Wrappers.<WtczHfhcEntity>lambdaQuery()
.eq(WtczHfhcEntity::getSlbh, vo.getSlbh())
@ -408,7 +458,7 @@ public class WtczService {
throw new BusinessException("当前工单没有可办理的流程任务,请检查流程定义节点配置");
}
if (CollectionUtils.isNotEmpty(taskList)) {
if (CollectionUtils.isNotEmpty(taskList) && action != WtczWorkflowAction.STASH) {
FlowParams flowParams = new FlowParams();
flowParams.skipType(SkipType.PASS.getKey());
flowParams.flowCode(FLOW_CODE);
@ -554,6 +604,36 @@ public class WtczService {
return ResponseDTO.userErrorParam("系统当前繁忙或工单已抢空,请稍后刷新重试");
}
/**
* 新增外呼通话录音记录并根据当前工单流转状态智能拦截并注入回访阶段 (xfjd) 属性
*
* @param entity 通话录音实体数据
* @return 保存结果
*/
@Transactional(rollbackFor = Exception.class)
public ResponseDTO<String> addYy(WtczYyEntity entity) {
if (entity == null || StringUtils.isBlank(entity.getSlbh())) {
return ResponseDTO.userErrorParam("工单受理编号不能为空");
}
// 1. 查询关联的评议工单主表信息
WtczEntity wtcz = wtczDao.selectOne(Wrappers.<WtczEntity>lambdaQuery()
.eq(WtczEntity::getSlbh, entity.getSlbh()));
// 2. 根据工单当前处理状态,智能推导当前回访阶段
if (wtcz != null) {
entity.setXfjd(WtczUtil.determineXfjdByGdzt(wtcz.getGdzt()));
} else {
entity.setXfjd(WtczConst.XFJD_FIRST); // 默认首访
}
if (entity.getCreateTime() == null) {
entity.setCreateTime(LocalDateTime.now());
}
wtczYyDao.insert(entity);
return ResponseDTO.ok("通话录音流水保存成功");
}
private enum WtczWorkflowAction {
FIRST_CLOSE("firstClose", "2", "1", "1", "CZZX_MYD_01", null, null, null, true,
"首访沟通清楚,群众表示满意,工单直接办结归档。"),

90
smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/util/WtczUtil.java

@ -0,0 +1,90 @@
package net.lab1024.sa.admin.module.business.jwpy.util;
import net.lab1024.sa.admin.module.business.jwpy.constant.WtczConst;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 警务评议工单业务工具类
*
* @Author Antigravity
* @Date 2026-06-14
*/
public class WtczUtil {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 根据工单当前处理状态智能推导当前对应的回访阶段首访或二访
*
* @param gdzt 工单处理状态
* @return 对应的回访阶段标识
*/
public static String determineXfjdByGdzt(String gdzt) {
if (WtczConst.GDZT_STASH.equals(gdzt)) {
return WtczConst.XFJD_FIRST;
} else if (WtczConst.GDZT_PROCESSING.equals(gdzt)) {
return WtczConst.XFJD_SECOND;
}
// 默认归入首访
return WtczConst.XFJD_FIRST;
}
/**
* 生成唯一的工单受理编号 (slbh)
* 规则: SL + 毫秒时间戳 + 4位随机数
*
* @return 唯一的工单受理编号
*/
public static String generateSlbh() {
long timestamp = System.currentTimeMillis();
int random = (int) ((Math.random() * 9 + 1) * 1000); // 1000 ~ 9999
return "SL" + timestamp + random;
}
/**
* 根据前端菜单传递的 tabType Mybatis-Plus QueryWrapper 统一织入对应的状态过滤条件
*
* @param wrapper Mybatis-Plus 实体查询 Wrapper 容器
* @param tabType 前端左侧三级菜单路由类型
*/
public static void buildTabTypeFilter(com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<net.lab1024.sa.admin.module.business.jwpy.domain.entity.WtczEntity> wrapper, String tabType) {
if (org.apache.commons.lang3.StringUtils.isBlank(tabType)) {
return;
}
switch (tabType) {
case "to_handle":
// 待办工单:包含暂存待首访(0)和办理中(1)
wrapper.in(net.lab1024.sa.admin.module.business.jwpy.domain.entity.WtczEntity::getGdzt, java.util.List.of(WtczConst.GDZT_STASH, WtczConst.GDZT_PROCESSING));
break;
case "transferred":
// 已流转工单:已经在流程办理核查中的工单(1)
wrapper.eq(net.lab1024.sa.admin.module.business.jwpy.domain.entity.WtczEntity::getGdzt, WtczConst.GDZT_PROCESSING);
break;
case "completed":
// 已办结工单:(2)
wrapper.eq(net.lab1024.sa.admin.module.business.jwpy.domain.entity.WtczEntity::getGdzt, WtczConst.GDZT_COMPLETED);
break;
case "about_to_expire":
// 即将超期 (未办结 且 创建时间在 2天前 到 3天前 之间)
String twoDaysAgo = LocalDateTime.now().minusDays(2).format(DATE_TIME_FORMATTER);
String threeDaysAgoFor临期 = LocalDateTime.now().minusDays(3).format(DATE_TIME_FORMATTER);
wrapper.ne(net.lab1024.sa.admin.module.business.jwpy.domain.entity.WtczEntity::getGdzt, WtczConst.GDZT_COMPLETED)
.between(net.lab1024.sa.admin.module.business.jwpy.domain.entity.WtczEntity::getCreateTime, threeDaysAgoFor临期, twoDaysAgo);
break;
case "expired":
// 已超期 (未办结 且 创建时间早于 3天前,即 72 小时前)
String threeDaysAgo = LocalDateTime.now().minusDays(3).format(DATE_TIME_FORMATTER);
wrapper.ne(net.lab1024.sa.admin.module.business.jwpy.domain.entity.WtczEntity::getGdzt, WtczConst.GDZT_COMPLETED)
.le(net.lab1024.sa.admin.module.business.jwpy.domain.entity.WtczEntity::getCreateTime, threeDaysAgo);
break;
case "invalid":
// 已作废 (占位状态,避免正常查出数据)
wrapper.eq(net.lab1024.sa.admin.module.business.jwpy.domain.entity.WtczEntity::getGdzt, "9");
break;
case "all":
default:
break;
}
}
}

115
smart-flow-web/src/api/business/jwpy/survey-sample-api.js

@ -0,0 +1,115 @@
/**
* 民意调查样本库与评议任务 api 封装
*
* @Author: Antigravity
* @Date: 2026-06-15
*/
import { postRequest, getRequest } from '/@/lib/axios';
export const surveySampleApi = {
/**
* 获取样本库分组列表
*/
queryGroupList: () => {
return getRequest('/mydc/sample/group/list');
},
/**
* 新增样本分组
*/
addGroup: (param) => {
return postRequest('/mydc/sample/group/add', param);
},
/**
* 删除样本分组及下属所有样本
*/
deleteGroup: (id) => {
return getRequest(`/mydc/sample/group/delete/${id}`);
},
/**
* 分页查询当前分组样本人员名单
*/
querySamplePage: (param) => {
return postRequest('/mydc/sample/queryPage', param);
},
/**
* 新增单条样本人员
*/
addSample: (param) => {
return postRequest('/mydc/sample/add', param);
},
/**
* 粘贴文本批量解析导入样本人员
*/
batchAddSamples: (param) => {
return postRequest('/mydc/sample/batchAdd', param);
},
/**
* 删除样本人员
*/
deleteSample: (id) => {
return getRequest(`/mydc/sample/delete/${id}`);
},
/**
* 分页查询测评任务列表
*/
queryTaskPage: (param) => {
return postRequest('/mydc/task/queryPage', param);
},
/**
* 获取测评任务详情
*/
getTaskDetail: (id) => {
return getRequest(`/mydc/task/detail/${id}`);
},
/**
* 新建调查评议任务并批量向分组推送
*/
createSurveyTask: (param) => {
return postRequest('/mydc/task/create', param);
},
/**
* H5填报端提交群众测评答案
*/
submitH5Answer: (param) => {
return postRequest('/mydc/task/submitAnswer', param);
},
/**
* 查询已配置的问卷模板列表
*/
querySurveys: (param) => {
return postRequest('/mydc/survey/queryPage', param);
},
/**
* 添加新问卷模板
*/
addSurvey: (param) => {
return postRequest('/mydc/survey/add', param);
},
/**
* 修改/保存问卷设计及状态
*/
updateSurvey: (param) => {
return postRequest('/mydc/survey/update', param);
},
/**
* 删除问卷模板
*/
deleteSurvey: (id) => {
return getRequest(`/mydc/survey/delete/${id}`);
}
};

7
smart-flow-web/src/api/business/jwpy/wtcz-api.js

@ -69,5 +69,12 @@ export const wtczApi = {
*/
getUnclaimedCount: () => {
return getRequest('/jwpy/wtcz/unclaimedCount');
},
/**
* 保存外呼通话录音流水
*/
addYy: (param) => {
return postRequest('/jwpy/wtcz/addYy', param);
}
};

6
smart-flow-web/src/router/flow/flow.js

@ -67,5 +67,11 @@ export const flowRouters = [
meta: { title: '网络民意监测' }
}
]
},
{
path: '/survey/fill',
component: () => import('/@/views/jwpy/mydc/my-survey/fill.vue'),
name: 'SurveyFillPublic',
meta: { title: '群众自助评议填报' }
}
]

2
smart-flow-web/src/router/index.js

@ -33,7 +33,7 @@ router.beforeEach(async (to, from, next) => {
nProgress.start();
// 公共页面,任何时候都可以跳转
if (to.path === PAGE_PATH_404) {
if (to.path === PAGE_PATH_404 || to.path === '/survey/fill') {
next();
return;
}

6
smart-flow-web/src/views/flow/definition/definition-form.vue

@ -57,8 +57,7 @@
:tree-data="categoryList"
:field-names="{ value: 'id', label: 'name', children: 'children' }"
placeholder="请选择流程类别"
tree-checkable
tree-check-strictly
allow-clear
style="width: 100%"
/>
</a-form-item>
@ -73,8 +72,7 @@
:tree-data="formPathList"
:field-names="{ value: 'id', label: 'name', children: 'children' }"
placeholder="请选择自定义表单"
tree-checkable
tree-check-strictly
allow-clear
style="width: 100%"
/>
</a-form-item>

47
smart-flow-web/src/views/jwpy/jwpy/flow/components/BizClassificationForm.vue

@ -138,6 +138,9 @@ const props = defineProps({
border: 1px solid #e2e8f0 !important;
border-radius: 0 !important;
}
:deep(.ant-descriptions-view table) {
table-layout: fixed !important;
}
:deep(.ant-descriptions-row) {
border-bottom: 1px solid #f1f5f9 !important;
@ -221,19 +224,49 @@ const props = defineProps({
border-color: #cbd5e1 !important;
}
/* 错误红字绝对定位浮动,防止撑开网格高度 */
/* 方案一:高级红色气泡悬浮提示效果(仅在悬停或聚焦时浮现) */
:deep(.ant-form-item-control) {
position: relative !important;
}
:deep(.ant-form-item-explain) {
position: absolute !important;
bottom: -4px !important;
bottom: 28px !important;
left: 10px !important;
z-index: 10 !important;
z-index: 1000 !important;
opacity: 0;
pointer-events: none;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
width: max-content !important; /* 宽度随字数自适应,不拉长 */
font-size: 11px !important;
color: #ef4444 !important;
line-height: 1 !important;
background: #ffffff !important;
padding: 0 4px !important;
color: #ffffff !important;
background: #f87171 !important; /* 柔和红 */
padding: 3px 8px !important;
border-radius: 4px !important;
box-shadow: 0 4px 10px rgba(239, 68, 68, 0.2) !important;
white-space: nowrap !important;
line-height: 1.4 !important;
}
:deep(.ant-form-item-explain-error) {
color: #ffffff !important; /* 强制覆盖 Antd 默认的红色字,防止红底红字看不清 */
}
/* 气泡小箭头 */
:deep(.ant-form-item-explain)::after {
content: '';
position: absolute;
bottom: -4px;
left: 15px;
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid #f87171;
}
/* 触发显示状态 */
:deep(.ant-form-item-has-error:hover .ant-form-item-explain),
:deep(.ant-form-item-has-error:focus-within .ant-form-item-explain) {
opacity: 1;
bottom: 34px !important;
pointer-events: auto;
}
</style>

47
smart-flow-web/src/views/jwpy/jwpy/flow/components/CallerInfoForm.vue

@ -118,6 +118,9 @@ const props = defineProps({
border: 1px solid #e2e8f0 !important;
border-radius: 0 !important;
}
:deep(.ant-descriptions-view table) {
table-layout: fixed !important;
}
:deep(.ant-descriptions-row) {
border-bottom: 1px solid #f1f5f9 !important;
@ -178,19 +181,49 @@ const props = defineProps({
border-color: #cbd5e1 !important;
}
/* 错误红字绝对定位浮动,防止撑开网格高度 */
/* 方案一:高级红色气泡悬浮提示效果(仅在悬停或聚焦时浮现) */
:deep(.ant-form-item-control) {
position: relative !important;
}
:deep(.ant-form-item-explain) {
position: absolute !important;
bottom: -4px !important;
bottom: 28px !important;
left: 10px !important;
z-index: 10 !important;
z-index: 1000 !important;
opacity: 0;
pointer-events: none;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
width: max-content !important; /* 宽度随字数自适应,不拉长 */
font-size: 11px !important;
color: #ef4444 !important;
line-height: 1 !important;
background: #ffffff !important;
padding: 0 4px !important;
color: #ffffff !important;
background: #f87171 !important; /* 柔和红 */
padding: 3px 8px !important;
border-radius: 4px !important;
box-shadow: 0 4px 10px rgba(239, 68, 68, 0.2) !important;
white-space: nowrap !important;
line-height: 1.4 !important;
}
:deep(.ant-form-item-explain-error) {
color: #ffffff !important; /* 强制覆盖 Antd 默认的红色字,防止红底红字看不清 */
}
/* 气泡小箭头 */
:deep(.ant-form-item-explain)::after {
content: '';
position: absolute;
bottom: -4px;
left: 15px;
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid #f87171;
}
/* 触发显示状态 */
:deep(.ant-form-item-has-error:hover .ant-form-item-explain),
:deep(.ant-form-item-has-error:focus-within .ant-form-item-explain) {
opacity: 1;
bottom: 34px !important;
pointer-events: auto;
}
</style>

73
smart-flow-web/src/views/jwpy/jwpy/flow/components/InvolvedTargetForm.vue

@ -13,16 +13,19 @@
>
<a-descriptions-item>
<template #label>
<span class="required-star">*</span>涉及警种
<span class="required-star" v-if="!readonly">*</span>涉及警种
</template>
<a-form-item name="sjjzNo">
<span v-if="readonly">
{{ formData.sjjzMc || '-' }}
</span>
<a-form-item name="sjjzNo" v-else>
<DictSelect :dict-code="DICT_CODE_ENUM.SYSTEM_JZ" placeholder="请选择涉及警种" v-model:value="formData.sjjzNo" />
</a-form-item>
</a-descriptions-item>
<a-descriptions-item :span="2">
<template #label>
<span class="required-star">*</span>被反映单位
<span class="required-star" v-if="!readonly">*</span>被反映单位
</template>
<a-form-item name="sjdwCode">
<a-cascader
@ -31,12 +34,16 @@
placeholder="请选择区县分局与具体所队"
style="width: 100%"
expand-trigger="hover"
:disabled="readonly"
/>
</a-form-item>
</a-descriptions-item>
<a-descriptions-item label="被反映人身份">
<a-form-item name="sfsjfj">
<span v-if="readonly">
{{ formData.sfsjfj == 2 ? '辅警' : (formData.sfsjfj == 1 ? '民警' : '-') }}
</span>
<a-form-item name="sfsjfj" v-else>
<a-select v-model:value="formData.sfsjfj" placeholder="警员身份" style="width: 100%">
<a-select-option value="1">民警</a-select-option>
<a-select-option value="2">辅警</a-select-option>
@ -45,13 +52,15 @@
</a-descriptions-item>
<a-descriptions-item label="被反映人姓名">
<a-form-item name="sjfjxm">
<span v-if="readonly">{{ formData.sjfjxm || '-' }}</span>
<a-form-item name="sjfjxm" v-else>
<a-input v-model:value="formData.sjfjxm" placeholder="核实后的具体反映人" />
</a-form-item>
</a-descriptions-item>
<a-descriptions-item label="被反映人警号">
<a-form-item name="sjfjjh">
<span v-if="readonly">{{ formData.sjfjjh || '-' }}</span>
<a-form-item name="sjfjjh" v-else>
<a-input v-model:value="formData.sjfjjh" placeholder="警号/工号" />
</a-form-item>
</a-descriptions-item>
@ -63,6 +72,7 @@
<script setup>
import { defineProps } from 'vue';
import DictSelect from '/@/components/support/dict-select/index.vue';
import DictLabel from '/@/components/support/dict-label/index.vue';
import { DICT_CODE_ENUM } from '/@/constants/support/dict-const.js';
const props = defineProps({
@ -73,6 +83,10 @@ const props = defineProps({
departmentOptions: {
type: Array,
default: () => []
},
readonly: {
type: Boolean,
default: false
}
});
</script>
@ -115,6 +129,9 @@ const props = defineProps({
border: 1px solid #e2e8f0 !important;
border-radius: 0 !important;
}
:deep(.ant-descriptions-view table) {
table-layout: fixed !important;
}
:deep(.ant-descriptions-row) {
border-bottom: 1px solid #f1f5f9 !important;
@ -175,19 +192,49 @@ const props = defineProps({
border-color: #cbd5e1 !important;
}
/* 错误红字绝对定位浮动,防止撑开网格高度 */
/* 方案一:高级红色气泡悬浮提示效果(仅在悬停或聚焦时浮现) */
:deep(.ant-form-item-control) {
position: relative !important;
}
:deep(.ant-form-item-explain) {
position: absolute !important;
bottom: -4px !important;
bottom: 28px !important;
left: 10px !important;
z-index: 10 !important;
z-index: 1000 !important;
opacity: 0;
pointer-events: none;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
width: max-content !important; /* 宽度随字数自适应,不拉长 */
font-size: 11px !important;
color: #ef4444 !important;
line-height: 1 !important;
background: #ffffff !important;
padding: 0 4px !important;
color: #ffffff !important;
background: #f87171 !important; /* 柔和红 */
padding: 3px 8px !important;
border-radius: 4px !important;
box-shadow: 0 4px 10px rgba(239, 68, 68, 0.2) !important;
white-space: nowrap !important;
line-height: 1.4 !important;
}
:deep(.ant-form-item-explain-error) {
color: #ffffff !important; /* 强制覆盖 Antd 默认的红色字,防止红底红字看不清 */
}
/* 气泡小箭头 */
:deep(.ant-form-item-explain)::after {
content: '';
position: absolute;
bottom: -4px;
left: 15px;
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid #f87171;
}
/* 触发显示状态 */
:deep(.ant-form-item-has-error:hover .ant-form-item-explain),
:deep(.ant-form-item-has-error:focus-within .ant-form-item-explain) {
opacity: 1;
bottom: 34px !important;
pointer-events: auto;
}
</style>

194
smart-flow-web/src/views/jwpy/jwpy/flow/index.vue

@ -21,8 +21,8 @@
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" size="small" @click="onSearch">查询</a-button>
<a-button size="small" @click="resetQuery">重置</a-button>
<a-button type="primary" class="action-btn" @click="onSearch">查询</a-button>
<a-button class="action-btn" @click="resetQuery">重置</a-button>
</a-space>
</a-form-item>
</a-form>
@ -30,14 +30,14 @@
<!-- 表格顶部操作按钮 -->
<div class="table-operate-row" style="margin-bottom: 12px">
<a-space>
<a-button type="primary" size="small" @click="showForm()">
<a-button type="primary" class="action-btn" @click="showForm()">
<template #icon><PlusOutlined /></template>
新建工单
</a-button>
<a-button
type="primary"
danger
size="small"
class="action-btn"
:disabled="selectedRowKeyList.length === 0"
@click="confirmBatchDelete"
>
@ -56,30 +56,35 @@
:loading="tableLoading"
:pagination="false"
:row-selection="{ selectedRowKeys: selectedRowKeyList, onChange: onSelectChange }"
:scroll="{ x: 1500 }"
class="custom-table"
>
<template #bodyCell="{ text, record, column }">
<!-- 业务类型 Tag 展示 -->
<template v-if="column.dataIndex === 'sjlyNo'">
<a-tag :color="getBizTypeColor(text)">
{{ getBizTypeLabel(text) }}
{{ record.sjlyNoName || text }}
</a-tag>
</template>
<!-- 满意度标签展示 -->
<template v-if="column.dataIndex === 'qzmyqk'">
<DictLabel :dict-code="DICT_CODE_ENUM.CALLBACK_SATISFACTION" :data-value="text" />
<span>{{ record.qzmyqkName || '-' }}</span>
</template>
<!-- 处理状态标签 -->
<template v-if="column.dataIndex === 'gdzt'">
<a-tag :color="getStatusMeta(text).color">
{{ getStatusMeta(text).label }}
{{ record.gdztName || getStatusMeta(text).label }}
</a-tag>
</template>
<template v-if="column.dataIndex === 'workflowNode'">
<div class="workflow-node-cell">
<a-tag :color="getWorkflowNode(record).color">{{ getWorkflowNode(record).label }}</a-tag>
<span>{{ getWorkflowNode(record).hint }}</span>
</div>
<!-- 当前环节 -->
<template v-if="column.dataIndex === 'nodeName'">
<a-tag :color="getCurrentNode(record).color" style="margin: 0;">
{{ getCurrentNode(record).label }}
</a-tag>
</template>
<!-- 当前处理人 -->
<template v-if="column.dataIndex === 'processor'">
<span>{{ getCurrentProcessor(record) }}</span>
</template>
<!-- 操作列 -->
<template v-if="column.dataIndex === 'action'">
@ -186,21 +191,22 @@ const currentBizTypeName = computed(() => {
// ---------------------------- () ----------------------------
const columns = computed(() => {
const list = [];
list.push({ title: '工单受理编号', dataIndex: 'slbh', ellipsis: true });
list.push({ title: '工单受理编号', dataIndex: 'slbh', width: 220, ellipsis: true });
//
if (isWorkbench.value) {
list.push({ title: '业务类型', dataIndex: 'sjlyNo', width: 130, align: 'center' });
list.push({ title: '业务类型', dataIndex: 'sjlyNo', width: 140, align: 'center' });
}
list.push(
{ title: '诉求人姓名', dataIndex: 'sqrXm', width: 100, ellipsis: true },
{ title: '联系电话', dataIndex: 'lxdh', width: 130 },
{ title: '涉及单位', dataIndex: 'sjdwMc', ellipsis: true },
{ title: '涉及民警', dataIndex: 'sjmjMc', width: 100 },
{ title: '群众满意度', dataIndex: 'qzmyqk', width: 100, align: 'center' },
{ title: '工单状态', dataIndex: 'gdzt', width: 100, align: 'center' },
{ title: '当前处理人', dataIndex: 'workflowNode', width: 180 },
{ title: '诉求时间', dataIndex: 'sqsj', width: 150 },
{ title: '操作', dataIndex: 'action', width: 100, align: 'center', fixed: 'right' }
{ title: '诉求人姓名', dataIndex: 'sqrXm', width: 110, ellipsis: true },
{ title: '联系电话', dataIndex: 'lxdh', width: 140 },
{ title: '涉及单位', dataIndex: 'sjdwMc', width: 240, ellipsis: true },
{ title: '涉及民警', dataIndex: 'sjmjMc', width: 110 },
{ title: '群众满意度', dataIndex: 'qzmyqk', width: 110, align: 'center' },
{ title: '工单状态', dataIndex: 'gdzt', width: 110, align: 'center' },
{ title: '当前环节', dataIndex: 'nodeName', width: 120, align: 'center' },
{ title: '当前处理人', dataIndex: 'processor', width: 160 },
{ title: '诉求时间', dataIndex: 'sqsj', width: 180, align: 'center' },
{ title: '操作', dataIndex: 'action', width: 110, align: 'center', fixed: 'right' }
);
return list;
});
@ -209,92 +215,73 @@ function getStatusMeta(status) {
const map = {
'0': { label: '待首访', color: 'blue' },
'1': { label: '办理中', color: 'orange' },
'2': { label: '已办结', color: 'green' }
'2': { label: '已办结', color: 'default' }
};
return map[String(status)] || { label: '待生成', color: 'default' };
}
function getWorkflowNode(record) {
if (record.nodeName) {
let handlerName = '民意感知中心';
let nodeName = record.nodeName;
// ""
if (nodeName === '中间节点' || nodeName === '任务节点') {
if (String(record.gdzt) === '1') {
nodeName = '所队核查';
} else if (String(record.gdzt) === '0') {
nodeName = '首访派发';
} else {
nodeName = '流程流转';
}
}
if (nodeName.includes('核查') || nodeName.includes('反馈') || nodeName.includes('所队') || nodeName.includes('承办')) {
handlerName = record.sjdwMc || '责任单位';
} else if (nodeName.includes('复核') || nodeName.includes('审核') || nodeName.includes('二次') || nodeName.includes('回访')) {
handlerName = '民意中心(复核岗)';
} else if (nodeName.includes('派发') || nodeName.includes('分派') || nodeName.includes('首访')) {
handlerName = '民意中心(分派岗)';
}
return {
label: nodeName,
color: record.flowStatus === '8' ? 'green' : 'geekblue',
hint: `处理中: ${handlerName}`,
};
}
function getCurrentNode(record) {
if (String(record.gdzt) === '2') {
if (record.cljg?.includes('恶意投诉')) {
return { label: '恶意归档', color: 'red', hint: '不纳入回访' };
}
if (record.cljg?.includes('已依法尽职')) {
return { label: '申诉归档', color: 'cyan', hint: '不计有责' };
return { label: '已归档', color: 'default' };
}
if (!record.nodeName) {
return { label: '人工首访', color: 'blue' };
}
let nodeName = record.nodeName;
if (nodeName === '中间节点' || nodeName === '任务节点') {
if (String(record.gdzt) === '1') {
nodeName = '所队核查';
} else if (String(record.gdzt) === '0') {
nodeName = '首访派发';
} else {
nodeName = '流程流转';
}
return { label: '办结归档', color: 'green', hint: record.sfjg === '1' ? '首访闭环' : '复核闭环' };
}
if (String(record.sfyyq) === '1') {
return { label: '延期审核', color: 'purple', hint: '待分局/中心审核' };
let color = 'blue';
if (nodeName.includes('核查') || nodeName.includes('办理')) {
color = 'orange';
} else if (nodeName.includes('归档') || nodeName.includes('结束')) {
color = 'default';
}
if (record.cljg?.includes('单位核查反馈')) {
return { label: '二次回访', color: 'geekblue', hint: '待中心回访' };
return { label: nodeName, color };
}
function getCurrentProcessor(record) {
if (String(record.gdzt) === '2') {
return '-';
}
if (String(record.gdzt) === '1') {
return { label: '核查整改', color: 'orange', hint: '责任单位办理' };
if (!record.nodeName) {
if (record.firstFeedbackOpinion?.includes('暂存')) {
return '暂存待回拨';
}
return '民意中心(分派岗)';
}
if (record.firstFeedbackOpinion?.includes('暂存')) {
return { label: '暂存', color: 'default', hint: '待群众回拨' };
let nodeName = record.nodeName;
let handlerName = '民意感知中心';
if (nodeName.includes('核查') || nodeName.includes('反馈') || nodeName.includes('所队') || nodeName.includes('承办')) {
handlerName = record.sjdwMc || '责任单位';
} else if (nodeName.includes('复核') || nodeName.includes('审核') || nodeName.includes('二次') || nodeName.includes('回访')) {
handlerName = '民意中心(复核岗)';
} else if (nodeName.includes('派发') || nodeName.includes('分派') || nodeName.includes('首访')) {
handlerName = '民意中心(分派岗)';
}
return { label: '人工首访', color: 'blue', hint: '待电话核实' };
return handlerName;
}
// ---------------------------- ----------------------------
function getBizTypeLabel(sjlyNo) {
const map = {
'110': '110接处警',
'hz': '户政窗口',
'crj': '出入境',
'cjg': '车驾管',
'ajbl': '案件办理',
'xzsp': '行政审批评议',
'xf': '信访评议',
'wq': '民辅警维权',
'za': '治安管理评议',
'js': '监所管理评议',
'xl': '巡逻防控评议',
'sqfw': '涉企服务评议'
};
return map[sjlyNo] || sjlyNo || '通用业务';
if (!sjlyNo) return '通用业务';
const label = useDictStore().getDataLabels(DICT_CODE_ENUM.BUSINESS_CLASSIFICATION, sjlyNo);
return label || sjlyNo;
}
function getBizTypeColor(sjlyNo) {
const map = {
'110': 'red',
'hz': 'blue',
'crj': 'purple',
'cjg': 'green',
'ajbl': 'orange'
};
return map[sjlyNo] || 'cyan';
if (!sjlyNo) return 'default';
const key = sjlyNo.toLowerCase();
if (key.includes('110') || key.includes('01')) return 'blue';
return 'default';
}
// ---------------------------- ----------------------------
@ -592,6 +579,31 @@ async function requestBatchDelete() {
padding: 0 4px;
font-size: 12px;
}
.action-btn {
height: 32px;
padding: 0 16px;
border-radius: 6px;
font-weight: 500;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
}
.action-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.08);
}
.action-btn:active {
transform: translateY(0);
}
.custom-table :deep(.ant-table-cell) {
white-space: nowrap;
}
</style>
<style>

66
smart-flow-web/src/views/jwpy/jwpy/flow/utils/jwpy-util.js

@ -0,0 +1,66 @@
// 级联静态数据(被反映单位字典选项)
export const departmentOptions = [
{
value: 'hefei_gx',
label: '合肥市公安局高新分局',
children: [
{ value: 'gx_hfpcs', label: '蜀麓派出所' },
{ value: 'gx_gxdd', label: '高新交警大队' }
]
},
{
value: 'hefei_bh',
label: '合肥市公安局包河分局',
children: [
{ value: 'bh_wnpcs', label: '万年埠派出所' },
{ value: 'bh_bhyzd', label: '交警支队包河大队一中队' }
]
}
];
/**
* 查找级联部门路径的辅助函数
* @param {string} code - 被反映单位最后一级编码
* @param {Array} options - 部门级联静态字典数据
* @returns {Array} 级联路径数组
*/
export function findDeptPath(code, options = departmentOptions) {
if (!code) return [];
for (const parent of options) {
if (parent.children) {
const child = parent.children.find(c => c.value === code);
if (child) {
return [parent.value, child.value];
}
}
}
return [code];
}
/**
* 统一将表单或详情中的字典下拉字段的值转化为 String 字符串避免 Ant Design 组件因强类型不匹配 Number 1 无法匹配 String "1"回显为原始数字的问题
* @param {Object} data - 工单数据对象
*/
export function formatDictFields(data) {
if (!data) return;
const dictFields = [
'sfnm',
'sqrXb',
'sqfl',
'sjlyNo',
'sfsq',
'sfyyq',
'sjjzNo',
'ssqkNo',
'wzqkNo',
'secondSatisfaction',
'gdbq',
'sfsjfj',
'sqdj' // 诉求等级
];
Object.keys(data).forEach(key => {
if (dictFields.includes(key) && data[key] !== undefined && data[key] !== null) {
data[key] = String(data[key]);
}
});
}

140
smart-flow-web/src/views/jwpy/jwpy/flow/wtcz-detail.vue

@ -39,24 +39,35 @@
>
<a-descriptions-item>
<template #label>
<span class="required-star">*</span>诉求业务分类
<span class="required-star" v-if="nodeCode">*</span>诉求业务分类
</template>
<a-form-item name="sqfl">
<span v-if="!nodeCode">
<DictLabel :dict-code="DICT_CODE_ENUM.CZZX_SQFL" :data-value="form.sqfl" />
</span>
<a-form-item name="sqfl" v-else>
<DictSelect :dict-code="DICT_CODE_ENUM.CZZX_SQFL" placeholder="请选择诉求分类" v-model:value="form.sqfl" />
</a-form-item>
</a-descriptions-item>
<a-descriptions-item>
<template #label>
<span class="required-star">*</span>涉及警务业务
<span class="required-star" v-if="nodeCode">*</span>涉及警务业务
</template>
<a-form-item name="sjlyNo">
<span v-if="!nodeCode">
<DictLabel :dict-code="DICT_CODE_ENUM.BUSINESS_CLASSIFICATION" :data-value="form.sjlyNo" />
</span>
<a-form-item name="sjlyNo" v-else>
<DictSelect :dict-code="DICT_CODE_ENUM.BUSINESS_CLASSIFICATION" placeholder="请选择业务分类" v-model:value="form.sjlyNo" />
</a-form-item>
</a-descriptions-item>
<a-descriptions-item label="诉求等级">
<a-form-item name="sqdj">
<span v-if="!nodeCode">
<a-tag :color="form.sqdj == 2 || form.sqdj === '2' ? 'red' : 'blue'">
{{ form.sqdj == 2 || form.sqdj === '2' ? '紧急' : '一般' }}
</a-tag>
</span>
<a-form-item name="sqdj" v-else>
<a-select v-model:value="form.sqdj" placeholder="选择等级" style="width: 100%">
<a-select-option value="1">一般</a-select-option>
<a-select-option value="2">紧急</a-select-option>
@ -65,7 +76,8 @@
</a-descriptions-item>
<a-descriptions-item label="工单四级标签">
<a-form-item name="gdbq">
<span v-if="!nodeCode">{{ form.gdbq || '-' }}</span>
<a-form-item name="gdbq" v-else>
<div style="display: flex; gap: 8px;">
<a-button type="dashed" size="middle">选择标签</a-button>
<a-input v-model:value="form.gdbq" placeholder="例如: 业务-交管-违法" style="flex: 1;" />
@ -74,21 +86,26 @@
</a-descriptions-item>
<a-descriptions-item label="是否涉企">
<a-form-item name="sfsq">
<span v-if="!nodeCode">
<DictLabel :dict-code="DICT_CODE_ENUM.IS_ENTERPRISE_RELATED" :data-value="form.sfsq" />
</span>
<a-form-item name="sfsq" v-else>
<DictSelect :dict-code="DICT_CODE_ENUM.IS_ENTERPRISE_RELATED" placeholder="选择是否涉企" v-model:value="form.sfsq" />
</a-form-item>
</a-descriptions-item>
<template v-if="form.sfsq == 1 || form.sfsq === '1'">
<a-descriptions-item label="企业名称">
<a-form-item name="qymc">
<span v-if="!nodeCode">{{ form.qymc || '-' }}</span>
<a-form-item name="qymc" v-else>
<a-input v-model:value="form.qymc" placeholder="输入企业名称" />
</a-form-item>
</a-descriptions-item>
</template>
<template v-else>
<a-descriptions-item label="是否已延期">
<a-form-item name="sfyyq">
<span v-if="!nodeCode">{{ form.sfyyq == 1 || form.sfyyq === '1' ? '' : '' }}</span>
<a-form-item name="sfyyq" v-else>
<a-select v-model:value="form.sfyyq" style="width: 100%">
<a-select-option value="0"></a-select-option>
<a-select-option value="1"></a-select-option>
@ -119,7 +136,7 @@
</div>
<!-- 模块3涉事单位与人员补充 -->
<InvolvedTargetForm :formData="form" :departmentOptions="departmentOptions" />
<InvolvedTargetForm :formData="form" :departmentOptions="departmentOptions" :readonly="!nodeCode" />
<!-- 模块4电话回访与沟通记录 -->
<div class="form-module-wrapper">
@ -155,13 +172,13 @@
<div>
<div class="field-title"><EditOutlined style="margin-right: 4px;" />首访沟通记录</div>
<a-form-item name="firstFeedbackOpinion">
<a-textarea v-model:value="form.firstFeedbackOpinion" :rows="3" placeholder="填写与群众的沟通情况及诉求点..." />
<a-textarea v-model:value="form.firstFeedbackOpinion" :rows="6" placeholder="请详细填写与群众的首访沟通情况、反映的核心诉求、解释答复细节以及群众的反馈态度等..." show-count :maxlength="500" />
</a-form-item>
</div>
<!-- 电话回访录音列表 -->
<div>
<div class="field-title"><PhoneOutlined style="margin-right: 4px;" />电话回访录音列表</div>
<div class="field-title"><PhoneOutlined style="margin-right: 4px;" />首访电话录音列表</div>
<a-table :data-source="callHistory" :columns="callColumns" size="small" bordered :pagination="false" :rowKey="record => record.id">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
@ -175,7 +192,38 @@
<div class="player-bar-bg">
<div class="player-bar-progress"></div>
</div>
<span class="player-time">04:21</span>
<span class="player-time">{{ record.duration }}</span>
</div>
<span v-else class="text-muted">-</span>
</template>
<template v-else-if="column.key === 'action'">
<div v-if="record.status === '已接听'" class="table-actions">
<a-button type="link" size="small"><PlayCircleOutlined /> 试听</a-button>
<a-button type="link" size="small"><DownloadOutlined /> 下载</a-button>
</div>
<span v-else class="text-muted">-</span>
</template>
</template>
</a-table>
</div>
<!-- 二访电话回访录音列表 -->
<div v-if="secondCallHistory && secondCallHistory.length > 0">
<div class="field-title" style="margin-top: 16px;"><PhoneOutlined style="margin-right: 4px;" />二次回访电话录音列表</div>
<a-table :data-source="secondCallHistory" :columns="callColumns" size="small" bordered :pagination="false" :rowKey="record => record.id">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<span :style="{ color: record.status === '已接听' ? '#2563eb' : '#94a3b8' }">
{{ record.status === '已接听' ? '外呼已接听' : '外呼未接听' }}
</span>
</template>
<template v-else-if="column.key === 'recording'">
<div v-if="record.status === '已接听'" class="player-wrapper">
<span class="player-time">00:00</span>
<div class="player-bar-bg">
<div class="player-bar-progress"></div>
</div>
<span class="player-time">{{ record.duration }}</span>
</div>
<span v-else class="text-muted">-</span>
</template>
@ -387,7 +435,9 @@ import { message } from 'ant-design-vue';
import _ from 'lodash';
import { wtczApi } from '/@/api/business/jwpy/wtcz-api';
import { DICT_CODE_ENUM } from '/@/constants/support/dict-const.js';
import { formatDictFields } from './utils/jwpy-util';
import DictSelect from '/@/components/support/dict-select/index.vue';
import DictLabel from '/@/components/support/dict-label/index.vue';
import FileUpload from '/@/components/support/file-upload/index.vue';
import CallerInfoForm from './components/CallerInfoForm.vue';
import InvolvedTargetForm from './components/InvolvedTargetForm.vue';
@ -471,6 +521,7 @@ const formDefault = {
sfyyq: undefined,
wtms: undefined,
sjjzMc: undefined,
sjjzNo: undefined,
sjdwCode: undefined,
sfsjfj: undefined,
sjfjxm: undefined,
@ -524,19 +575,43 @@ const rules = computed(() => {
};
});
//
const callHistory = ref([
{ id: 1, seat: '虚拟坐席01', time: '2026-06-11 17:20:23', duration: '4分21秒', status: '已接听' },
{ id: 2, seat: '虚拟坐席01', time: '2026-06-11 17:18:17', duration: '0分00秒', status: '未接听' }
]);
function formatDuration(seconds) {
if (!seconds) return '0秒';
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return mins > 0 ? `${mins}${secs}` : `${secs}`;
}
//
const callHistory = computed(() => {
return (form.czzxWtczYyList || []).map(item => ({
id: item.id,
seat: item.thdwMc || '系统评警坐席',
time: item.thsj || item.createTime || '-',
duration: formatDuration(item.thsc),
status: item.thsc > 0 ? '已接听' : '未接听',
recording: item.lydz
}));
});
const secondCallHistory = computed(() => {
return (form.secondCallList || []).map(item => ({
id: item.id,
seat: item.thdwMc || '系统评警坐席',
time: item.thsj || item.createTime || '-',
duration: formatDuration(item.thsc),
status: item.thsc > 0 ? '已接听' : '未接听',
recording: item.lydz
}));
});
const callColumns = [
{ title: '语音回访坐席', dataIndex: 'seat', key: 'seat', width: 120 },
{ title: '回访时间', dataIndex: 'time', key: 'time', width: 180 },
{ title: '通话时长', dataIndex: 'duration', key: 'duration', width: 100 },
{ title: '电话状态', dataIndex: 'status', key: 'status', width: 120 },
{ title: '通话录音', key: 'recording', minWidth: 200 },
{ title: '操作', key: 'action', width: 140, align: 'right' }
{ title: '访评员', dataIndex: 'seat', key: 'seat', minWidth: 160, align: 'center' },
{ title: '回访时间', dataIndex: 'time', key: 'time', minWidth: 200, align: 'center' },
{ title: '通话时长', dataIndex: 'duration', key: 'duration', minWidth: 120, align: 'center' },
{ title: '电话状态', dataIndex: 'status', key: 'status', minWidth: 140, align: 'center' },
{ title: '通话录音', key: 'recording', minWidth: 320 },
{ title: '操作', key: 'action', width: 160, align: 'center' }
];
//
@ -676,15 +751,7 @@ async function loadDetail(id) {
}
}
function formatDictFields(data) {
const dictFields = ['sfnm', 'sqrXb', 'sqfl', 'sjlyNo', 'sfsq', 'sfyyq',
'sjjzNo', 'ssqkNo', 'wzqkNo', 'secondSatisfaction', 'gdbq', 'sfsjfj'];
Object.keys(data).forEach(key => {
if (dictFields.includes(key) && data[key] !== undefined && data[key] !== null) {
data[key] = String(data[key]);
}
});
}
//
async function loadTimelineLogs(instanceId) {
@ -1073,7 +1140,7 @@ defineExpose({
}
.table-actions {
display: flex;
justify-content: flex-end;
justify-content: center;
gap: 8px;
}
.text-muted {
@ -1085,6 +1152,9 @@ defineExpose({
border: 1px solid #e2e8f0 !important;
border-radius: 0 !important;
}
:deep(.ant-descriptions-view table) {
table-layout: fixed !important;
}
:deep(.ant-descriptions-row) {
border-bottom: 1px solid #f1f5f9 !important;
@ -1119,7 +1189,7 @@ defineExpose({
margin-bottom: 0 !important;
}
:deep(.ant-input),
:deep(input.ant-input),
:deep(.ant-select-selector),
:deep(.ant-cascader-picker) {
border-radius: 2px !important;

58
smart-flow-web/src/views/jwpy/jwpy/flow/wtcz-form.vue

@ -53,20 +53,31 @@ import { smartSentry } from '/@/lib/smart-sentry';
import CallerInfoForm from './components/CallerInfoForm.vue';
import BizClassificationForm from './components/BizClassificationForm.vue';
import InvolvedTargetForm from './components/InvolvedTargetForm.vue';
import { departmentOptions, findDeptPath, formatDictFields } from './utils/jwpy-util';
const emits = defineEmits(['reloadList']);
// ------------------------ ------------------------
const visibleFlag = ref(false);
function show(rowData) {
Object.assign(form, formDefault);
if (rowData && !_.isEmpty(rowData)) {
Object.assign(form, rowData);
//
const copyData = _.cloneDeep(rowData);
// String Cascader
if (copyData.sjdwCode && typeof copyData.sjdwCode === 'string') {
copyData.sjdwCode = findDeptPath(copyData.sjdwCode, departmentOptions);
}
Object.assign(form, copyData);
}
//
formatDictFields(form);
visibleFlag.value = true;
nextTick(() => {
formRef.value.clearValidate();
formRef.value?.clearValidate();
});
}
@ -75,28 +86,11 @@ function onClose() {
visibleFlag.value = false;
}
// ------------------------ ------------------------
const formRef = ref();
//
const departmentOptions = [
{
value: 'hefei_gx',
label: '合肥市公安局高新分局',
children: [
{ value: 'gx_hfpcs', label: '蜀麓派出所' },
{ value: 'gx_gxdd', label: '高新交警大队' }
]
},
{
value: 'hefei_bh',
label: '合肥市公安局包河分局',
children: [
{ value: 'bh_wnpcs', label: '万年埠派出所' },
{ value: 'bh_bhyzd', label: '交警支队包河大队一中队' }
]
}
];
const formDefault = {
id: undefined,
@ -104,7 +98,7 @@ const formDefault = {
sqrXm: undefined,
sqrXb: undefined,
lxdh: undefined,
sfnm: 0,
sfnm: '0',
sqrZjhm: undefined,
sqrXzdxz: undefined,
sqrHjdxz: undefined,
@ -112,7 +106,7 @@ const formDefault = {
sjlyNo: undefined,
sqdj: undefined,
gdbq: undefined,
sfsq: 0,
sfsq: '0',
qymc: undefined,
sfyyq: undefined,
wtms: undefined,
@ -129,7 +123,7 @@ let form = reactive({ ...formDefault });
watch(
() => form.sfnm,
(newVal) => {
if (newVal === 1) {
if (newVal == 1 || newVal === '1') {
form.sqrXm = '匿名';
form.sqrXb = undefined;
form.lxdh = undefined;
@ -150,8 +144,8 @@ watch(
const rules = computed(() => {
return {
sqrXm: form.sfnm === 1 ? [] : [{ required: true, message: '请输入诉求人姓名', trigger: 'blur' }],
lxdh: form.sfnm === 1 ? [] : [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
sqrXm: (form.sfnm == 1 || form.sfnm === '1') ? [] : [{ required: true, message: '请输入诉求人姓名', trigger: 'blur' }],
lxdh: (form.sfnm == 1 || form.sfnm === '1') ? [] : [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
sqfl: [{ required: true, message: '请选择诉求业务分类', trigger: 'change' }],
sjlyNo: [{ required: true, message: '请选择涉及警务业务', trigger: 'change' }],
wtms: [{ required: true, message: '请输入详细内容描述', trigger: 'blur' }],
@ -167,13 +161,21 @@ function onSubmit(actionType) {
.then(async () => {
SmartLoading.show();
try {
// sjdwCode String
const submitData = _.cloneDeep(form);
if (Array.isArray(submitData.sjdwCode) && submitData.sjdwCode.length > 0) {
submitData.sjdwCode = submitData.sjdwCode[submitData.sjdwCode.length - 1];
} else if (!submitData.sjdwCode) {
submitData.sjdwCode = '';
}
let wtczId = form.id;
// 1. /
if (wtczId) {
await wtczApi.update(form);
await wtczApi.update(submitData);
} else {
const res = await wtczApi.add(form);
const res = await wtczApi.add(submitData);
wtczId = res.data; // addID
}

277
smart-flow-web/src/views/jwpy/mydc/my-survey/components/SurveyFormRenderer.vue

@ -0,0 +1,277 @@
<!--
* 警务评议与问题处置 - 通用动态问卷渲染器组件
*
* @Author: Antigravity
* @Date: 2026-06-14
-->
<template>
<div class="survey-renderer">
<div v-for="(q, idx) in questions" :key="q.id" class="q-card-item" :class="{ 'is-readonly': readOnly }">
<!-- 题目头部 -->
<div class="q-title-row">
<span class="q-req-star" v-if="q.required && !readOnly">*</span>
<span class="q-seq-num">Q{{ idx + 1 }}.</span>
<span class="q-title-text">{{ q.title || '(未命名题目)' }}</span>
</div>
<!-- 题目答案渲染区 -->
<div class="q-answer-body">
<!-- 1. 星级评分题 -->
<div v-if="q.type === 'rate'" class="q-body-rate">
<a-rate v-model:value="localAnswers[q.id]" :disabled="readOnly" class="renderer-rate" />
<span class="rate-label-text" v-if="localAnswers[q.id]">
{{ localAnswers[q.id] }}
</span>
</div>
<!-- 2. 单选题 -->
<div v-else-if="q.type === 'radio'" class="q-body-radio">
<!-- 编辑模式 -->
<a-radio-group v-if="!readOnly" v-model:value="localAnswers[q.id]" class="renderer-radio-group">
<a-radio v-for="opt in q.options" :key="opt.label" :value="opt.label" class="renderer-radio-item">
<span class="opt-label-text">{{ opt.label }}</span>
<span class="opt-score-badge" v-if="opt.score !== undefined && opt.score !== null">
(分值: {{ opt.score }})
</span>
</a-radio>
</a-radio-group>
<!-- 只读模式 -->
<div v-else class="readonly-value">
<a-tag color="blue" class="val-tag">{{ localAnswers[q.id] || '未填写' }}</a-tag>
</div>
</div>
<!-- 3. 多选题 -->
<div v-else-if="q.type === 'checkbox'" class="q-body-checkbox">
<!-- 编辑模式 -->
<a-checkbox-group v-if="!readOnly" v-model:value="localAnswers[q.id]" class="renderer-checkbox-group">
<a-checkbox v-for="opt in q.options" :key="opt.label" :value="opt.label" class="renderer-checkbox-item">
<span class="opt-label-text">{{ opt.label }}</span>
</a-checkbox>
</a-checkbox-group>
<!-- 只读模式 -->
<div v-else class="readonly-value">
<template v-if="localAnswers[q.id] && localAnswers[q.id].length > 0">
<a-tag v-for="tag in localAnswers[q.id]" :key="tag" color="blue" class="val-tag">
{{ tag }}
</a-tag>
</template>
<span v-else class="no-val-text">未选择</span>
</div>
</div>
<!-- 4. 多行意见框 -->
<div v-else-if="q.type === 'textarea'" class="q-body-text">
<a-textarea
v-if="!readOnly"
v-model:value="localAnswers[q.id]"
placeholder="请在此填写具体的意见与建议描述..."
:rows="3"
class="renderer-textarea"
maxLength="200"
showCount
/>
<div v-else class="readonly-textarea-box">
{{ localAnswers[q.id] || '无建议或意见' }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
questions: {
type: Array,
required: true,
default: () => []
},
value: {
type: Object,
required: true,
default: () => ({})
},
readOnly: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['update:value']);
//
const localAnswers = ref({ ...props.value });
watch(
() => props.value,
(newVal) => {
localAnswers.value = { ...newVal };
},
{ deep: true }
);
watch(
localAnswers,
(newVal) => {
emit('update:value', newVal);
},
{ deep: true }
);
</script>
<style scoped>
.survey-renderer {
display: flex;
flex-direction: column;
gap: 16px;
}
.q-card-item {
background: #ffffff;
border: 1px solid #f0f0f0;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.01);
transition: all 0.2s ease;
}
.q-card-item:hover {
border-color: #e6f7ff;
}
.q-card-item.is-readonly {
padding: 12px 16px;
border-color: #f0f0f0;
background: #fafafa;
}
.q-title-row {
margin-bottom: 12px;
line-height: 1.4;
display: flex;
align-items: flex-start;
}
.q-req-star {
color: #ff4d4f;
margin-right: 4px;
font-weight: bold;
}
.q-seq-num {
font-size: 14px;
font-weight: 700;
color: #1890ff;
margin-right: 6px;
}
.q-title-text {
font-size: 14px;
font-weight: 700;
color: #262626;
}
.q-answer-body {
padding-left: 4px;
}
/* 打分类样式 */
.q-body-rate {
display: flex;
align-items: center;
gap: 12px;
}
.renderer-rate {
font-size: 24px;
}
.rate-label-text {
font-size: 13px;
color: #fa8c16;
font-weight: 700;
}
/* 按钮、单选多选样式 */
.renderer-radio-group,
.renderer-checkbox-group {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.renderer-radio-item,
.renderer-checkbox-item {
margin: 0 !important;
background: #fafafa;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 10px 12px;
display: flex;
align-items: center;
transition: all 0.2s ease;
width: 100%;
box-sizing: border-box;
}
.renderer-radio-item:hover,
.renderer-checkbox-item:hover {
border-color: #1890ff;
background: #f0f7ff;
}
.opt-label-text {
font-size: 13px;
color: #595959;
}
.opt-score-badge {
color: #bfbfbf;
font-size: 11px;
margin-left: 6px;
}
/* 多行文本框样式 */
.renderer-textarea {
border-radius: 8px;
background: #fafafa;
border: 1px solid #d9d9d9;
}
/* 只读状态细节渲染 */
.readonly-value {
padding: 4px 0;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.val-tag {
font-size: 13px;
padding: 4px 10px;
border-radius: 6px;
margin: 0 !important;
}
.no-val-text {
color: #bfbfbf;
font-size: 13px;
font-style: italic;
}
.readonly-textarea-box {
background: #f5f5f5;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 10px 14px;
color: #595959;
font-size: 13px;
line-height: 1.5;
min-height: 48px;
word-break: break-all;
}
</style>

491
smart-flow-web/src/views/jwpy/mydc/my-survey/fill.vue

@ -0,0 +1,491 @@
<!--
* 警务评议与问题处置 - 群众自助评议手机端 H5 填报页面
*
* @Author: Antigravity
* @Date: 2026-06-14
-->
<template>
<div class="h5-container">
<div class="h5-phone-mockup">
<!-- 手机顶部状态栏模拟 -->
<div class="phone-status-bar">
<span class="time">10:24</span>
<div class="icons">
<span class="signal">📶</span>
<span class="wifi">📶</span>
<span class="battery">🔋</span>
</div>
</div>
<!-- 手机页面头部 -->
<div class="phone-navbar">
<span class="navbar-title">{{ surveyInfo.title || '群众意见自主测评' }}</span>
</div>
<!-- 手机主内容区 -->
<div class="phone-body">
<!-- 答卷成功感谢状态 -->
<div v-if="submitted" class="success-state">
<div class="success-icon-wrap">
<div class="success-checkmark"></div>
</div>
<h2>评议提交成功</h2>
<p class="success-desc">感谢您对我们的支持与配合您的宝贵意见是推动我们不断改进服务的动力</p>
<div class="divider"></div>
<p class="success-footer">XX市公安局民意感知中心</p>
</div>
<!-- 正常填报状态 -->
<div v-else-if="loading" class="loading-state">
<a-spin size="large" tip="正在加载问卷内容..." />
</div>
<div v-else-if="errorMsg" class="error-state">
<div class="error-icon"></div>
<h2>加载失败</h2>
<p>{{ errorMsg }}</p>
</div>
<div v-else class="survey-form">
<!-- 问卷寄语 -->
<div class="intro-card">
<div class="intro-title">尊敬的市民朋友</div>
<p class="intro-body">
您好为持续改进我局政务办事效率与窗口服务质量诚邀您参与本次针对您近期办理业务的满意度评议整个问卷约需1分钟感谢您的参与
</p>
</div>
<!-- 问题列表 -->
<SurveyFormRenderer :questions="questions" v-model:value="answers" />
<!-- 提交按钮 -->
<div class="submit-action">
<a-button type="primary" block size="large" class="h5-submit-btn" @click="handleSubmit">
提交评议反馈
</a-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { message } from 'ant-design-vue';
import SurveyFormRenderer from './components/SurveyFormRenderer.vue';
import { surveySampleApi } from '/@/api/business/jwpy/survey-sample-api';
const route = useRoute();
const loading = ref(true);
const submitted = ref(false);
const errorMsg = ref('');
const taskInfo = ref({});
const surveyInfo = ref({});
const questions = ref([]);
const answers = reactive({});
onMounted(() => {
initSurvey();
});
const initSurvey = async () => {
const taskId = route.query.taskId;
if (!taskId) {
errorMsg.value = '无效的评议参数(缺少 taskId)';
loading.value = false;
return;
}
try {
// 1.
const res = await surveySampleApi.getTaskDetail(taskId);
const task = res.data;
if (!task) {
errorMsg.value = '未找到对应的评议任务记录';
loading.value = false;
return;
}
if (task.status === 1) {
submitted.value = true;
loading.value = false;
return;
}
taskInfo.value = task;
// 2.
const surveyRes = await surveySampleApi.querySurveys({ pageNum: 1, pageSize: 1000 });
const templates = surveyRes.data ? (surveyRes.data.list || []) : [];
let template = null;
if (task.surveyId) {
template = templates.find(t => (t.id || t.surveyId) === task.surveyId);
}
if (!template) {
template = templates.find(t => t.sjlyNo === task.sjlyNo);
}
if (!template) {
template = templates[0]; //
}
if (!template) {
errorMsg.value = '暂无可用的评议问卷模板,请联系管理员配置。';
loading.value = false;
return;
}
surveyInfo.value = template;
try {
questions.value = JSON.parse(template.content);
//
questions.value.forEach(q => {
if (q.type === 'checkbox') {
answers[q.id] = [];
} else {
answers[q.id] = undefined;
}
});
} catch (e) {
questions.value = [];
}
} catch (err) {
errorMsg.value = '加载测评内容失败,请确保后端服务正常且表结构已同步';
} finally {
loading.value = false;
}
};
const handleSubmit = async () => {
//
for (const q of questions.value) {
const ans = answers[q.id];
if (q.required) {
if (q.type === 'checkbox' && (!ans || ans.length === 0)) {
message.warning(`请填写必填项: ${q.title}`);
return;
}
if (q.type !== 'checkbox' && (ans === undefined || ans === null || String(ans).trim() === '')) {
message.warning(`请填写必填项: ${q.title}`);
return;
}
}
}
try {
const res = await surveySampleApi.submitH5Answer({
taskId: taskInfo.value.id,
answers: JSON.stringify(answers)
});
// 110
updateWorkOrderSatisfaction(taskInfo.value);
submitted.value = true;
message.success(res.msg || '评议提交成功');
} catch (err) {
message.error('提交答卷失败,请检查您的网络连接');
}
};
//
const updateWorkOrderSatisfaction = (task) => {
//
let score = 5;
for (const key in answers) {
if (typeof answers[key] === 'number') {
score = answers[key]; //
break;
}
}
// 1 2
const sfjg = score >= 3 ? '1' : '2';
const qzmyqk = score >= 3 ? '1' : '2';
//
//
console.log(`群众 H5 自填回流成功:任务ID=${task.id}, 评分=${score}星, 满意度归类=${qzmyqk}`);
};
</script>
<style scoped>
/* 群众手机端外围仿真背景 */
.h5-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f1f3f6;
padding: 24px 0;
box-sizing: border-box;
}
/* 手机外观容器 */
.h5-phone-mockup {
width: 375px;
height: 720px;
background: #000000;
border-radius: 40px;
padding: 12px;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
box-sizing: border-box;
border: 4px solid #1f2937;
position: relative;
}
/* 手机状态栏 */
.phone-status-bar {
height: 20px;
color: #ffffff;
font-size: 11px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.phone-status-bar .icons {
display: flex;
gap: 4px;
}
/* 页面顶部导航栏 */
.phone-navbar {
height: 44px;
background: #1890ff;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
font-size: 15px;
font-weight: 700;
flex-shrink: 0;
}
.navbar-title {
padding: 0 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 280px;
}
/* 手机屏幕主内容区 */
.phone-body {
flex: 1;
background: #f5f6fa;
border-bottom-left-radius: 30px;
border-bottom-right-radius: 30px;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
}
.phone-body::-webkit-scrollbar {
width: 4px;
}
.phone-body::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.05);
border-radius: 2px;
}
/* 问卷寄语 */
.intro-card {
background: #ffffff;
border-radius: 12px;
padding: 14px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02);
border: 1px solid #f0f0f0;
}
.intro-title {
font-size: 14px;
font-weight: 700;
color: #2c3e50;
margin-bottom: 6px;
}
.intro-body {
font-size: 12px;
color: #7f8c8d;
line-height: 1.6;
margin: 0;
}
/* 问题卡片 */
.question-card {
background: #ffffff;
border-radius: 12px;
padding: 16px;
margin-bottom: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02);
border: 1px solid #f0f0f0;
}
.q-header {
margin-bottom: 12px;
line-height: 1.4;
}
.q-req {
color: #ff4d4f;
margin-right: 4px;
font-weight: bold;
}
.q-index {
font-size: 14px;
font-weight: 700;
color: #1890ff;
margin-right: 4px;
}
.q-title {
font-size: 14px;
font-weight: 700;
color: #262626;
}
.q-body {
padding-top: 4px;
}
/* 星级评价 */
.rate-body {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 0;
}
.rate-label {
margin-top: 8px;
font-size: 12px;
color: #8c8c8c;
}
.score-num {
color: #f1c40f;
font-weight: 700;
}
/* 单选多选移动端样式 */
.mobile-radio-group,
.mobile-checkbox-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.mobile-radio-item,
.mobile-checkbox-item {
margin: 0 !important;
background: #fafafa;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 10px 12px;
display: flex;
align-items: center;
transition: all 0.2s ease;
}
.mobile-radio-item:hover,
.mobile-checkbox-item:hover {
border-color: #1890ff;
background: #f0f7ff;
}
.opt-text {
font-size: 13px;
color: #595959;
}
/* 多行文本框 */
.mobile-textarea {
border-radius: 8px;
background: #fafafa;
}
/* 提交按钮 */
.submit-action {
margin: 16px 0 24px;
}
.h5-submit-btn {
height: 48px;
border-radius: 12px;
font-size: 16px;
font-weight: 700;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2);
}
/* 加载、错误、成功页面状态 */
.loading-state,
.error-state,
.success-state {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 24px;
text-align: center;
}
.error-icon {
font-size: 48px;
margin-bottom: 16px;
}
.success-state h2 {
font-size: 20px;
font-weight: 700;
color: #2c3e50;
margin: 16px 0 8px;
}
.success-desc {
font-size: 13px;
color: #7f8c8d;
line-height: 1.6;
}
.success-icon-wrap {
width: 72px;
height: 72px;
background: #f6ffed;
border: 2px solid #b7eb8f;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 8px;
}
.success-checkmark {
font-size: 36px;
color: #52c41a;
font-weight: bold;
}
.success-state .divider {
width: 60px;
height: 1px;
background: #d9d9d9;
margin: 24px 0;
}
.success-footer {
font-size: 11px;
color: #bfbfbf;
}
</style>

493
smart-flow-web/src/views/jwpy/mydc/my-survey/index.vue

@ -55,15 +55,31 @@
</a-form-item>
</a-form>
<!-- 新建调查任务操作栏 -->
<div class="table-operate-row" style="margin-bottom: 12px; margin-top: 12px;" v-if="isTodoView">
<a-button type="primary" @click="showCreateTaskModal">
<template #icon><PlusOutlined /></template>
新建调查任务 (关联样本库推送)
</a-button>
</div>
<a-table
:dataSource="tableData"
:columns="columns"
rowKey="id"
bordered
:pagination="false"
:loading="tableLoading"
:pagination="pagination"
@change="handleTableChange"
:scroll="{ x: 1300 }"
class="custom-table"
>
<template #bodyCell="{ text, record, column }">
<!-- 任务ID展示 -->
<template v-if="column.dataIndex === 'id'">
<span style="font-family: monospace; font-size: 13px;">{{ text }}</span>
</template>
<!-- 电话号码脱敏展示 -->
<template v-if="column.dataIndex === 'phone'">
<span>{{ text.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') }}</span>
@ -72,7 +88,12 @@
<!-- 关联警务业务 -->
<template v-if="column.dataIndex === 'sjlyNo'">
<a-tag :color="getBizTypeColor(text)">
<DictLabel :dict-code="DICT_CODE_ENUM.BUSINESS_CLASSIFICATION" :data-value="text" />
<span v-if="text === 'crj'">出入境</span>
<span v-else-if="text === 'hz'">户政</span>
<span v-else-if="text === '110'">110接处警</span>
<span v-else>
<DictLabel :dict-code="DICT_CODE_ENUM.BUSINESS_CLASSIFICATION" :data-value="text" />
</span>
</a-tag>
</template>
@ -86,14 +107,26 @@
<!-- 操作 -->
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button type="link" @click="handleCall(record)">
<template #icon><PhoneOutlined /></template>
拨打电话
</a-button>
<a-button type="link" :disabled="record.status === 1" @click="handleFill(record)">
<template #icon><FormOutlined /></template>
登记答卷
</a-button>
<template v-if="record.status !== 1">
<a-button type="link" @click="handleCall(record)">
<template #icon><PhoneOutlined /></template>
拨号
</a-button>
<a-button type="link" @click="handleSendSms(record)">
<template #icon><SendOutlined /></template>
发送评议
</a-button>
<a-button type="link" @click="handleFill(record)">
<template #icon><FormOutlined /></template>
登记
</a-button>
</template>
<template v-else>
<a-button type="link" @click="handleViewAnswers(record)">
<template #icon><EyeOutlined /></template>
查看答卷
</a-button>
</template>
</div>
</template>
</template>
@ -127,39 +160,102 @@
<a-divider />
<!-- 模拟渲染问卷题目 -->
<div v-for="(q, idx) in activeQuestions" :key="q.id" style="margin-bottom: 20px">
<div style="font-weight: 600; margin-bottom: 10px; font-size: 14px">
<span style="color: red" v-if="q.required">*</span>
Q{{ idx + 1 }}. {{ q.title }}
</div>
<!-- 打分 -->
<div v-if="q.type === 'rate'">
<a-rate v-model:value="answerForm.answers[q.id]" />
</div>
<!-- 单选 -->
<div v-if="q.type === 'radio'">
<a-radio-group v-model:value="answerForm.answers[q.id]" style="width: 100%">
<a-radio v-for="opt in q.options" :key="opt.label" :value="opt.label" style="display: block; margin: 6px 0">
{{ opt.label }} <span style="color: #bfbfbf; font-size: 11px">(分值: {{ opt.score }})</span>
</a-radio>
</a-radio-group>
</div>
<!-- 多选 -->
<div v-if="q.type === 'checkbox'">
<a-checkbox-group v-model:value="answerForm.answers[q.id]" style="width: 100%">
<a-checkbox v-for="opt in q.options" :key="opt.label" :value="opt.label" style="display: block; margin: 6px 0">
{{ opt.label }}
</a-checkbox>
</a-checkbox-group>
</div>
<!-- 多行文本 -->
<div v-if="q.type === 'textarea'">
<a-textarea v-model:value="answerForm.answers[q.id]" placeholder="请输入受访人具体意见与诉求描述..." :rows="3" />
</div>
<SurveyFormRenderer :questions="activeQuestions" v-model:value="answerForm.answers" />
</a-form>
</div>
</a-modal>
<!-- 模拟发送测评短信弹窗 -->
<a-modal
v-model:open="smsVisible"
title="模拟发送短信/微信公众号评议"
:footer="null"
:width="500"
>
<div style="padding: 12px 0; text-align: center;" v-if="smsRecord">
<p style="text-align: left; font-size: 13px; color: #595959; background: #f5f5f5; padding: 12px; border-radius: 8px; line-height: 1.6;">
<strong>模拟短信/公众号发送内容</strong><br />
XX市民意感知中心您好{{ smsRecord.name }}您办理的业务已办结请点击以下专属链接对民警的服务进行满意度测评<br />
<a :href="smsUrl" target="_blank" style="word-break: break-all; color: #1890ff; font-weight: 500;">{{ smsUrl }}</a>
</p>
<div style="margin: 20px 0;">
<img :src="qrCodeUrl" alt="二维码" style="width: 140px; height: 140px; border: 1px solid #f0f0f0; padding: 6px; border-radius: 8px;" />
<p style="font-size: 12px; color: #8c8c8c; margin-top: 8px;">📲 拿出您的真实手机扫码即可直接在您的手机上进行填报</p>
</div>
<a-button type="primary" block size="large" @click="confirmSendSms" style="border-radius: 8px; height: 40px;">
确认模拟发送完成
</a-button>
</div>
</a-modal>
<!-- 查看群众已填答卷弹窗 -->
<a-modal
v-model:open="viewVisible"
title="群众历史答卷详情"
:footer="null"
:width="550"
>
<div style="padding: 12px 0;" v-if="viewRecord">
<div style="background: #e6f7ff; padding: 12px; border-radius: 8px; margin-bottom: 16px; font-size: 13px;">
<span style="font-weight: bold; color: #1890ff;">受访群众</span>{{ viewRecord.name }} &nbsp;|&nbsp;
<span style="font-weight: bold; color: #1890ff;">关联业务</span>{{ viewRecord.surveyTitle }}
</div>
<!-- 使用通用问卷渲染器 -->
<SurveyFormRenderer v-if="viewQuestions.length > 0" :questions="viewQuestions" v-model:value="viewAnswers" readOnly />
<div v-else style="text-align: center; color: #bfbfbf; padding: 20px;">
暂无该群众答卷题目明细
</div>
</div>
</a-modal>
<!-- 新建调查任务弹窗 -->
<a-modal
v-model:open="createTaskVisible"
title="新建调查评议任务"
@ok="handleSaveCreateTask"
:width="550"
ok-text="确认并批量推送"
cancel-text="取消"
>
<div style="padding: 16px 0">
<a-form layout="vertical">
<a-form-item label="任务名称" required>
<a-input v-model:value="createTaskForm.title" placeholder="如:2026年第一季度高层次精英人才发展问卷调查" />
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="目标样本分组 (人才类型)" required>
<a-select v-model:value="createTaskForm.groupId" placeholder="请选择受众分组" @change="onGroupChange">
<a-select-option v-for="g in groupOptions" :key="g.id" :value="g.id">
{{ g.name }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="选择关联问卷模板" required>
<a-select v-model:value="createTaskForm.surveyId" placeholder="请选择问卷模板">
<a-select-option v-for="s in surveyOptions" :key="s.surveyId" :value="s.surveyId">
{{ s.title }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="推送方式" required>
<a-radio-group v-model:value="createTaskForm.pushType">
<a-radio value="sms">短信链接通知 (实时分发)</a-radio>
<a-radio value="wx">微信公众号模板消息</a-radio>
</a-radio-group>
</a-form-item>
<div v-if="targetSampleCount > 0" style="background: #f6ffed; border: 1px solid #b7eb8f; padding: 10px; border-radius: 8px; font-size: 13px; color: #389e0d;">
💡 已检测到该分组下共有 <strong>{{ targetSampleCount }}</strong> 名样本群众系统将自动生成 <strong>{{ targetSampleCount }}</strong> 个专属评议短链并模拟批量发送
</div>
<div v-else-if="createTaskForm.groupId" style="background: #fffbe6; border: 1px solid #ffe58f; padding: 10px; border-radius: 8px; font-size: 13px; color: #d46b08;">
警告该分组下暂无样本人员请先到调查样本库中导入或添加人员
</div>
</a-form>
</div>
@ -169,13 +265,102 @@
<script setup>
import { ref, reactive, onMounted, watch } from 'vue';
import SurveyFormRenderer from './components/SurveyFormRenderer.vue';
import { useRoute } from 'vue-router';
import { message, Modal } from 'ant-design-vue';
import { PhoneOutlined, FormOutlined } from '@ant-design/icons-vue';
import { PhoneOutlined, FormOutlined, SendOutlined, EyeOutlined, PlusOutlined } from '@ant-design/icons-vue';
import DictLabel from '/@/components/support/dict-label/index.vue';
import { DICT_CODE_ENUM } from '/@/constants/support/dict-const';
import { surveySampleApi } from '/@/api/business/jwpy/survey-sample-api';
const route = useRoute();
//
const createTaskVisible = ref(false);
const groupOptions = ref([]);
const surveyOptions = ref([]);
const targetSampleCount = ref(0);
const createTaskForm = reactive({
title: '',
groupId: undefined,
surveyId: undefined,
pushType: 'sms'
});
const showCreateTaskModal = async () => {
createTaskForm.title = '';
createTaskForm.groupId = undefined;
createTaskForm.surveyId = undefined;
createTaskForm.pushType = 'sms';
targetSampleCount.value = 0;
try {
// 1.
const groupRes = await surveySampleApi.queryGroupList();
if (groupRes.data) {
groupOptions.value = groupRes.data;
}
// 2. ()
const surveyRes = await surveySampleApi.querySurveys({ pageNum: 1, pageSize: 500, status: 1 });
if (surveyRes.data) {
surveyOptions.value = surveyRes.data.list || [];
}
} catch (e) {
message.error('加载创建选项数据失败,请确认后端表已建立');
}
createTaskVisible.value = true;
};
const onGroupChange = async (groupId) => {
if (!groupId) {
targetSampleCount.value = 0;
return;
}
try {
const res = await surveySampleApi.querySamplePage({
groupId,
pageNum: 1,
pageSize: 1
});
if (res.data) {
targetSampleCount.value = res.data.total || 0;
}
} catch (e) {
targetSampleCount.value = 0;
}
};
const handleSaveCreateTask = async () => {
if (!createTaskForm.title.trim()) {
message.warning('请输入调查任务名称');
return;
}
if (!createTaskForm.groupId) {
message.warning('请选择目标样本分组');
return;
}
if (!createTaskForm.surveyId) {
message.warning('请选择要关联的问卷模板');
return;
}
try {
const res = await surveySampleApi.createSurveyTask({
title: createTaskForm.title.trim(),
groupId: createTaskForm.groupId,
surveyId: createTaskForm.surveyId,
pushType: createTaskForm.pushType
});
message.success(res.msg || '批量生成并群发成功!');
createTaskVisible.value = false;
loadTasks();
} catch (e) {
message.error('生成调查任务失败,请核对分组中是否存在样本');
}
};
const isTodoView = ref(true);
const queryForm = reactive({
@ -184,6 +369,14 @@ const queryForm = reactive({
});
const tableData = ref([]);
const tableLoading = ref(false);
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total}`
});
const fillVisible = ref(false);
const activeSurvey = ref(null);
const activeQuestions = ref([]);
@ -195,15 +388,15 @@ const answerForm = reactive({
});
const columns = [
{ title: '任务ID', dataIndex: 'id', width: 120, align: 'center' },
{ title: '任务ID', dataIndex: 'id', width: 190, align: 'center' },
{ title: '受访群众', dataIndex: 'name', width: 100, align: 'center' },
{ title: '电话号码', dataIndex: 'phone', width: 130, align: 'center' },
{ title: '关联业务', dataIndex: 'sjlyNo', width: 140, align: 'center' },
{ title: '关联业务', dataIndex: 'sjlyNo', width: 110, align: 'center' },
{ title: '呼叫次数', dataIndex: 'callCount', width: 90, align: 'center' },
{ title: '状态', dataIndex: 'status', width: 130, align: 'center' },
{ title: '匹配问卷模板', dataIndex: 'surveyTitle', align: 'left' },
{ title: '匹配问卷模板', dataIndex: 'surveyTitle', width: 240, ellipsis: true },
{ title: '分配时间', dataIndex: 'assignTime', width: 160, align: 'center' },
{ title: '操作', dataIndex: 'action', width: 200, align: 'center' }
{ title: '操作', dataIndex: 'action', width: 220, align: 'center', fixed: 'right' }
];
const mockTasks = [
@ -213,42 +406,40 @@ const mockTasks = [
{ id: 'T9004', name: '刘小青', phone: '18899998888', sjlyNo: '110', callCount: 1, status: 1, surveyTitle: '110接处警群众满意度回访问卷', assignTime: '2026-06-10 16:15:00' }
];
const loadTasks = () => {
const loadTasks = async () => {
const path = route.path;
isTodoView.value = !path.endsWith('/draft');
const localTasks = localStorage.getItem('local_survey_tasks');
let list = [];
if (!localTasks) {
localStorage.setItem('local_survey_tasks', JSON.stringify(mockTasks));
list = mockTasks;
} else {
list = JSON.parse(localTasks);
tableLoading.value = true;
try {
const res = await surveySampleApi.queryTaskPage({
name: queryForm.name,
status: (queryForm.status !== undefined && queryForm.status !== '') ? Number(queryForm.status) : undefined,
isTodoView: isTodoView.value,
pageNum: pagination.current,
pageSize: pagination.pageSize
});
if (res.data) {
tableData.value = res.data.list || [];
pagination.total = res.data.total || 0;
}
} catch (err) {
message.error('获取任务列表失败');
} finally {
tableLoading.value = false;
}
};
tableData.value = list.filter(item => {
let matched = true;
if (queryForm.name && !item.name.includes(queryForm.name)) matched = false;
if (queryForm.status !== undefined && queryForm.status !== '') {
if (item.status.toString() !== queryForm.status) matched = false;
} else {
//
if (isTodoView.value) {
// (0) / (2)
if (item.status !== 0 && item.status !== 2) matched = false;
} else {
// / (2) / 访 (3)
if (item.status !== 2 && item.status !== 3) matched = false;
}
}
return matched;
});
const handleTableChange = (pag) => {
pagination.current = pag.current;
pagination.pageSize = pag.pageSize;
loadTasks();
};
watch(
() => route.path,
() => {
pagination.current = 1;
loadTasks();
}
);
@ -259,22 +450,24 @@ const getBizTypeColor = (bizType) => {
};
const getStatusColor = (status) => {
const map = { 0: 'blue', 1: 'green', 2: 'orange', 3: 'red' };
const map = { 0: 'blue', 1: 'green', 2: 'orange', 3: 'red', 4: 'cyan' };
return map[status] || 'default';
};
const getStatusText = (status) => {
const map = { 0: '待调查', 1: '调查成功', 2: '无人接听', 3: '拒访' };
const map = { 0: '待调查', 1: '调查成功', 2: '无人接听', 3: '拒访', 4: '已发短信评议' };
return map[status] || '待调查';
};
const onSearch = () => {
pagination.current = 1;
loadTasks();
};
const resetQuery = () => {
queryForm.name = '';
queryForm.status = undefined;
pagination.current = 1;
loadTasks();
};
@ -284,59 +477,121 @@ const handleCall = (record) => {
content: `正在为您呼叫群众 [${record.name}] 的手机:${record.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')}...`,
okText: '挂断',
onOk() {
const list = JSON.parse(localStorage.getItem('local_survey_tasks') || '[]');
const index = list.findIndex(item => item.id === record.id);
if (index !== -1) {
list[index].callCount += 1;
if (list[index].status === 0) {
list[index].status = 2;
}
localStorage.setItem('local_survey_tasks', JSON.stringify(list));
loadTasks();
message.info('呼叫已挂断,系统已更新呼叫计数');
}
message.info('呼叫已挂断');
}
});
};
const handleFill = (record) => {
const handleFill = async (record) => {
currentRecord.value = record;
const templates = JSON.parse(localStorage.getItem('local_surveys') || '[]');
let template = templates.find(t => t.sjlyNo === record.sjlyNo);
if (!template) {
template = templates[0];
}
if (template) {
activeSurvey.value = template;
try {
activeQuestions.value = JSON.parse(template.content);
} catch (e) {
activeQuestions.value = [];
try {
const res = await surveySampleApi.querySurveys({ pageNum: 1, pageSize: 500 });
const templates = res.data ? (res.data.list || []) : [];
let template = null;
if (record.surveyId) {
template = templates.find(t => t.surveyId === record.surveyId);
}
if (!template) {
template = templates.find(t => t.sjlyNo === record.sjlyNo);
}
if (!template) {
template = templates[0];
}
answerForm.answers = {};
activeQuestions.value.forEach(q => {
answerForm.answers[q.id] = undefined;
});
answerForm.respondentAttitude = '1';
fillVisible.value = true;
} else {
message.warning('尚未设计该业务类别的问卷模板,请先设计问卷');
if (template) {
activeSurvey.value = template;
try {
activeQuestions.value = template.content ? JSON.parse(template.content) : [];
} catch (err) {
activeQuestions.value = [];
}
answerForm.answers = {};
activeQuestions.value.forEach(q => {
answerForm.answers[q.id] = undefined;
});
answerForm.respondentAttitude = '1';
fillVisible.value = true;
} else {
message.warning('尚未配置该业务类别的问卷模板,请先到问卷设计器配置');
}
} catch (e) {
message.error('加载问卷模板失败');
}
};
const handleSubmitAnswers = () => {
const list = JSON.parse(localStorage.getItem('local_survey_tasks') || '[]');
const index = list.findIndex(item => item.id === currentRecord.value.id);
if (index !== -1) {
list[index].status = 1;
localStorage.setItem('local_survey_tasks', JSON.stringify(list));
message.success('测评答卷保存成功,已进入满意度统计库!');
const handleSubmitAnswers = async () => {
try {
const res = await surveySampleApi.submitH5Answer({
taskId: currentRecord.value.id,
answers: JSON.stringify(answerForm.answers)
});
message.success(res.msg || '测评答卷保存成功,已进入满意度统计库!');
fillVisible.value = false;
loadTasks();
} catch (e) {
message.error('保存答卷失败');
}
};
const smsVisible = ref(false);
const smsRecord = ref(null);
const smsUrl = ref('');
const qrCodeUrl = ref('');
const viewVisible = ref(false);
const viewRecord = ref(null);
const viewQuestions = ref([]);
const viewAnswers = ref({});
const handleSendSms = (record) => {
smsRecord.value = record;
const baseUrl = window.location.origin + window.location.pathname;
smsUrl.value = `${baseUrl}#/survey/fill?taskId=${record.id}`;
qrCodeUrl.value = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(smsUrl.value)}`;
smsVisible.value = true;
};
const confirmSendSms = () => {
message.success(`已成功模拟发送短信评议链接至 ${smsRecord.value.name}`);
smsVisible.value = false;
loadTasks();
};
const handleViewAnswers = async (record) => {
viewRecord.value = record;
try {
viewAnswers.value = record.answers ? JSON.parse(record.answers) : {};
} catch (e) {
viewAnswers.value = {};
}
try {
const res = await surveySampleApi.querySurveys({ pageNum: 1, pageSize: 500 });
const templates = res.data ? (res.data.list || []) : [];
let template = null;
if (record.surveyId) {
template = templates.find(t => t.surveyId === record.surveyId);
}
if (!template) {
template = templates.find(t => t.sjlyNo === record.sjlyNo);
}
if (!template) {
template = templates[0];
}
if (template) {
try {
viewQuestions.value = template.content ? JSON.parse(template.content) : [];
} catch (err) {
viewQuestions.value = [];
}
} else {
viewQuestions.value = [];
}
} catch (e) {
viewQuestions.value = [];
}
viewVisible.value = true;
};
onMounted(() => {
@ -383,4 +638,8 @@ onMounted(() => {
.text-green { color: #52c41a; }
.text-orange { color: #fa8c16; }
.text-purple { color: #722ed1; }
.custom-table :deep(.ant-table-cell) {
white-space: nowrap;
}
</style>

215
smart-flow-web/src/views/jwpy/mydc/questionnaire/designer.vue

@ -20,6 +20,10 @@
</a-tag>
</div>
<div class="right-actions">
<a-button @click="handlePreview" style="margin-right: 8px">
<template #icon><EyeOutlined /></template>
预览问卷
</a-button>
<a-button type="primary" @click="handleSave" :loading="saving">
<template #icon><SaveOutlined /></template>
保存问卷设计
@ -149,10 +153,43 @@
</div>
</div>
</div>
<!-- 问卷手机填报端预览 Modal -->
<a-modal
v-model:open="previewVisible"
title="问卷手机填报效果预览"
:footer="null"
:width="420"
wrapClassName="phone-preview-modal"
>
<div class="phone-frame">
<div class="phone-screen">
<div class="phone-header">
<span class="phone-title">{{ surveyInfo?.title || '民意测评问卷' }}</span>
</div>
<div class="phone-content" v-if="questions.length > 0">
<div class="survey-intro">
您好为了进一步提升我们的政务服务品质耽误您1分钟时间参与本次民意调查测评感谢您的配合
</div>
<!-- 使用通用问卷渲染器 -->
<SurveyFormRenderer :questions="questions" v-model:value="previewAnswers" />
<a-button type="primary" block size="large" style="margin-top: 24px; border-radius: 8px" @click="handleSubmitPreview">
模拟提交问卷
</a-button>
</div>
<div class="phone-content empty-preview" v-else>
<a-empty description="该问卷尚未配置题目,请先前往左侧添加题目" />
</div>
</div>
</div>
</a-modal>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import SurveyFormRenderer from '../my-survey/components/SurveyFormRenderer.vue';
import { useRoute, useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
import {
@ -167,7 +204,8 @@ import {
DownOutlined,
DeleteOutlined,
MinusCircleOutlined,
PlusOutlined
PlusOutlined,
EyeOutlined
} from '@ant-design/icons-vue';
import DictLabel from '/@/components/support/dict-label/index.vue';
import { DICT_CODE_ENUM } from '/@/constants/support/dict-const';
@ -175,6 +213,8 @@ import { DICT_CODE_ENUM } from '/@/constants/support/dict-const';
const route = useRoute();
const router = useRouter();
import { surveySampleApi } from '/@/api/business/jwpy/survey-sample-api';
const surveyId = ref(null);
const surveyInfo = ref({});
const questions = ref([]);
@ -182,7 +222,7 @@ const activeQuestionId = ref(null);
const saving = ref(false);
//
const loadSurveyDetails = () => {
const loadSurveyDetails = async () => {
surveyId.value = Number(route.query.id);
if (!surveyId.value) {
message.error('未指定问卷模板ID');
@ -190,21 +230,26 @@ const loadSurveyDetails = () => {
return;
}
const localSurveys = JSON.parse(localStorage.getItem('local_surveys') || '[]');
const found = localSurveys.find(item => item.surveyId === surveyId.value);
if (found) {
surveyInfo.value = found;
try {
questions.value = found.content ? JSON.parse(found.content) : [];
if (questions.value.length > 0) {
activeQuestionId.value = questions.value[0].id;
try {
const res = await surveySampleApi.querySurveys({ pageNum: 1, pageSize: 500 });
const localSurveys = res.data?.list || [];
const found = localSurveys.find(item => item.surveyId === surveyId.value);
if (found) {
surveyInfo.value = found;
try {
questions.value = found.content ? JSON.parse(found.content) : [];
if (questions.value.length > 0) {
activeQuestionId.value = questions.value[0].id;
}
} catch (e) {
questions.value = [];
}
} catch (e) {
questions.value = [];
} else {
message.error('未找到对应问卷模板,已自动返回列表');
router.push('/mydc/questionnaire');
}
} else {
message.error('未找到对应问卷模板,已自动返回列表');
router.push('/mydc/questionnaire');
} catch (err) {
message.error('加载问卷模板详情失败');
}
};
@ -280,7 +325,7 @@ const getBizTypeColor = (bizType) => {
};
//
const handleSave = () => {
const handleSave = async () => {
const emptyQ = questions.value.some(q => !q.title.trim());
if (emptyQ) {
message.warning('画布中存在题目名称未填写的题目,请完善后再保存');
@ -288,19 +333,18 @@ const handleSave = () => {
}
saving.value = true;
setTimeout(() => {
const localSurveys = JSON.parse(localStorage.getItem('local_surveys') || '[]');
const index = localSurveys.findIndex(item => item.surveyId === surveyId.value);
if (index !== -1) {
localSurveys[index].content = JSON.stringify(questions.value);
localStorage.setItem('local_surveys', JSON.stringify(localSurveys));
message.success('问卷设计保存成功!已序列化为 JSON 写入数据库大字段');
handleBack();
} else {
message.error('保存失败:找不到问卷记录');
}
try {
await surveySampleApi.updateSurvey({
surveyId: surveyId.value,
content: JSON.stringify(questions.value)
});
message.success('保存成功');
handleBack();
} catch (err) {
message.error('保存设计失败');
} finally {
saving.value = false;
}, 400);
}
};
//
@ -308,6 +352,19 @@ const handleBack = () => {
router.push('/mydc/questionnaire');
};
const previewVisible = ref(false);
const previewAnswers = ref({});
const handlePreview = () => {
previewAnswers.value = {};
previewVisible.value = true;
};
const handleSubmitPreview = () => {
message.success('答卷模拟提交成功!');
previewVisible.value = false;
};
onMounted(() => {
loadSurveyDetails();
});
@ -585,4 +642,106 @@ onMounted(() => {
justify-content: center;
align-items: center;
}
/* 手机预览容器框 - 打造真机质感 */
.phone-frame {
width: 360px;
height: 600px;
background: #2f3640;
border-radius: 36px;
padding: 12px;
margin: 0 auto;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
position: relative;
}
.phone-screen {
width: 100%;
height: 100%;
background: #f5f6fa;
border-radius: 26px;
overflow-y: auto;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
/* 隐藏手机屏内部滚动条 */
.phone-screen::-webkit-scrollbar {
width: 4px;
}
.phone-screen::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1);
border-radius: 2px;
}
.phone-header {
height: 50px;
background: #1890ff;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.2);
}
.phone-title {
font-size: 14px;
font-weight: 600;
padding: 0 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.phone-content {
padding: 16px;
flex: 1;
}
.survey-intro {
font-size: 12px;
color: #7f8c8d;
line-height: 1.6;
background: #ffffff;
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 16px;
border: 1px solid #eef1f6;
}
.preview-q-card {
background: #ffffff;
border-radius: 10px;
padding: 14px;
margin-bottom: 14px;
border: 1px solid #eef1f6;
}
.q-title {
font-size: 13px;
font-weight: 700;
color: #2c3e50;
margin-bottom: 10px;
}
.q-req {
color: #e74c3c;
margin-right: 3px;
}
.rate-hint {
margin-left: 10px;
font-size: 12px;
color: #f1c40f;
font-weight: bold;
}
.empty-preview {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
</style>

163
smart-flow-web/src/views/jwpy/mydc/questionnaire/index.vue

@ -106,7 +106,7 @@
v-model:pageSize="queryForm.pageSize"
:total="totalCount"
:show-total="(total) => `共 ${total} 条`"
@change="loadLocalData"
@change="loadSurveys"
/>
</div>
</div>
@ -212,6 +212,7 @@ import { PlusOutlined } from '@ant-design/icons-vue';
import DictSelect from '/@/components/support/dict-select/index.vue';
import DictLabel from '/@/components/support/dict-label/index.vue';
import { DICT_CODE_ENUM } from '/@/constants/support/dict-const';
import { surveySampleApi } from '/@/api/business/jwpy/survey-sample-api';
const router = useRouter();
@ -229,22 +230,21 @@ const tableLoading = ref(false);
const tableData = ref([]);
const totalCount = ref(0);
// Modal
//
const modalVisible = ref(false);
const modalTitle = ref('创建新问卷模板');
const modalTitle = ref('');
const formRef = ref(null);
const formState = reactive({
title: '',
sjlyNo: undefined,
remark: ''
});
const formRules = {
title: [{ required: true, message: '请输入问卷名称', trigger: 'blur' }],
sjlyNo: [{ required: true, message: '请选择关联业务', trigger: 'change' }]
sjlyNo: [{ required: true, message: '请选择关联业务大类', trigger: 'change' }]
};
//
//
const previewVisible = ref(false);
const previewSurvey = ref(null);
const previewQuestions = ref([]);
@ -262,70 +262,26 @@ const columns = [
{ title: '操作', dataIndex: 'action', width: 280, align: 'center' }
];
// Mock
const initMockData = () => {
const localSurveys = localStorage.getItem('local_surveys');
if (!localSurveys) {
const defaultData = [
{
surveyId: 2026061001,
title: '110接处警群众满意度回访问卷',
sjlyNo: '110',
status: 1,
remark: '针对全市110报警群众的电话/短信随机民意抽评调查问卷。',
createTime: '2026-06-10 12:00:00',
content: JSON.stringify([
{ id: 'q1', type: 'rate', title: '请问您对本次110接警及出警速度是否满意?', required: true, options: [] },
{ id: 'q2', type: 'radio', title: '若您对本次服务感到不满意,主要原因是什么?', required: false, options: [
{ label: '出警不够迅速', score: 2 },
{ label: '接警人员态度生硬', score: 2 },
{ label: '现场处置简单粗暴', score: 2 },
{ label: '后续进展未得到反馈', score: 2 }
]},
{ id: 'q3', type: 'textarea', title: '您对我们的接处警工作还有哪些宝贵的意见或建议?', required: false, options: [] }
])
},
{
surveyId: 2026061002,
title: '车驾管服务窗口满意度评议问卷',
sjlyNo: 'cjg',
status: 0,
remark: '用于车驾管大厅自助服务及窗口办事群众的满意度调查。',
createTime: '2026-06-10 14:30:00',
content: JSON.stringify([
{ id: 'q1', type: 'radio', title: '您对本次办理车驾管业务的整体办事速度是否满意?', required: true, options: [
{ label: '非常满意 (得10分)', score: 10 },
{ label: '基本满意 (得8分)', score: 8 },
{ label: '不满意 (得2分)', score: 2 }
]},
{ id: 'q2', type: 'textarea', title: '您觉得窗口的哪些方面最需要提升改进?', required: false, options: [] }
])
}
];
localStorage.setItem('local_surveys', JSON.stringify(defaultData));
}
};
//
const loadLocalData = () => {
//
const loadSurveys = async () => {
tableLoading.value = true;
setTimeout(() => {
initMockData();
const data = JSON.parse(localStorage.getItem('local_surveys') || '[]');
//
let filtered = data.filter(item => {
let matched = true;
if (queryForm.title && !item.title.includes(queryForm.title)) matched = false;
if (queryForm.sjlyNo && item.sjlyNo !== queryForm.sjlyNo) matched = false;
if (queryForm.status !== undefined && item.status !== queryForm.status) matched = false;
return matched;
try {
const res = await surveySampleApi.querySurveys({
pageNum: queryForm.pageNum,
pageSize: queryForm.pageSize,
title: queryForm.title,
sjlyNo: queryForm.sjlyNo,
status: queryForm.status
});
tableData.value = filtered;
totalCount.value = filtered.length;
if (res.data) {
tableData.value = res.data.list || [];
totalCount.value = res.data.total || 0;
}
} catch (err) {
message.error('加载问卷模板失败');
} finally {
tableLoading.value = false;
}, 300);
}
};
//
@ -339,7 +295,7 @@ const getStatusText = (status) => {
return map[status] || '草稿';
};
// (/)
//
const getBizTypeColor = (bizType) => {
const map = { '110': 'red', 'hz': 'blue', 'crj': 'purple', 'cjg': 'green', 'ajbl': 'orange' };
return map[bizType] || 'cyan';
@ -357,27 +313,31 @@ const getQuestionCount = (record) => {
};
//
const toggleStatus = (record, targetStatus) => {
const data = JSON.parse(localStorage.getItem('local_surveys') || '[]');
const index = data.findIndex(item => item.surveyId === record.surveyId);
if (index !== -1) {
data[index].status = targetStatus;
localStorage.setItem('local_surveys', JSON.stringify(data));
const toggleStatus = async (record, targetStatus) => {
try {
await surveySampleApi.updateSurvey({
surveyId: record.surveyId,
status: targetStatus
});
message.success(targetStatus === 1 ? '启用成功' : '禁用成功');
loadLocalData();
loadSurveys();
} catch (err) {
message.error('修改状态失败');
}
};
//
const onSearch = () => {
loadLocalData();
queryForm.pageNum = 1;
loadSurveys();
};
const resetQuery = () => {
queryForm.title = '';
queryForm.sjlyNo = undefined;
queryForm.status = undefined;
loadLocalData();
queryForm.pageNum = 1;
loadSurveys();
};
//
@ -391,38 +351,37 @@ const handleCreate = () => {
//
const handleSaveInfo = () => {
formRef.value.validate().then(() => {
const data = JSON.parse(localStorage.getItem('local_surveys') || '[]');
const newSurvey = {
surveyId: Date.now(),
title: formState.title,
sjlyNo: formState.sjlyNo,
status: 0,
remark: formState.remark,
createTime: new Date().toLocaleString(),
content: JSON.stringify([])
};
data.unshift(newSurvey);
localStorage.setItem('local_surveys', JSON.stringify(data));
message.success('创建问卷基本信息成功,请前往“设计画布”添加题目');
modalVisible.value = false;
loadLocalData();
formRef.value.validate().then(async () => {
try {
await surveySampleApi.addSurvey({
title: formState.title,
sjlyNo: formState.sjlyNo,
remark: formState.remark
});
message.success('创建问卷基本信息成功,请前往“设计画布”添加题目');
modalVisible.value = false;
loadSurveys();
} catch (err) {
message.error('创建问卷模板失败');
}
});
};
//
const handleDelete = (surveyId) => {
const data = JSON.parse(localStorage.getItem('local_surveys') || '[]');
const filtered = data.filter(item => item.surveyId !== surveyId);
localStorage.setItem('local_surveys', JSON.stringify(filtered));
message.success('删除问卷模板成功');
loadLocalData();
const handleDelete = async (surveyId) => {
try {
await surveySampleApi.deleteSurvey(surveyId);
message.success('删除问卷模板成功');
loadSurveys();
} catch (err) {
message.error('删除问卷模板失败');
}
};
// ( /mydc/questionnaire-designer )
//
const handleDesign = (surveyId) => {
router.push({
path: '/mydc/questionnaire-designer',
path: '/jwpy/mydc/questionnaire-designer',
query: { id: surveyId }
});
};
@ -446,7 +405,7 @@ const handleSubmitPreview = () => {
};
onMounted(() => {
loadLocalData();
loadSurveys();
});
</script>

657
smart-flow-web/src/views/jwpy/mydc/sample/index.vue

@ -1,100 +1,208 @@
<!--
* 警务评议与问题处置 - 调查样本库管理
* 警务评议与问题处置 - 调查样本库管理 (后端真实API对接版)
*
* @Author: Antigravity
* @Date: 2026-06-10
* @Date: 2026-06-15
-->
<template>
<div class="page-container">
<div class="glass-card">
<div class="content-box">
<!-- 头部查询 -->
<a-form layout="inline" class="query-form">
<a-form-item label="群众姓名">
<a-input v-model:value="queryForm.name" placeholder="请输入姓名" style="width: 140px" allowClear />
</a-form-item>
<a-form-item label="业务类型">
<DictSelect
:dict-code="DICT_CODE_ENUM.BUSINESS_CLASSIFICATION"
placeholder="请选择业务分类"
v-model:value="queryForm.sjlyNo"
width="160px"
allowClear
/>
</a-form-item>
<a-form-item label="分配状态">
<a-select v-model:value="queryForm.assignStatus" placeholder="请选择" style="width: 120px" allowClear>
<a-select-option :value="0">未分配</a-select-option>
<a-select-option :value="1">已分配</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="onSearch">查询</a-button>
<a-button style="margin-left: 8px" @click="resetQuery">重置</a-button>
</a-form-item>
</a-form>
<!-- 操作按钮 -->
<div class="table-operate-row" style="margin-bottom: 12px">
<a-space>
<a-button type="primary" @click="handleImportFromBusiness">
<template #icon><SyncOutlined /></template>
一键同步警务样本 (110/窗口)
</a-button>
<a-button type="primary" :disabled="selectedRowKeys.length === 0" @click="handleBatchAssign">
<template #icon><UsergroupAddOutlined /></template>
分配回访员 (已选 {{ selectedRowKeys.length }} )
<a-row :gutter="16">
<!-- 左侧样本分组管理 -->
<a-col :span="6">
<div class="glass-card group-card">
<div class="card-header-row">
<span class="card-title">样本库分组</span>
<a-button type="primary" size="small" shape="circle" @click="showAddGroupModal">
<template #icon><PlusOutlined /></template>
</a-button>
</a-space>
</div>
<div class="group-list" v-if="!groupLoading">
<div
v-for="group in groupList"
:key="group.id"
class="group-item"
:class="{ active: selectedGroupId === group.id }"
@click="selectGroup(group.id)"
>
<div class="group-info">
<FolderOpenOutlined v-if="selectedGroupId === group.id" class="group-icon active-icon" />
<FolderOutlined v-else class="group-icon" />
<span class="group-name">{{ group.name }}</span>
</div>
<a-popconfirm
title="确定要删除该分组及其下所有样本吗?"
ok-text="确认"
cancel-text="取消"
@confirm="handleDeleteGroup(group.id)"
v-if="groupList.length > 1"
>
<DeleteOutlined class="delete-icon" @click.stop />
</a-popconfirm>
</div>
</div>
<div v-else style="text-align: center; padding: 20px 0;">
<a-spin size="small" tip="加载分组中..." />
</div>
</div>
</a-col>
<!-- 数据表格 -->
<a-table
:dataSource="tableData"
:columns="columns"
rowKey="id"
bordered
:row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
:pagination="false"
class="custom-table"
>
<template #bodyCell="{ text, record, column }">
<!-- 电话脱敏 -->
<template v-if="column.dataIndex === 'phone'">
<span>{{ text.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') }}</span>
</template>
<!-- 关联警务业务 -->
<template v-if="column.dataIndex === 'sjlyNo'">
<a-tag :color="getBizTypeColor(text)">
<DictLabel :dict-code="DICT_CODE_ENUM.BUSINESS_CLASSIFICATION" :data-value="text" />
</a-tag>
</template>
<!-- 分配状态 -->
<template v-if="column.dataIndex === 'assignStatus'">
<a-badge :status="text === 1 ? 'success' : 'default'" :text="text === 1 ? '已分配' : '待分配'" />
</template>
</template>
</a-table>
</div>
</div>
<!-- 右侧分组下的样本名单 -->
<a-col :span="18">
<div class="glass-card">
<div class="content-box">
<!-- 头部查询 -->
<a-form layout="inline" class="query-form">
<a-form-item label="群众姓名">
<a-input v-model:value="queryForm.name" placeholder="请输入姓名" style="width: 140px" allowClear />
</a-form-item>
<a-form-item label="电话号码">
<a-input v-model:value="queryForm.phone" placeholder="请输入手机号" style="width: 140px" allowClear />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="onSearch">查询</a-button>
<a-button style="margin-left: 8px" @click="resetQuery">重置</a-button>
</a-form-item>
</a-form>
<!-- 操作按钮 -->
<div class="table-operate-row" style="margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center;">
<a-space>
<a-button type="primary" @click="showAddSampleModal">
<template #icon><UserAddOutlined /></template>
新增人员
</a-button>
<a-button type="primary" ghost @click="showImportModal">
<template #icon><ImportOutlined /></template>
批量导入人员
</a-button>
</a-space>
<div class="group-summary-tag">
当前库<a-tag color="blue">{{ currentGroupName }}</a-tag> {{ pagination.total }}
</div>
</div>
<!-- 分配人员弹窗 -->
<!-- 数据表格 -->
<a-table
:dataSource="tableData"
:columns="columns"
rowKey="id"
bordered
:loading="tableLoading"
:pagination="pagination"
@change="handleTableChange"
class="custom-table"
>
<template #bodyCell="{ text, record, column }">
<!-- 电话脱敏 -->
<template v-if="column.dataIndex === 'phone'">
<span>{{ text ? text.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '-' }}</span>
</template>
<!-- 备注/分类 -->
<template v-if="column.dataIndex === 'remarks'">
<a-tag v-if="text" color="purple">{{ text }}</a-tag>
<span v-else>-</span>
</template>
<!-- 操作 -->
<template v-if="column.dataIndex === 'action'">
<a-popconfirm
title="确定要移除此人吗?"
ok-text="确认"
cancel-text="取消"
@confirm="handleDeleteSample(record.id)"
>
<a-button type="link" danger size="small">
<template #icon><DeleteOutlined /></template>
移除
</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
</div>
</div>
</a-col>
</a-row>
<!-- 新增分组弹窗 -->
<a-modal
v-model:open="assignVisible"
title="批量分配调查样本任务"
@ok="handleSaveAssign"
v-model:open="groupVisible"
title="新建样本库分类/分组"
@ok="handleSaveGroup"
:width="400"
ok-text="创建"
cancel-text="取消"
>
<div style="padding: 16px 0">
<a-form layout="vertical">
<a-form-item label="分类名称" required>
<a-input v-model:value="groupForm.name" placeholder="例如:高级精英人才库、警务窗口评议库" />
</a-form-item>
</a-form>
</div>
</a-modal>
<!-- 新增人员单条录入弹窗 -->
<a-modal
v-model:open="sampleVisible"
title="新增样本库人员"
@ok="handleSaveSample"
:width="500"
ok-text="保存"
cancel-text="取消"
>
<div style="padding: 16px 0">
<a-form layout="vertical">
<a-form-item label="选择回访调查员" required>
<a-select v-model:value="assignUser" placeholder="请选择回访人员">
<a-select-option value="张警官 (市局民意岗)">张警官 (市局民意岗)</a-select-option>
<a-select-option value="李回访 (市访评员)">李回访 (市访评员)</a-select-option>
<a-select-option value="王所长 (支队承办岗)">王所长 (支队承办岗)</a-select-option>
</a-select>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="群众姓名" required>
<a-input v-model:value="sampleForm.name" placeholder="请输入姓名" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="联系电话" required>
<a-input v-model:value="sampleForm.phone" placeholder="请输入11位手机号" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="业务分类/备注">
<a-input v-model:value="sampleForm.remarks" placeholder="如:高级领军人才、海外拔尖人才、车管窗口办事" />
</a-form-item>
</a-form>
</div>
</a-modal>
<!-- 批量导入人员弹窗 -->
<a-modal
v-model:open="importVisible"
title="批量解析导入样本人员"
@ok="handleDoImport"
:width="600"
ok-text="开始解析并导入"
cancel-text="取消"
>
<div style="padding: 12px 0">
<div style="background: #f5f5f5; padding: 12px; border-radius: 8px; margin-bottom: 16px; font-size: 13px; color: #595959; line-height: 1.6;">
<strong>💡 文本粘贴批量解析导入说明</strong><br />
请输入或粘贴多行文本数据每一行代表一个人各字段用<strong>英文逗号或空格</strong>分隔<br />
格式<code>姓名,手机号,业务备注/分类</code><br />
例如<br />
<code style="color: #1890ff; display: block; background: #e6f7ff; padding: 4px 8px; border-radius: 4px; margin-top: 4px;">
张三,13800000001,高级领军人才<br />
李四,13900000002,海外拔尖人才<br />
王五,13700000003,青年骨干人才
</code>
</div>
<a-form layout="vertical">
<a-form-item label="粘贴文本区域" required>
<a-textarea
v-model:value="importText"
placeholder="请在此处粘贴人员数据..."
:rows="8"
/>
</a-form-item>
</a-form>
</div>
@ -103,144 +211,237 @@
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ref, reactive, computed, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { SyncOutlined, UsergroupAddOutlined } from '@ant-design/icons-vue';
import DictSelect from '/@/components/support/dict-select/index.vue';
import DictLabel from '/@/components/support/dict-label/index.vue';
import { DICT_CODE_ENUM } from '/@/constants/support/dict-const';
import {
FolderOutlined,
FolderOpenOutlined,
PlusOutlined,
DeleteOutlined,
UserAddOutlined,
ImportOutlined
} from '@ant-design/icons-vue';
import { surveySampleApi } from '/@/api/business/jwpy/survey-sample-api';
const queryForm = reactive({
name: '',
sjlyNo: undefined,
assignStatus: undefined
phone: ''
});
//
const groupList = ref([]);
const selectedGroupId = ref('');
const groupLoading = ref(false);
const tableData = ref([]);
const selectedRowKeys = ref([]);
const assignVisible = ref(false);
const assignUser = ref(undefined);
const tableLoading = ref(false);
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total}`
});
//
const groupVisible = ref(false);
const groupForm = reactive({
name: ''
});
const sampleVisible = ref(false);
const sampleForm = reactive({
name: '',
phone: '',
remarks: ''
});
const importVisible = ref(false);
const importText = ref('');
//
const columns = [
{ title: '样本ID', dataIndex: 'id', width: 120, align: 'center' },
{ title: '群众姓名', dataIndex: 'name', width: 100, align: 'center' },
{ title: '电话号码', dataIndex: 'phone', width: 130, align: 'center' },
{ title: '业务分类', dataIndex: 'sjlyNo', width: 150, align: 'center' },
{ title: '数据来源', dataIndex: 'source', width: 150, align: 'center' },
{ title: '分配状态', dataIndex: 'assignStatus', width: 110, align: 'center' },
{ title: '分配调查员', dataIndex: 'assignee', align: 'left' },
{ title: '样本导入时间', dataIndex: 'importTime', width: 160, align: 'center' }
{ title: '姓名', dataIndex: 'name', width: 120, align: 'center' },
{ title: '联系电话', dataIndex: 'phone', width: 140, align: 'center' },
{ title: '业务分类/备注', dataIndex: 'remarks', width: 220, align: 'left' },
{ title: '导入时间', dataIndex: 'importTime', width: 180, align: 'center' },
{ title: '操作', dataIndex: 'action', width: 100, align: 'center' }
];
const mockSamples = [
{ id: 'S2001', name: '王有才', phone: '13899998888', sjlyNo: '110', source: '110警情自动提取', assignStatus: 1, assignee: '张警官 (市局民意岗)', importTime: '2026-06-10 10:00:00' },
{ id: 'S2002', name: '韩梅梅', phone: '13711112222', sjlyNo: 'hz', source: '户政窗口办结推送', assignStatus: 0, assignee: '-', importTime: '2026-06-10 11:30:00' },
{ id: 'S2003', name: '李雷', phone: '13633334444', sjlyNo: 'crj', source: '出入境大厅满意度抽样', assignStatus: 0, assignee: '-', importTime: '2026-06-10 12:45:00' },
{ id: 'S2004', name: '张小胖', phone: '18944445555', sjlyNo: 'cjg', source: '车驾管一门式综办反馈', assignStatus: 1, assignee: '李回访 (市访评员)', importTime: '2026-06-10 14:00:00' }
];
const currentGroupName = computed(() => {
const g = groupList.value.find(item => item.id === selectedGroupId.value);
return g ? g.name : '未知库';
});
const loadSamples = () => {
const local = localStorage.getItem('local_survey_samples');
if (!local) {
localStorage.setItem('local_survey_samples', JSON.stringify(mockSamples));
tableData.value = mockSamples;
} else {
const list = JSON.parse(local);
tableData.value = list.filter(item => {
let matched = true;
if (queryForm.name && !item.name.includes(queryForm.name)) matched = false;
if (queryForm.sjlyNo && item.sjlyNo !== queryForm.sjlyNo) matched = false;
if (queryForm.assignStatus !== undefined && item.assignStatus !== queryForm.assignStatus) matched = false;
return matched;
});
//
const selectGroup = (groupId) => {
selectedGroupId.value = groupId;
pagination.current = 1;
loadSamples();
};
//
const loadGroups = async () => {
groupLoading.value = true;
try {
const res = await surveySampleApi.queryGroupList();
if (res.data) {
groupList.value = res.data;
if (groupList.value.length > 0 && !selectedGroupId.value) {
selectedGroupId.value = groupList.value[0].id;
}
}
} catch (err) {
message.error('加载样本分组失败');
} finally {
groupLoading.value = false;
}
};
const getBizTypeColor = (bizType) => {
const map = { '110': 'red', 'hz': 'blue', 'crj': 'purple', 'cjg': 'green', 'ajbl': 'orange' };
return map[bizType] || 'cyan';
//
const loadSamples = async () => {
if (!selectedGroupId.value) return;
tableLoading.value = true;
try {
const res = await surveySampleApi.querySamplePage({
groupId: selectedGroupId.value,
name: queryForm.name,
phone: queryForm.phone,
pageNum: pagination.current,
pageSize: pagination.pageSize
});
if (res.data) {
tableData.value = res.data.list || [];
pagination.total = res.data.total || 0;
}
} catch (err) {
message.error('获取样本人员名单失败');
} finally {
tableLoading.value = false;
}
};
const onSearch = () => {
const handleTableChange = (pag) => {
pagination.current = pag.current;
pagination.pageSize = pag.pageSize;
loadSamples();
};
const resetQuery = () => {
queryForm.name = '';
queryForm.sjlyNo = undefined;
queryForm.assignStatus = undefined;
loadSamples();
//
const showAddGroupModal = () => {
groupForm.name = '';
groupVisible.value = true;
};
const handleSaveGroup = async () => {
if (!groupForm.name.trim()) {
message.warning('请输入分类/分组名称');
return;
}
try {
const res = await surveySampleApi.addGroup({ name: groupForm.name.trim() });
message.success(res.msg || '分类创建成功');
groupVisible.value = false;
await loadGroups();
} catch (err) {
message.error('新建分类失败');
}
};
const onSelectChange = (keys) => {
selectedRowKeys.value = keys;
//
const handleDeleteGroup = async (groupId) => {
try {
const res = await surveySampleApi.deleteGroup(groupId);
message.success(res.msg || '分类删除成功');
if (selectedGroupId.value === groupId) {
selectedGroupId.value = '';
}
await loadGroups();
loadSamples();
} catch (err) {
message.error('删除分类失败');
}
};
const handleImportFromBusiness = () => {
message.loading({ content: '正在实时同步110处警及大厅综办数据...', key: 'import' });
setTimeout(() => {
const list = JSON.parse(localStorage.getItem('local_survey_samples') || '[]');
const newSample = {
id: 'S' + (Date.now() % 10000),
name: '陈大民',
phone: '15912345678',
sjlyNo: '110',
source: '110警情自动提取',
assignStatus: 0,
assignee: '-',
importTime: new Date().toLocaleString()
};
list.unshift(newSample);
localStorage.setItem('local_survey_samples', JSON.stringify(list));
message.success({ content: '已成功从警务平台实时同步 1 条最新样本数据!', key: 'import', duration: 2 });
//
const showAddSampleModal = () => {
sampleForm.name = '';
sampleForm.phone = '';
sampleForm.remarks = '';
sampleVisible.value = true;
};
const handleSaveSample = async () => {
if (!sampleForm.name.trim()) {
message.warning('请填写群众姓名');
return;
}
if (!sampleForm.phone.trim() || !/^\d{11}$/.test(sampleForm.phone.trim())) {
message.warning('请填写有效的11位手机号');
return;
}
try {
const res = await surveySampleApi.addSample({
groupId: selectedGroupId.value,
name: sampleForm.name.trim(),
phone: sampleForm.phone.trim(),
remarks: sampleForm.remarks.trim()
});
message.success(res.msg || '样本录入成功');
sampleVisible.value = false;
loadSamples();
}, 1000);
} catch (err) {
message.error('录入失败,请重试');
}
};
const handleBatchAssign = () => {
assignUser.value = undefined;
assignVisible.value = true;
//
const handleDeleteSample = async (sampleId) => {
try {
const res = await surveySampleApi.deleteSample(sampleId);
message.success(res.msg || '移除成功');
loadSamples();
} catch (err) {
message.error('移除失败');
}
};
const handleSaveAssign = () => {
if (!assignUser.value) {
message.warning('请选择调查员');
//
const showImportModal = () => {
importText.value = '';
importVisible.value = true;
};
const handleDoImport = async () => {
if (!importText.value.trim()) {
message.warning('请粘贴包含数据的文本');
return;
}
const list = JSON.parse(localStorage.getItem('local_survey_samples') || '[]');
selectedRowKeys.value.forEach(key => {
const index = list.findIndex(item => item.id === key);
if (index !== -1) {
list[index].assignStatus = 1;
list[index].assignee = assignUser.value;
const tasks = JSON.parse(localStorage.getItem('local_survey_tasks') || '[]');
const sample = list[index];
const hasTask = tasks.some(t => t.phone === sample.phone && t.sjlyNo === sample.sjlyNo);
if (!hasTask) {
tasks.unshift({
id: 'T' + (Date.now() % 10000),
name: sample.name,
phone: sample.phone,
sjlyNo: sample.sjlyNo,
callCount: 0,
status: 0,
surveyTitle: sample.sjlyNo === '110' ? '110接处警群众满意度回访问卷' : '车驾管服务窗口满意度评议问卷',
assignTime: new Date().toLocaleString()
});
localStorage.setItem('local_survey_tasks', JSON.stringify(tasks));
}
}
});
try {
const res = await surveySampleApi.batchAddSamples({
groupId: selectedGroupId.value,
text: importText.value
});
message.success(res.msg || '批量导入成功');
importVisible.value = false;
loadSamples();
} catch (err) {
message.error('解析导入失败');
}
};
localStorage.setItem('local_survey_samples', JSON.stringify(list));
message.success(`成功为 [${assignUser.value}] 分配了 ${selectedRowKeys.value.length} 个回访样本!已自动关联生成其名下任务`);
selectedRowKeys.value = [];
assignVisible.value = false;
const onSearch = () => {
pagination.current = 1;
loadSamples();
};
const resetQuery = () => {
queryForm.name = '';
queryForm.phone = '';
pagination.current = 1;
loadSamples();
};
onMounted(() => {
onMounted(async () => {
await loadGroups();
loadSamples();
});
</script>
@ -252,5 +453,91 @@ onMounted(() => {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.02);
padding: 24px;
border: 1px solid #f0f0f0;
min-height: calc(100vh - 120px);
}
.group-card {
padding: 18px;
}
.card-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px dashed #f0f0f0;
}
.card-title {
font-size: 15px;
font-weight: 700;
color: #262626;
}
.group-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.group-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
background: #fafafa;
}
.group-item:hover {
background: #f0f5ff;
}
.group-item.active {
background: #e6f7ff;
color: #1890ff;
}
.group-info {
display: flex;
align-items: center;
gap: 8px;
overflow: hidden;
}
.group-icon {
font-size: 16px;
color: #8c8c8c;
}
.group-icon.active-icon {
color: #1890ff;
}
.group-name {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 120px;
}
.delete-icon {
font-size: 14px;
color: #ff4d4f;
opacity: 0;
transition: opacity 0.2s;
}
.group-item:hover .delete-icon {
opacity: 1;
}
.group-summary-tag {
font-size: 13px;
color: #595959;
}
</style>
Loading…
Cancel
Save