소스 검색

“提交项目”

wangqian 2 달 전
부모
커밋
e45c9c2d82
100개의 변경된 파일6344개의 추가작업 그리고 0개의 파일을 삭제
  1. 35 0
      government-demo-service/.gitignore
  2. 8 0
      government-demo-service/Dockerfile
  3. 8 0
      government-demo-service/Dockerfile-arm
  4. 93 0
      government-demo-service/pom.xml
  5. 24 0
      government-demo-service/src/main/java/cn/gov/customs/demo/GovernmentDemoApplication.java
  6. 64 0
      government-demo-service/src/main/java/cn/gov/customs/demo/government/config/GovernmentDataSourceConfig.java
  7. 29 0
      government-demo-service/src/main/java/cn/gov/customs/demo/government/constants/GovernmentResultCodeMessage.java
  8. 119 0
      government-demo-service/src/main/java/cn/gov/customs/demo/government/controller/GovernmentController.java
  9. 31 0
      government-demo-service/src/main/java/cn/gov/customs/demo/government/dao/GovernmentDao.java
  10. 155 0
      government-demo-service/src/main/java/cn/gov/customs/demo/government/json/DemoParentProcess.json
  11. 117 0
      government-demo-service/src/main/java/cn/gov/customs/demo/government/json/DemoSimpleProcess.json
  12. 77 0
      government-demo-service/src/main/java/cn/gov/customs/demo/government/json/DeomChildProcess.json
  13. 44 0
      government-demo-service/src/main/java/cn/gov/customs/demo/government/mock/MockUserAspect.java
  14. 108 0
      government-demo-service/src/main/java/cn/gov/customs/demo/government/mock/MockUserController.java
  15. 21 0
      government-demo-service/src/main/java/cn/gov/customs/demo/government/mock/MockUserHelper.java
  16. 20 0
      government-demo-service/src/main/java/cn/gov/customs/demo/government/pojo/GovernmentInfo.java
  17. 26 0
      government-demo-service/src/main/java/cn/gov/customs/demo/government/pojo/GovernmentQueryInfo.java
  18. 24 0
      government-demo-service/src/main/java/cn/gov/customs/demo/government/pojo/UserTaskQueryInfo.java
  19. 21 0
      government-demo-service/src/main/java/cn/gov/customs/demo/government/service/GovernmentService.java
  20. 96 0
      government-demo-service/src/main/java/cn/gov/customs/demo/government/service/GovernmentServiceImpl.java
  21. 14 0
      government-demo-service/src/main/java/cn/gov/customs/demo/government/service/GovernmentWorkflowEngineService.java
  22. 26 0
      government-demo-service/src/main/java/cn/gov/customs/demo/government/service/GovernmentWorkflowEngineServiceImpl.java
  23. 164 0
      government-demo-service/src/main/resources/application.yml
  24. 75 0
      government-demo-service/src/main/resources/mapper/government/GovernmentMapper.xml
  25. 78 0
      government-demo-web/.gitignore
  26. 8 0
      government-demo-web/.prettierrc.json
  27. 7 0
      government-demo-web/.vscode/extensions.json
  28. 20 0
      government-demo-web/Dockerfile
  29. 12 0
      government-demo-web/Dockerfile-arm
  30. 123 0
      government-demo-web/README.md
  31. BIN
      government-demo-web/cacp/icon/svg-icons-0.1.2.tgz
  32. BIN
      government-demo-web/cacp/ui/ui-0.3.17.tgz
  33. 1 0
      government-demo-web/env.d.ts
  34. 19 0
      government-demo-web/eslint.config.js
  35. 14 0
      government-demo-web/index.html
  36. 73 0
      government-demo-web/nginx.conf
  37. 1 0
      government-demo-web/node_version
  38. 69 0
      government-demo-web/package.json
  39. 10 0
      government-demo-web/public/config.js
  40. BIN
      government-demo-web/public/favicon.ico
  41. 72 0
      government-demo-web/src/App.vue
  42. 56 0
      government-demo-web/src/apis/accessory.ts
  43. 10 0
      government-demo-web/src/apis/authority.ts
  44. 10 0
      government-demo-web/src/apis/frame.ts
  45. 25 0
      government-demo-web/src/apis/government.ts
  46. 23 0
      government-demo-web/src/apis/mock.ts
  47. 78 0
      government-demo-web/src/apis/workflow/definition.ts
  48. 129 0
      government-demo-web/src/apis/workflow/engine.ts
  49. 54 0
      government-demo-web/src/apis/workflow/runtime.ts
  50. 36 0
      government-demo-web/src/assets/base.less
  51. BIN
      government-demo-web/src/assets/images/404.png
  52. BIN
      government-demo-web/src/assets/images/left-menu-bg.png
  53. 12 0
      government-demo-web/src/assets/main.less
  54. 154 0
      government-demo-web/src/components/accessory/AccessoryItem.vue
  55. 272 0
      government-demo-web/src/components/accessory/AccessoryUploader.vue
  56. 69 0
      government-demo-web/src/components/accessory/UploadFileItem.vue
  57. 14 0
      government-demo-web/src/components/accessory/constants.ts
  58. 40 0
      government-demo-web/src/components/editTable/EditTableColumn.vue
  59. 5 0
      government-demo-web/src/components/editTable/index.ts
  60. 23 0
      government-demo-web/src/components/editTable/registerComp.ts
  61. 9 0
      government-demo-web/src/components/editTable/type.ts
  62. 85 0
      government-demo-web/src/components/workflow/ProcessComments.vue
  63. 478 0
      government-demo-web/src/components/workflow/ProcessOperation.vue
  64. 117 0
      government-demo-web/src/components/workflow/ProcessOperationRoute.vue
  65. 56 0
      government-demo-web/src/components/workflow/ProcessTraceContainer.vue
  66. 70 0
      government-demo-web/src/components/workflow/ProcessTraceDetail.vue
  67. 115 0
      government-demo-web/src/components/workflow/ProcessTraceList.vue
  68. 18 0
      government-demo-web/src/config.ts
  69. 8 0
      government-demo-web/src/directives/index.ts
  70. 35 0
      government-demo-web/src/directives/permission.ts
  71. 0 0
      government-demo-web/src/index.d.ts
  72. 34 0
      government-demo-web/src/main.ts
  73. 10 0
      government-demo-web/src/plugins/icon.ts
  74. 1 0
      government-demo-web/src/plugins/index.ts
  75. 75 0
      government-demo-web/src/router/app-routers.ts
  76. 83 0
      government-demo-web/src/router/index.ts
  77. 42 0
      government-demo-web/src/stores/app-stores.ts
  78. 55 0
      government-demo-web/src/stores/core.ts
  79. 48 0
      government-demo-web/src/stores/designer-stores.ts
  80. 4 0
      government-demo-web/src/stores/index.ts
  81. 20 0
      government-demo-web/src/stores/pinia.ts
  82. 43 0
      government-demo-web/src/types/government.ts
  83. 27 0
      government-demo-web/src/types/process.ts
  84. 10 0
      government-demo-web/src/utils/authhelper.ts
  85. 16 0
      government-demo-web/src/utils/frame.ts
  86. 97 0
      government-demo-web/src/utils/http.ts
  87. 6 0
      government-demo-web/src/utils/nanoid.ts
  88. 114 0
      government-demo-web/src/utils/request.ts
  89. 25 0
      government-demo-web/src/views/ErrorView.vue
  90. 12 0
      government-demo-web/src/views/HomeView.vue
  91. 33 0
      government-demo-web/src/views/ResultView.vue
  92. 517 0
      government-demo-web/src/views/designer/ProcessDesignContainer.vue
  93. 76 0
      government-demo-web/src/views/designer/ProcessDesignFlowProperty.vue
  94. 43 0
      government-demo-web/src/views/designer/ProcessDesignJson.vue
  95. 259 0
      government-demo-web/src/views/designer/ProcessDesignNodeProperty.vue
  96. 72 0
      government-demo-web/src/views/designer/ProcessDesignProcessProperty.vue
  97. 73 0
      government-demo-web/src/views/designer/ProcessDesignProperty.vue
  98. 46 0
      government-demo-web/src/views/designer/useEditTable.ts
  99. 474 0
      government-demo-web/src/views/designer/utils.ts
  100. 122 0
      government-demo-web/src/views/government/GovernmentDetailOper.vue

+ 35 - 0
government-demo-service/.gitignore

@@ -0,0 +1,35 @@
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+# https://github.com/takari/maven-wrapper#usage-without-binary-jar
+.mvn/wrapper/maven-wrapper.jar
+.idea/
+.flattened-pom.xml
+# Compiled class file
+*.class
+# Log file
+*.log
+# BlueJ files
+*.ctxt
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.rar
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+.classpath
+.metadata
+.settings
+.project
+*.iml

+ 8 - 0
government-demo-service/Dockerfile

@@ -0,0 +1,8 @@
+FROM registry.cacp-test.hg.cn:31104/haiguan_base/openjdk:8u312-jdk-slim-hg-tsf-1.46
+
+ADD ./target/*.jar /app/app.jar
+RUN mkdir -p /app/servlet/logs
+ENV TRACE_AGENT="-javaagent:/app/opentelemetry-javaagent.jar -Dotel.javaagent.extensions=/app/femas-trace-opentelemetry.jar -Dotel.traces.exporter=none -Dotel.propagators=b3,b3multi"
+CMD ["sh", "-c", "cd /app; java ${JAVA_OPTS} ${TRACE_AGENT} -Djava.security.egd=file:/dev/./urandom -jar /app/app.jar ${RUN_ARGS}"]
+
+EXPOSE 15000

+ 8 - 0
government-demo-service/Dockerfile-arm

@@ -0,0 +1,8 @@
+FROM 10.100.23.30:31104/tsf_base/openjdk:8u312-jdk-slim-hg-tsf-1.46
+
+ADD ./target/*.jar /app/app.jar
+RUN mkdir -p /app/servlet/logs
+ENV TRACE_AGENT="-javaagent:/app/opentelemetry-javaagent.jar -Dotel.javaagent.extensions=/app/femas-trace-opentelemetry.jar -Dotel.traces.exporter=none -Dotel.propagators=b3,b3multi"
+CMD ["sh", "-c", "cd /app; java ${JAVA_OPTS} ${TRACE_AGENT} -Djava.security.egd=file:/dev/./urandom -jar /app/app.jar ${RUN_ARGS}"]
+
+EXPOSE 15000

+ 93 - 0
government-demo-service/pom.xml

@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>cacp-spring-boot-parent</artifactId>
+        <groupId>cn.gov.customs.cacp</groupId>
+        <version>2024.0.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>cn.gov.customs.demo</groupId>
+    <artifactId>government-demo-service</artifactId>
+    <version>1.0-SNAPSHOT</version>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.gov.customs.cacp</groupId>
+            <artifactId>cacp-service-spring-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.gov.customs.cacp</groupId>
+            <artifactId>cacp-datasource-spring-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.gov.customs.cacp</groupId>
+            <artifactId>cacp-audit-sdk</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.gov.customs.cacp</groupId>
+            <artifactId>cacp-auth-sdk</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.gov.customs.cacp</groupId>
+            <artifactId>cacp-hgid-sdk</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.gov.customs.cacp</groupId>
+            <artifactId>cacp-frame-api-sdk</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.gov.customs.cacp</groupId>
+            <artifactId>cacp-enhance-accessory-sdk</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.gov.customs.cacp</groupId>
+            <artifactId>cacp-enhance-workflow-full-sdk</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
+        <!-- dm -->
+        <dependency>
+            <groupId>com.dameng</groupId>
+            <artifactId>DmJdbcDriver18</artifactId>
+        </dependency>
+        <!-- swagger-ui -->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-ui</artifactId>
+            <version>2.10.5</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+    <distributionManagement>
+        <repository>
+            <id>host-maven-snapshot</id>   <!-- id的名字可以任意取,但是在setting文件中的属性<server>的ID与这里一致 -->
+            <name>host-maven-snapshot</name>
+            <url>http://43.137.18.189:8881/repository/host-maven-snapshot/</url>
+        </repository>
+        <snapshotRepository>
+            <id>host-maven-public</id>
+            <name>host-maven-public</name>
+            <url>http://43.137.18.189:8881/repository/host-maven-public/</url>
+        </snapshotRepository>
+    </distributionManagement>
+</project>

+ 24 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/GovernmentDemoApplication.java

@@ -0,0 +1,24 @@
+package cn.gov.customs.demo;
+
+import static org.springframework.boot.SpringApplication.run;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.tsf.annotation.EnableTsf;
+
+/**
+ * 启动类
+ *
+ * @author chuxianming
+ * @since 2024-06-14 11:16:12
+ */
+@EnableTsf
+@SpringBootApplication
+@EnableFeignClients(basePackages = "cn.gov.customs")
+@ComponentScan(basePackages = "cn.gov.customs")
+public class GovernmentDemoApplication {
+  public static void main(String[] args) {
+    run(GovernmentDemoApplication.class, args);
+  }
+}

+ 64 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/government/config/GovernmentDataSourceConfig.java

@@ -0,0 +1,64 @@
+package cn.gov.customs.demo.government.config;
+
+import javax.annotation.Resource;
+import javax.sql.DataSource;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.SqlSessionFactoryBean;
+import org.mybatis.spring.SqlSessionTemplate;
+import org.mybatis.spring.annotation.MapperScan;
+import org.mybatis.spring.boot.autoconfigure.MybatisProperties;
+import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+/**
+ * DateSourceConfig content
+ *
+ * @author chuxianming
+ * @date 2024-05-24 10:18:12
+ */
+@Configuration
+@MapperScan(
+    basePackages = {"cn.gov.customs.demo.government.dao"},
+    annotationClass = Mapper.class,
+    sqlSessionFactoryRef = "governmentSqlSessionFactory",
+    sqlSessionTemplateRef = "governmentSqlTemplate")
+@EnableTransactionManagement
+public class GovernmentDataSourceConfig {
+
+  @Resource
+  private MybatisProperties properties;
+  @Resource
+  @Qualifier("GOVERNMENTDataSource")
+  private DataSource dataSource;
+
+  @Bean(name = "governmentSqlSessionFactory")
+  public SqlSessionFactory governmentSqlSessionFactory() throws Exception {
+    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
+    bean.setConfiguration(this.properties.getConfiguration());
+    bean.setVfs(SpringBootVFS.class);
+    bean.setDataSource(dataSource);
+    bean.setMapperLocations(
+        new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/government/*.xml"));
+    bean.setTypeAliasesPackage("cn.gov.customs.demo.government");
+    return bean.getObject();
+  }
+
+  @Bean(name = "governmentSqlTemplate")
+  public SqlSessionTemplate governmentSqlTemplate(
+      @Qualifier("governmentSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
+    return new SqlSessionTemplate(sqlSessionFactory);
+  }
+
+  @Bean(name="governmentTransactionManager")
+  public PlatformTransactionManager governmentTransactionManager() {
+    return new DataSourceTransactionManager(this.dataSource);
+  }
+}

+ 29 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/government/constants/GovernmentResultCodeMessage.java

@@ -0,0 +1,29 @@
+package cn.gov.customs.demo.government.constants;
+
+import cn.gov.customs.cacp.sdks.core.result.ResultCodeMessage;
+import lombok.Getter;
+
+/**
+ * todo
+ *
+ * @author sunxuewen
+ * @date 2024/7/29 10:39
+ */
+@Getter
+public enum GovernmentResultCodeMessage implements ResultCodeMessage {
+  /**
+   * 10100, "通用异常"
+   */
+  COMMON_ERROR("ZWCJ-COMMON-ERROR", "通用异常"),
+
+  NO_PERMISSION("ZWCJ-NO-PERMISSION", "用户没有权限使用该功能"),
+  USER_NOTEXISTS("ZWCJ-USER-NOTEXISTS", "授权认证中未找到用户");
+
+  private final String code;
+  private final String message;
+
+  GovernmentResultCodeMessage(String code, String message) {
+    this.code = code;
+    this.message = message;
+  }
+}

+ 119 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/government/controller/GovernmentController.java

@@ -0,0 +1,119 @@
+package cn.gov.customs.demo.government.controller;
+
+import cn.gov.customs.cacp.enhance.workflow.controller.BaseWorkflowController;
+import cn.gov.customs.cacp.enhance.workflow.pojo.runtime.RuntimeOgu;
+import cn.gov.customs.cacp.enhance.workflow.pojo.runtime.RuntimeTask;
+import cn.gov.customs.cacp.enhance.workflow.runtime.WorkflowRuntimeContext;
+import cn.gov.customs.cacp.enhance.workflow.service.ProcessRuntimeService;
+import cn.gov.customs.cacp.sdks.core.config.CacpAppProperties;
+import cn.gov.customs.cacp.sdks.core.result.Result;
+import cn.gov.customs.cacp.sdks.core.user.annotation.LogonUser;
+import cn.gov.customs.cacp.sdks.core.user.pojo.CacpLogonUser;
+import cn.gov.customs.cacp.sdks.hgid.HgidGenerator;
+import cn.gov.customs.demo.government.pojo.GovernmentInfo;
+import cn.gov.customs.demo.government.pojo.GovernmentQueryInfo;
+import cn.gov.customs.demo.government.pojo.UserTaskQueryInfo;
+import cn.gov.customs.demo.government.service.GovernmentService;
+import cn.gov.customs.demo.government.service.GovernmentWorkflowEngineService;
+import com.github.pagehelper.PageInfo;
+import java.util.Map;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * TODO
+ *
+ * @author sunxuewen
+ * @since 2024/8/15 11:39
+ */
+@RestController
+@RequestMapping("/government")
+public class GovernmentController extends BaseWorkflowController<GovernmentInfo> {
+
+  public GovernmentController(CacpAppProperties appProperties, HgidGenerator idHelper, GovernmentService formService, ProcessRuntimeService runtimeService, GovernmentWorkflowEngineService engineService) {
+    super(appProperties, idHelper, formService, runtimeService, engineService);
+  }
+
+  @GetMapping("/get")
+  public Result<GovernmentInfo> get(@RequestParam String formId) {
+    String appCode = this.appProperties.getAuthAppCode();
+    GovernmentInfo info = this.formService.get(appCode, formId);
+    return Result.success(info);
+  }
+
+  @PostMapping("/get-page-list")
+  public Result<PageInfo<GovernmentInfo>> getPageList(@RequestBody GovernmentQueryInfo query) {
+    GovernmentService service = (GovernmentService) this.formService;
+    PageInfo<GovernmentInfo> result = service.getPageList(query);
+    return Result.success(result);
+  }
+
+  @PostMapping("/get-user-task-list-by-page")
+  public Result<PageInfo<RuntimeTask>> getUserTaskListByPage(@RequestBody UserTaskQueryInfo query, @LogonUser CacpLogonUser logonUser) {
+    String appCode = this.appProperties.getAppCode();
+    RuntimeOgu user = RuntimeOgu.from(logonUser);
+    GovernmentService service = (GovernmentService) this.formService;
+    PageInfo<RuntimeTask> result =  service.getUserTaskListByPage(appCode, user.getParentId(), user.getOguId(), query);
+    return Result.success(result);
+  }
+
+  @Override
+  protected void innerBeforeCreate(GovernmentInfo info, WorkflowRuntimeContext context) {
+
+  }
+
+  @Override
+  protected void innerAfterCreate(GovernmentInfo info, WorkflowRuntimeContext context) {
+
+  }
+
+  @Override
+  protected void innerAfterStart(GovernmentInfo info, WorkflowRuntimeContext context, Map<String, Object> fields) {
+
+  }
+
+  @Override
+  protected void innerAfterSave(GovernmentInfo info, WorkflowRuntimeContext context) {
+
+  }
+
+  @Override
+  protected void innerAfterAgree(GovernmentInfo info, boolean status, WorkflowRuntimeContext context, Map<String, Object> fields) {
+
+  }
+
+  @Override
+  protected void innerAfterBack(GovernmentInfo info, WorkflowRuntimeContext context, Map<String, Object> fields) {
+
+  }
+
+  @Override
+  protected void innerAfterRevoke(GovernmentInfo info, WorkflowRuntimeContext context) {
+
+  }
+
+  @Override
+  protected void innerAfterReject(GovernmentInfo info, WorkflowRuntimeContext context, Map<String, Object> fields) {
+
+  }
+
+  @Override
+  protected void innerAfterSuspend(GovernmentInfo info, WorkflowRuntimeContext context, Map<String, Object> fields) {
+
+  }
+
+  @Override
+  protected void innerAfterResume(GovernmentInfo info) {
+
+  }
+
+  @Override
+  protected void innerAfterRedistribution(GovernmentInfo info, WorkflowRuntimeContext context) {
+
+  }
+
+  @Override
+  protected void innerAfterDelete(GovernmentInfo info) {
+
+  }
+
+}

+ 31 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/government/dao/GovernmentDao.java

@@ -0,0 +1,31 @@
+package cn.gov.customs.demo.government.dao;
+
+import java.util.List;
+import java.util.Map;
+
+import cn.gov.customs.demo.government.pojo.GovernmentInfo;
+import cn.gov.customs.demo.government.pojo.GovernmentQueryInfo;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * TODO
+ *
+ * @author sunxuewen
+ * @since 2024/8/15 10:20
+ */
+@Mapper
+public interface GovernmentDao {
+
+  int insert(GovernmentInfo info);
+
+  int update(GovernmentInfo info);
+
+  int updateWithFields(@Param("formId") String formId, @Param("fields") Map<String, Object> fields);
+
+  int delete(@Param("formId") String formId);
+
+  GovernmentInfo get(@Param("formId") String formId);
+
+  List<GovernmentInfo> getList(GovernmentQueryInfo query);
+}

+ 155 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/government/json/DemoParentProcess.json

@@ -0,0 +1,155 @@
+{
+  "code": "DemoParentProcess",
+  "name": "父流程",
+  "branch": false,
+  "expireDays": 10,
+  "description": "父流程描述",
+  "nodes": [
+    {
+      "type": "START",
+      "code": "ISSUE",
+      "name": "拟稿",
+      "routeOptional": false
+    },
+    {
+      "type": "APPROVE",
+      "code": "CHECK",
+      "name": "一级审批",
+      "backCode": "ISSUE",
+      "routeOptional": true,
+      "completeStrategy": {
+        "strategy": "ONE"
+      },
+      "assignment": {
+        "type": "ROLE_LEVEL",
+        "roleCode": "CHECK",
+        "deptLevel": 1
+      }
+    },
+    {
+      "type": "APPROVE",
+      "code": "CHECK2",
+      "name": "二级审批",
+      "backCode": "ISSUE",
+      "routeOptional": false,
+      "completeStrategy": {
+        "strategy": "ONE"
+      },
+      "assignment": {
+        "type": "ROLE_LEVEL",
+        "roleCode": "CHECK2",
+        "deptLevel": 2
+      }
+    },
+    {
+      "type": "APPROVE",
+      "code": "LINK",
+      "name": "收发",
+      "backCode": "ISSUE",
+      "routeOptional": false,
+      "completeStrategy": {
+        "strategy": "ONE"
+      },
+      "assignment": {
+        "type": "ROLE_LEVEL",
+        "roleCode": "LINK",
+        "deptLevel": 2
+      }
+    },
+    {
+      "type": "BRANCH",
+      "code": "DISTRIBUTE",
+      "name": "办理",
+      "branchCode": "DemoChildProcess",
+      "dealType": "HUIQIAN",
+      "completeStrategy": {
+        "strategy": "BRANCH"
+      },
+      "assignment": {
+        "type": "BRANCH_DEPT",
+        "branchDepts": "#{['branchDepts']}"
+      }
+    },
+    {
+      "type": "APPROVE",
+      "code": "SUM",
+      "name": "汇总",
+      "backCode": "LINK",
+      "routeOptional": false,
+      "completeStrategy": {
+        "strategy": "ONE"
+      },
+      "assignment": {
+        "type": "BRANCH_CREATOR"
+      }
+    },
+    {
+      "type": "APPROVE",
+      "code": "FEEDBACK",
+      "name": "反馈",
+      "routeOptional": false,
+      "completeStrategy": {
+        "strategy": "ONE"
+      },
+      "assignment": {
+        "type": "ISSUER"
+      }
+    },
+    {
+      "type": "END",
+      "code": "END",
+      "name": "办结"
+    }
+  ],
+  "flows": [
+    {
+      "type": "NORMAL",
+      "code": "f1",
+      "name": "f1",
+      "source": "ISSUE",
+      "target": "CHECK"
+    },
+    {
+      "type": "NORMAL",
+      "code": "f2",
+      "name": "f2",
+      "source": "CHECK",
+      "target": "CHECK2"
+    },
+    {
+      "type": "NORMAL",
+      "code": "f3",
+      "name": "f3",
+      "source": "CHECK2",
+      "target": "LINK"
+    },
+    {
+      "type": "NORMAL",
+      "code": "f4",
+      "name": "f4",
+      "source": "LINK",
+      "target": "DISTRIBUTE"
+    },
+    {
+      "type": "NORMAL",
+      "code": "f5",
+      "name": "f5",
+      "source": "DISTRIBUTE",
+      "target": "SUM"
+    },
+    {
+      "type": "NORMAL",
+      "code": "f6",
+      "name": "f6",
+      "source": "SUM",
+      "target": "FEEDBACK"
+    },
+    {
+      "type": "NORMAL",
+      "code": "f7",
+      "name": "f7",
+      "source": "FEEDBACK",
+      "target": "END"
+    }
+  ]
+}

+ 117 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/government/json/DemoSimpleProcess.json

@@ -0,0 +1,117 @@
+{
+  "code": "DemoSimpleProcess",
+  "name": "简单流程",
+  "branch": false,
+  "expireDays": 10,
+  "description": "简单流程描述",
+  "nodes": [
+    {
+      "type": "START",
+      "code": "ISSUE",
+      "name": "拟稿",
+      "routeOptional": false
+    },
+    {
+      "type": "APPROVE",
+      "code": "CHECK",
+      "name": "一级审批",
+      "backCode": "ISSUE",
+      "keepTrace": false,
+      "routeOptional": false,
+      "completeStrategy": {
+        "strategy": "ONE"
+      },
+      "assignment": {
+        "type": "ROLE_LEVEL",
+        "roleCode": "CHECK",
+        "deptLevel": 1
+      }
+    },
+    {
+      "type": "APPROVE",
+      "code": "CHECK2",
+      "name": "二级审批",
+      "backCode": "ISSUE",
+      "keepTrace": true,
+      "routeOptional": false,
+      "completeStrategy": {
+        "strategy": "ALL"
+      },
+      "assignment": {
+        "type": "ROLE_LEVEL",
+        "roleCode": "CHECK2",
+        "deptLevel": 2
+      }
+    },
+    {
+      "type": "APPROVE",
+      "code": "DEPOSIT",
+      "name": "办理",
+      "backCode": "ISSUE",
+      "routeOptional": false,
+      "completeStrategy": {
+        "strategy": "ONE"
+      },
+      "assignment": {
+        "type": "ROLE_LEVEL",
+        "roleCode": "LINK",
+        "deptLevel": 2
+      }
+    },
+    {
+      "type": "APPROVE",
+      "code": "FEEDBACK",
+      "name": "反馈",
+      "backCode": "ISSUE",
+      "routeOptional": false,
+      "completeStrategy": {
+        "strategy": "ONE"
+      },
+      "assignment": {
+        "type": "ISSUER"
+      }
+    },
+    {
+      "type": "END",
+      "code": "END",
+      "name": "办结"
+    }
+  ],
+  "flows": [
+    {
+      "type": "NORMAL",
+      "code": "f1",
+      "name": "f1",
+      "source": "ISSUE",
+      "target": "CHECK"
+    },
+    {
+      "type": "NORMAL",
+      "code": "f2",
+      "name": "f2",
+      "source": "CHECK",
+      "target": "CHECK2"
+    },
+    {
+      "type": "NORMAL",
+      "code": "f3",
+      "name": "f3",
+      "source": "CHECK2",
+      "target": "DEPOSIT"
+    },
+    {
+      "type": "NORMAL",
+      "code": "f4",
+      "name": "f4",
+      "source": "DEPOSIT",
+      "target": "FEEDBACK"
+    },
+    {
+      "type": "NORMAL",
+      "code": "f5",
+      "name": "f5",
+      "source": "FEEDBACK",
+      "target": "END"
+    }
+  ]
+}

+ 77 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/government/json/DeomChildProcess.json

@@ -0,0 +1,77 @@
+{
+  "code": "DemoChildProcess",
+  "name": "子流程",
+  "branch": false,
+  "expireDays": 10,
+  "description": "子流程描述",
+  "nodes": [
+    {
+      "type": "START",
+      "code": "LINK",
+      "name": "接收",
+      "assignment": {
+        "type": "ROLE_DEPT",
+        "roleCode": "LINK",
+        "deptPaths": "#{['BRANCH_ITEM']}"
+      }
+    },
+    {
+      "type": "APPROVE",
+      "code": "CHECK",
+      "name": "审批",
+      "backCode": "#LAST#",
+      "routeOptional": false,
+      "completeStrategy": {
+        "strategy": "ONE"
+      },
+      "assignment": {
+        "type": "ROLE_LEVEL",
+        "roleCode": "CHECK",
+        "deptLevel": 2
+      }
+    },
+    {
+      "type": "APPROVE",
+      "code": "DEAL",
+      "name": "办理",
+      "backCode": "#LAST#",
+      "routeOptional": false,
+      "completeStrategy": {
+        "strategy": "ONE"
+      },
+      "assignment": {
+        "type": "ROLE_LEVEL",
+        "roleCode": "DEPOSIT",
+        "deptLevel": 1
+      }
+    },
+    {
+      "type": "END",
+      "code": "END",
+      "name": "办结"
+    }
+  ],
+  "flows": [
+    {
+      "type": "NORMAL",
+      "code": "f1",
+      "name": "f1",
+      "source": "LINK",
+      "target": "CHECK"
+    },
+    {
+      "type": "NORMAL",
+      "code": "f2",
+      "name": "f2",
+      "source": "CHECK",
+      "target": "DEAL"
+    },
+    {
+      "type": "NORMAL",
+      "code": "f3",
+      "name": "f3",
+      "source": "DEAL",
+      "target": "END"
+    }
+  ]
+}

+ 44 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/government/mock/MockUserAspect.java

@@ -0,0 +1,44 @@
+package cn.gov.customs.demo.government.mock;
+
+import cn.gov.customs.cacp.sdks.core.user.annotation.LogonUser;
+import cn.gov.customs.cacp.sdks.core.user.pojo.CacpLogonUser;
+import java.lang.reflect.Parameter;
+import java.util.Objects;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.stereotype.Component;
+
+/**
+ * H4A切面
+ *
+ * @author sunxuewen
+ * @since 2024/6/27 15:57
+ */
+@Aspect
+@Component
+public class MockUserAspect {
+
+  // execution(public * cn.gov.customs..*.*(..) && @args(cn.gov.customs.cacp.sdks.core.user.annotation.LogonUser)
+  // @Pointcut("@target(org.springframework.web.bind.annotation.RestController)")
+  @Pointcut("execution(public * cn.gov.customs..*Controller.*(..))")
+  public void aspect() {}
+
+  @Around("aspect()")
+  public Object execute(ProceedingJoinPoint point) throws Throwable {
+    CacpLogonUser mockUser = MockUserHelper.getMockUser();
+    if (Objects.isNull(mockUser)) {
+      return point.proceed();
+    }
+
+    Parameter[] parameters = ((MethodSignature) point.getSignature()).getMethod().getParameters();
+    for (int i = 0; i < parameters.length; i++) {
+      if (parameters[i].isAnnotationPresent(LogonUser.class)) {
+        point.getArgs()[i] = mockUser;
+      }
+    }
+    return point.proceed(point.getArgs());
+  }
+}

+ 108 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/government/mock/MockUserController.java

@@ -0,0 +1,108 @@
+package cn.gov.customs.demo.government.mock;
+
+import cn.gov.customs.cacp.sdks.auth.AuthAdapter;
+import cn.gov.customs.cacp.sdks.auth.CacpUser;
+import cn.gov.customs.cacp.sdks.auth.h4a.pojo.H4AUser;
+import cn.gov.customs.cacp.sdks.core.result.Result;
+import cn.gov.customs.cacp.sdks.core.user.annotation.LogonUser;
+import cn.gov.customs.cacp.sdks.core.user.pojo.CacpLogonUser;
+import cn.gov.customs.cacp.sdks.frame.pojo.FrameUserInfo;
+import cn.gov.customs.cacp.sdks.frame.pojo.ThemeInfo;
+import cn.gov.customs.demo.government.constants.GovernmentResultCodeMessage;
+import com.google.common.collect.Lists;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @description TODO
+ * @author sunxuewen
+ * @since 2024/10/29 上午10:10
+ */
+@RestController
+@RequestMapping("/mock")
+public class MockUserController {
+
+  private final AuthAdapter adapter;
+
+  public MockUserController(AuthAdapter adapter) {
+    this.adapter = adapter;
+  }
+
+  @GetMapping("/get-frame-user")
+  public Result<FrameUserInfo> getFrameUser(@LogonUser CacpLogonUser logonUser) {
+    if (logonUser == null) {
+      return Result.success(null);
+    }
+    CacpUser cacpUser = this.adapter.getUser(logonUser.getViewCode(), logonUser.getParentId(), logonUser.getUserId());
+    if (Objects.isNull(cacpUser)) {
+      return Result.fail(GovernmentResultCodeMessage.NO_PERMISSION, "用户为" + logonUser.getFullPathName());
+    }
+
+    FrameUserInfo frameUser = new FrameUserInfo();
+    frameUser.setUserId(logonUser.getUserId());
+    frameUser.setParentId(logonUser.getParentId());
+    frameUser.setUserName(cacpUser.getOguName());
+    frameUser.setFullPathName(cacpUser.getFullPathName());
+    frameUser.setPersonId(cacpUser.getPersonId());
+    frameUser.setCustomsCode(logonUser.getCustomsCode());
+    frameUser.setStatus(cacpUser.getStatus());
+    frameUser.setSortOrder(cacpUser.getSortOrder());
+    frameUser.setEmail(cacpUser.getEmail());
+    frameUser.setMobile(cacpUser.getMobile());
+
+    H4AUser h4aUser = (H4AUser) cacpUser;
+    frameUser.setRankCode(h4aUser.getRankCode());
+    frameUser.setRankName(h4aUser.getRankName());
+    frameUser.setSideline(h4aUser.isSideline());
+
+    frameUser.setIpAddress(logonUser.getIpAddress());
+    frameUser.setViewCode(logonUser.getViewCode());
+    frameUser.setLogonType(logonUser.getLogonType());
+    frameUser.setLogonTime(logonUser.getLogonTime());
+
+//    frameUser.setThemeCode("DEFAULT");
+    ThemeInfo themeInfo = new ThemeInfo();
+    themeInfo.setThemeCode("DEFAULT");
+//    frameUser.setTheme(themeInfo);
+//    frameUser.setDelegates(Lists.newArrayList());
+
+    return Result.success(frameUser);
+  }
+
+  @GetMapping("/set-mock-user")
+  public Result<Boolean> setMockUser(@RequestParam String viewCode, @RequestParam String fullPathName) {
+    CacpUser cacpUser = this.adapter.getUserByPath(viewCode, fullPathName);
+    if (Objects.isNull(cacpUser)) {
+      return Result.fail(GovernmentResultCodeMessage.NO_PERMISSION, "用户为" + fullPathName);
+    }
+
+    CacpLogonUser logonUser = new CacpLogonUser();
+    logonUser.setUserId(cacpUser.getOguId());
+    logonUser.setUserName(cacpUser.getOguName());
+    logonUser.setParentId(cacpUser.getParentId());
+    logonUser.setFullPathName(cacpUser.getFullPathName());
+    logonUser.setPersonId(cacpUser.getPersonId());
+    logonUser.setCustomsCode(cacpUser.getCustomsCode());
+    logonUser.setStatus("1");
+    logonUser.setIpAddress("mock");
+    logonUser.setViewCode(viewCode);
+    logonUser.setLogonType("mock");
+    logonUser.setLogonTime(LocalDateTime.now());
+    MockUserHelper.setMockUser(logonUser);
+
+    return Result.success(true);
+  }
+
+  @GetMapping("/clear-mock-user")
+  public Result<Boolean> clearMockUser() {
+    MockUserHelper.setMockUser(null);
+    return Result.success(true);
+  }
+
+}

+ 21 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/government/mock/MockUserHelper.java

@@ -0,0 +1,21 @@
+package cn.gov.customs.demo.government.mock;
+
+import cn.gov.customs.cacp.sdks.core.user.pojo.CacpLogonUser;
+
+/**
+ * @description TODO
+ * @author sunxuewen
+ * @since 2024/10/14 下午5:26
+ */
+public final class MockUserHelper {
+
+  private static CacpLogonUser mockUser = null;
+
+  public static void setMockUser(CacpLogonUser user) {
+    mockUser = user;
+  }
+
+  public static CacpLogonUser getMockUser() {
+    return mockUser;
+  }
+}

+ 20 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/government/pojo/GovernmentInfo.java

@@ -0,0 +1,20 @@
+package cn.gov.customs.demo.government.pojo;
+
+import cn.gov.customs.cacp.enhance.workflow.pojo.form.BaseWorkflowForm;
+import java.io.Serializable;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * TODO
+ *
+ * @author sunxuewen
+ * @since 2024/8/15 10:10
+ */
+@Getter
+@Setter
+public class GovernmentInfo extends BaseWorkflowForm implements Serializable {
+
+  private String formContent;
+
+}

+ 26 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/government/pojo/GovernmentQueryInfo.java

@@ -0,0 +1,26 @@
+package cn.gov.customs.demo.government.pojo;
+
+import java.io.Serializable;
+import java.time.LocalDate;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @description:TODO
+ * @auth:fangchaolei
+ * @create:2024/6/27
+ */
+@Getter
+@Setter
+public class GovernmentQueryInfo implements Serializable {
+
+    private String title;
+
+    private LocalDate startDate;
+
+    private LocalDate endDate;
+
+    private int pageSize;
+
+    private int pageIndex;
+}

+ 24 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/government/pojo/UserTaskQueryInfo.java

@@ -0,0 +1,24 @@
+package cn.gov.customs.demo.government.pojo;
+
+import java.io.Serializable;
+import java.time.LocalDate;
+
+import cn.gov.customs.cacp.enhance.workflow.constants.TaskType;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @description:TODO
+ * @auth:fangchaolei
+ * @create:2024/6/27
+ */
+@Getter
+@Setter
+public class UserTaskQueryInfo implements Serializable {
+
+    private TaskType type;
+
+    private int pageSize;
+
+    private int pageIndex;
+}

+ 21 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/government/service/GovernmentService.java

@@ -0,0 +1,21 @@
+package cn.gov.customs.demo.government.service;
+
+import cn.gov.customs.cacp.enhance.workflow.pojo.runtime.RuntimeTask;
+import cn.gov.customs.cacp.enhance.workflow.service.BaseWorkflowFormService;
+import cn.gov.customs.demo.government.pojo.GovernmentInfo;
+import cn.gov.customs.demo.government.pojo.GovernmentQueryInfo;
+import cn.gov.customs.demo.government.pojo.UserTaskQueryInfo;
+import com.github.pagehelper.PageInfo;
+
+/**
+ * TODO
+ *
+ * @author sunxuewen
+ * @since 2024/8/15 10:22
+ */
+public interface GovernmentService extends BaseWorkflowFormService<GovernmentInfo> {
+
+  PageInfo<GovernmentInfo> getPageList(GovernmentQueryInfo query);
+
+  PageInfo<RuntimeTask> getUserTaskListByPage(String appCode, String parentId, String userId, UserTaskQueryInfo query);
+}

+ 96 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/government/service/GovernmentServiceImpl.java

@@ -0,0 +1,96 @@
+package cn.gov.customs.demo.government.service;
+
+import cn.gov.customs.cacp.enhance.workflow.dao.RuntimeTaskDao;
+import cn.gov.customs.cacp.enhance.workflow.pojo.runtime.RuntimeTask;
+import cn.gov.customs.demo.government.dao.GovernmentDao;
+import cn.gov.customs.demo.government.pojo.GovernmentInfo;
+import cn.gov.customs.demo.government.pojo.GovernmentQueryInfo;
+import cn.gov.customs.demo.government.pojo.UserTaskQueryInfo;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.collections.MapUtils;
+import org.springframework.stereotype.Service;
+
+/**
+ * TODO
+ *
+ * @author sunxuewen
+ * @since 2024/8/15 10:26
+ */
+@Service
+@RequiredArgsConstructor
+public class GovernmentServiceImpl implements GovernmentService {
+
+  private final GovernmentDao dao;
+
+  private final RuntimeTaskDao taskMapper;
+
+  @Override
+  public GovernmentInfo create() {
+    return new GovernmentInfo();
+  }
+
+  @Override
+  public int insert(GovernmentInfo info) {
+    return this.dao.insert(info);
+  }
+
+  @Override
+  public int updateWithFields(String appCode, GovernmentInfo form, Map<String, Object> fields) {
+    if (MapUtils.isNotEmpty(fields)) {
+      return this.dao.updateWithFields(appCode, fields);
+    }
+    return 0;
+  }
+
+  @Override
+  public int update(GovernmentInfo info) {
+    return this.dao.update(info);
+  }
+
+//  @Override
+//  public int updateWithFields(String appCode, String formId, Map<String, Object> fields) {
+//    if (MapUtils.isNotEmpty(fields)) {
+//      return this.dao.updateWithFields(formId, fields);
+//    }
+//    return 0;
+//  }
+
+  @Override
+  public int delete(String appCode, String formId) {
+    return this.dao.delete(formId);
+  }
+
+  @Override
+  public PageInfo<GovernmentInfo> getPageList(GovernmentQueryInfo query) {
+    PageHelper.startPage(query.getPageIndex(), query.getPageSize());
+    List<GovernmentInfo> list = this.dao.getList(query);
+    return PageInfo.of(list);
+  }
+
+  @Override
+  public GovernmentInfo get(String appCode, String formId) {
+    return this.dao.get(formId);
+  }
+
+  @Override
+  public PageInfo<RuntimeTask> getUserTaskListByPage(String appCode, String parentId, String userId, UserTaskQueryInfo query) {
+    PageHelper.startPage(query.getPageIndex(), query.getPageSize());
+    return PageInfo.of(this.taskMapper.getListByUser(appCode, parentId, userId, query.getType()));
+  }
+
+  @Override
+  public boolean combine(GovernmentInfo info) {
+    GovernmentInfo governmentInfoinfo = this.get(info.getAppCode(), info.getFormId());
+    if (Objects.nonNull(governmentInfoinfo)) {
+      // 各自写合并内容
+      return true;
+    } else {
+      return false;
+    }
+  }
+}

+ 14 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/government/service/GovernmentWorkflowEngineService.java

@@ -0,0 +1,14 @@
+package cn.gov.customs.demo.government.service;
+
+import cn.gov.customs.cacp.enhance.workflow.service.BaseWorkflowEngineService;
+import cn.gov.customs.demo.government.pojo.GovernmentInfo;
+
+/**
+ * TODO
+ *
+ * @author sunxuewen
+ * @since 2024/8/15 16:36
+ */
+public interface GovernmentWorkflowEngineService extends BaseWorkflowEngineService<GovernmentInfo> {
+
+}

+ 26 - 0
government-demo-service/src/main/java/cn/gov/customs/demo/government/service/GovernmentWorkflowEngineServiceImpl.java

@@ -0,0 +1,26 @@
+package cn.gov.customs.demo.government.service;
+
+import cn.gov.customs.cacp.enhance.workflow.service.ProcessDefinitionService;
+import cn.gov.customs.cacp.enhance.workflow.service.ProcessRuntimeService;
+import cn.gov.customs.cacp.enhance.workflow.service.impl.BaseWorkflowEngineServiceImpl;
+import cn.gov.customs.cacp.sdks.auth.AuthAdapter;
+import cn.gov.customs.cacp.sdks.hgid.HgidGenerator;
+import cn.gov.customs.demo.government.pojo.GovernmentInfo;
+import org.springframework.stereotype.Service;
+
+/**
+ * TODO
+ *
+ * @author sunxuewen
+ * @since 2024/8/15 16:38
+ */
+
+@Service
+public class GovernmentWorkflowEngineServiceImpl extends BaseWorkflowEngineServiceImpl<GovernmentInfo> implements GovernmentWorkflowEngineService {
+
+  public GovernmentWorkflowEngineServiceImpl(HgidGenerator idHelper, AuthAdapter authAdapter,
+                                             ProcessRuntimeService runtimeService, ProcessDefinitionService definitionService) {
+    super(idHelper, authAdapter, runtimeService, definitionService);
+  }
+
+}

+ 164 - 0
government-demo-service/src/main/resources/application.yml

@@ -0,0 +1,164 @@
+server:
+  port: 18003
+
+spring:
+  application:
+    name: government-demo-service
+  cache:
+    type: caffeine
+  redis:
+ #   host: 10.200.67.105
+    host: 43.137.18.189
+    port: 6397
+    password: qaz!@#
+  servlet:
+    multipart:
+      max-file-size: 100MB
+      max-request-size: 1000MB
+  http:
+    encoding:
+      force: true
+      charset: UTF-8
+
+tsf:
+  swagger:
+    enabled: true #swagger-ui 开启状态,上线时建议关闭
+
+#第三方SDK配置
+mybatis:
+  configuration:
+    mapUnderscoreToCamelCase: true
+
+logging:
+  level:
+    org.apache.http.client: info
+    org.apache.http.headers: info
+    org.apache.http.impl: info
+    org.apache.http.wire: info
+
+#平台提供的其它SDK配置
+cacp:
+  core:
+    logonUser:
+      mock: true
+      mockUser:
+        userId: a1111111-1111-1111-1111-111111111111
+        userName: admin
+        parentId: 3c870cc2-5b5f-b0f3-4db8-ff98e7fb99c2
+        fullPathName: 中国海关\admin
+        personId: 2200123
+        customsCode: 0000
+        status: 1
+        ipAddress: 127.0.0.1
+        viewCode: CCIC_VIEW
+        logonType: forms
+  app:
+    appCode: GOV
+    authAppCode: ZWCJ
+    featureCode: GOV
+    publishType: service
+  #database config配置
+  database:
+    initType: fromYml
+    datasource:
+      ENHANCE:
+        driverClassName: dm.jdbc.driver.DmDriver
+        url: jdbc:dm://43.137.18.189:5326
+        username: SYSDBA
+        password: Dameng@2025
+        isPrimary: true
+      GOVERNMENT:
+#        driverClassName: dm.jdbc.driver.DmDriver
+#        url: jdbc:dm://10.100.21.127:5236
+#        username: CACP50USER
+#        password: CACP50USER
+        driverClassName: dm.jdbc.driver.DmDriver
+        url: jdbc:dm://43.137.18.189:5326
+        username: SYSDBA
+        password: Dameng@2025
+  cache:
+    enabled: true
+    caffeine:
+      cache1: maximumSize=510, expireAfterWrite=3601s, recordStats
+      first: maximumSize=500, expireAfterWrite=3600s, recordStats
+  hgid:
+    type: local
+  audit:
+    enabled: true
+  auth:
+    type: h4a # h4a, cus4a, proxy
+    log: true
+    cus4a:
+      #单点登录地址
+      authenticateUrl: http://cus4a-sso.dev-xc.com
+      #注销地址
+      logOffUrl: http://cus4a-sso.dev-xc.com/logout
+      #注销后跳转地址
+      logOffCallBackUrl: /api/getlogoffurl
+      #H4A应用名称
+      appID: ZWCJ
+      #要求H4A返回的用户登录名类型(forms返回表单登录时录入的登录名,hr返回海关员工号...等)
+      idAuthenticationMode: forms
+      #认证类型
+      defaultAuthenticationMode: FormsAuthentication
+      #默认视角
+      defaultBaseView: CCIC_VIEW
+      #返回参数名称,默认token
+      paramT: token
+      #调用身份服务附带参数
+      beanObjectsDetailLastParam: PERSON_ID,SIDELINE,STATUS,PARENT_GUID
+      #调取机构服务附带参数
+      organizationCategoryLastParam: CUSTOMS_CODE,ORG_CLASS,STATUS,ORG_TYPE,PARENT_GUID
+    h4a:
+      #机构读取服务
+      oguReaderService: http://10.200.21.195/CupaaCenterService/OguReaderService.svc
+      #身份读取服务
+      accreditReaderService: http://10.200.21.195/CupaaCenterService/AccreditReaderService.svc
+      registerAppService: http://10.200.21.195/AppRegisterService/RegisterAppReaderService.svc
+      #默认视角
+      defaultBaseView: CCIC_VIEW
+      #系统基础视角
+      systemBaseView: BASE_VIEW
+      #调用身份服务附带参数
+      beanObjectsDetailLastParam: PERSON_ID,SIDELINE,STATUS,PARENT_GUID
+      #调取机构服务附带参数
+      organizationCategoryLastParam: CUSTOMS_CODE,ORG_CLASS,STATUS,ORG_TYPE,PARENT_GUID
+      #调用角色信息接口附带参数
+      beanRolesLastParam: RESOURCE_LEVEL
+      #单点登录地址
+      authenticateUrl: http://10.200.21.195/Passport/SsoLogin.aspx
+      #注销地址
+      logOffUrl: http://10.200.21.195/Passport/LogOff.aspx
+      #注销后跳转地址
+      logOffCallBackUrl: /api/getlogoffurl
+      #H4A应用名称
+      appID: ZWCJ
+      #要求H4A返回的用户登录名类型(forms返回表单登录时录入的登录名,hr返回海关员工号...等)
+      idAuthenticationMode: forms
+      #认证类型
+      defaultAuthenticationMode: FormsAuthentication
+      #返回参数名称,默认token
+      paramT: token
+      #建立webService连接超时时间
+      connectionTimeout: 10
+      #webService请求响应超时时间
+      requestTimeout: 10
+  proxy:
+    audit:
+      enabled: true
+      name: hlog-api-service
+      url: http://app-api.expc.dev2.com/hlog-api-service/
+    auth:
+      name: cacp-authproxy-service
+    fileserver:
+      url: http://application.testf-nb.com/file-access-service/
+    hgid:
+      enabled: false
+      name: hgid-main-service
+      url: http://application.testf-nb.com/hgid-main-service/
+
+#新4a服务地址配置
+cus4a:
+  open-api:
+    url: cus4a-api.dev-xc.com:80
+    auth-name: cus4a-authorization-new-read-service # cus4a-authorization-new-read-service为新授权服务名, cus4a-authorization-read-service为授权兼容版服务名

+ 75 - 0
government-demo-service/src/main/resources/mapper/government/GovernmentMapper.xml

@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.gov.customs.demo.government.dao.GovernmentDao">
+    <resultMap id="BaseResultMap" type="cn.gov.customs.demo.government.pojo.GovernmentInfo">
+        <id column="FORM_ID" jdbcType="VARCHAR" property="formId"/>
+        <result column="APP_CODE" jdbcType="VARCHAR" property="appCode"/>
+        <result column="FORM_TITLE" jdbcType="VARCHAR" property="formTitle"/>
+        <result column="FORM_CONTENT" jdbcType="VARCHAR" property="formContent"/>
+        <result column="FORM_STATUS" jdbcType="VARCHAR" property="formStatus"/>
+        <result column="PROCESS_ID" jdbcType="VARCHAR" property="processId"/>
+        <result column="PROCESS_CODE" jdbcType="VARCHAR" property="processCode"/>
+        <result column="PROCESS_RANGE" jdbcType="INTEGER" property="processRange"/>
+        <result column="ISSUE_USER_ID" jdbcType="VARCHAR" property="issueUserId"/>
+        <result column="ISSUE_USER_NAME" jdbcType="VARCHAR" property="issueUserName"/>
+        <result column="ISSUE_USER_PARENT_ID" jdbcType="VARCHAR" property="issueUserParentId"/>
+        <result column="ISSUE_USER_FULL_PATH_NAME" jdbcType="VARCHAR" property="issueUserFullPathName"/>
+        <result column="ISSUE_DEPT_FULL_PATH_NAME" jdbcType="VARCHAR" property="issueDeptFullPathName"/>
+        <result column="CREATE_TIME" jdbcType="TIMESTAMP" property="createTime"/>
+        <result column="COMPLETE_TIME" jdbcType="TIMESTAMP" property="completeTime"/>
+    </resultMap>
+
+    <insert id="insert">
+        INSERT INTO CACP_DEMO_GOVERNMENT_INFO
+            (FORM_ID, APP_CODE, FORM_TITLE, FORM_CONTENT, FORM_STATUS, PROCESS_ID, PROCESS_CODE, PROCESS_RANGE,
+            ISSUE_USER_ID, ISSUE_USER_NAME, ISSUE_USER_PARENT_ID, ISSUE_USER_FULL_PATH_NAME, ISSUE_DEPT_FULL_PATH_NAME,
+            CREATE_TIME, COMPLETE_TIME) VALUES
+        (#{formId}, #{appCode}, #{formTitle}, #{formContent}, #{formStatus}, #{processId}, #{processCode}, #{processRange},
+            #{issueUserId}, #{issueUserName}, #{issueUserParentId}, #{issueUserFullPathName}, #{issueDeptFullPathName},
+            #{createTime,jdbcType=TIMESTAMP}, #{completeTime,jdbcType=TIMESTAMP})
+    </insert>
+
+    <update id="update">
+        UPDATE CACP_DEMO_GOVERNMENT_INFO SET
+            FORM_TITLE = #{formTitle},
+            FORM_CONTENT = #{formContent},
+            FORM_STATUS = #{formStatus},
+            COMPLETE_TIME = #{completeTime},
+        WHERE FORM_ID = #{formId}
+    </update>
+
+    <update id="updateWithFields">
+        UPDATE CACP_DEMO_GOVERNMENT_INFO SET
+        <trim suffixOverrides=",">
+            <if test="fields.containsKey('formContent')">
+                FORM_CONTENT = #{fields.formContent},
+            </if>
+            <if test="fields.containsKey('formStatus')">
+                FORM_STATUS = #{fields.formStatus},
+            </if>
+            <if test="fields.containsKey('completeTime')">
+                COMPLETE_TIME = #{fields.completeTime},
+            </if>
+        </trim>
+        WHERE FORM_ID = #{formId}
+    </update>
+
+    <select id="getList" resultMap="BaseResultMap">
+        SELECT *
+        FROM CACP_DEMO_GOVERNMENT_INFO
+        WHERE CREATE_TIME &gt;= #{startDate,jdbcType=DATE} AND CREATE_TIME &lt; #{endDate,jdbcType=DATE} + 1
+        <if test="title != null and title != ''">
+            AND FORM_TITLE LIKE concat(concat('%',#{title}),'%')
+        </if>
+        ORDER BY CREATE_TIME DESC
+    </select>
+
+    <select id="get" resultMap="BaseResultMap">
+        SELECT * FROM CACP_DEMO_GOVERNMENT_INFO WHERE FORM_ID = #{formId}
+    </select>
+
+    <delete id="delete">
+        DELETE FROM CACP_DEMO_GOVERNMENT_INFO WHERE FORM_ID = #{formId}
+    </delete>
+
+</mapper>

+ 78 - 0
government-demo-web/.gitignore

@@ -0,0 +1,78 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+*.tsbuildinfo
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 8 - 0
government-demo-web/.prettierrc.json

@@ -0,0 +1,8 @@
+{
+  "$schema": "https://json.schemastore.org/prettierrc",
+  "semi": false,
+  "tabWidth": 2,
+  "singleQuote": true,
+  "printWidth": 120,
+  "trailingComma": "none"
+}

+ 7 - 0
government-demo-web/.vscode/extensions.json

@@ -0,0 +1,7 @@
+{
+  "recommendations": [
+    "Vue.volar",
+    "dbaeumer.vscode-eslint",
+    "esbenp.prettier-vscode"
+  ]
+}

+ 20 - 0
government-demo-web/Dockerfile

@@ -0,0 +1,20 @@
+#1.0.9环境
+FROM 10.200.16.14:60080/customer/nginx:v1.13.12
+
+#1.12环境
+#FROM 172.4.2.72:31104/haiguan_base/nginx:v1.21.5-alpine-hg
+
+#XC环境使用这个镜象
+#FROM 10.200.65.6:31104/tsf_base/nginx:1.21.5-alpine
+
+ADD ./dist /usr/share/nginx/html
+
+ADD tsf-consul-template-docker-x86.tar.gz /root/
+
+RUN mkdir -p /data/logs/customs
+
+COPY ./nginx.conf /etc/nginx/conf.d
+
+EXPOSE 8080
+CMD ["sh", "-c", "sh /root/tsf-consul-template-docker/script/start.sh;/usr/sbin/nginx -g 'daemon off;'"]
+

+ 12 - 0
government-demo-web/Dockerfile-arm

@@ -0,0 +1,12 @@
+FROM 10.200.62.170:31104/tsf_1/nginx:1.13.12_arm_v8
+
+ADD ./dist /usr/share/nginx/html
+
+ADD tsf-consul-template-docker.arm.tar.gz /root/
+
+RUN mkdir -p /data/logs/customs
+
+COPY ./nginx.conf /etc/nginx/conf.d
+
+EXPOSE 8080
+CMD ["sh", "-c", "sh /root/consul-template/script/start.sh;/usr/sbin/nginx -g 'daemon off;'"]

+ 123 - 0
government-demo-web/README.md

@@ -0,0 +1,123 @@
+# 应用开发脚手架
+
+## 启动项目
+
+### 安装
+
+```sh
+npm install
+```
+
+### 运行
+
+```sh
+npm run dev
+```
+
+### 使用线上环境联调
+
+如果需要使用线上联调环境需要如下配置保证接口调用权限,否则不需要配置。
+
+#### 登录到线上开发环境
+
+例如:http://www.h2018.dev-nb.com/
+
+#### 配置本地 host
+
+打开本地 host 文件,增加如下配置:
+
+``` sh
+127.0.0.1  local.dev-nb.com
+```
+
+主域名需要与登录的开发环境一致(例如:dev-nb.com),子域名可以任意配置(例如:local、dev)。
+
+#### 本地代理配置
+参考下面示例在 vite.config.ts 的 proxy 中配置。
+
+``` js
+// 本地接口联调示例
+'/demo-gov-service': {
+  target: 'http://10.200.24.106:19999',
+  changeOrigin: true,
+  // 如果本地接口不需要走微服务网关则去掉
+  rewrite: (path) => path.replace('/demo-gov-service', '')
+}
+
+// 线上接口联调配置示例
+'/parameter-service': {
+  target: 'http://app-api.expc.dev2.com',
+  changeOrigin: true,
+},
+```
+注意:接口调用不需要加 `/api` 前缀,上线会自动添加。
+
+#### 访问本地页面
+
+打开 local.dev-nb.com:8000(8000为默认的本地运行端口,如果不同自行替换),即为登录状态的页面。
+
+## 开发指南
+
+### 脚手架公共能力
+
+* element-plus ui 组件库
+* 海关自己的组件库(@cacp/ui)
+* 脚手架中的公共代码
+
+#### element-plus
+
+参考官方文档
+
+#### @cacp/ui
+
+文档:// TO ADD
+
+基于 element-plus 封装的业务组件库,主要有两个核心功能:
+
+* 提供全局的主题 css 文件
+* 典型页面及功能区组件
+
+#### 脚手架公共代码
+
+##### 公共 api
+
+* login
+
+##### assets
+
+* base.css
+
+##### directives
+
+* permission
+
+##### hooks
+
+* loading
+
+##### plugins
+
+* icon (element-plus icon)
+
+##### routers
+
+* error
+* not found
+
+##### stores
+
+* userStore
+
+##### types
+
+* core
+  * tool
+  * auth
+  * config
+  * framework
+
+## 构建
+
+```sh
+npm run build
+```

BIN
government-demo-web/cacp/icon/svg-icons-0.1.2.tgz


BIN
government-demo-web/cacp/ui/ui-0.3.17.tgz


+ 1 - 0
government-demo-web/env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 19 - 0
government-demo-web/eslint.config.js

@@ -0,0 +1,19 @@
+import pluginVue from 'eslint-plugin-vue'
+import vueTsEslintConfig from '@vue/eslint-config-typescript'
+import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
+
+export default [
+  ...pluginVue.configs['flat/essential'],
+  ...vueTsEslintConfig(),
+  {
+    name: 'app/files-to-lint',
+    files: ['**/*.{ts,mts,tsx,vue}'],
+    rules: { '@typescript-eslint/no-explicit-any': 0 }
+  },
+  {
+    name: 'app/files-to-ignore',
+    ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**']
+  },
+  
+  skipFormatting
+]

+ 14 - 0
government-demo-web/index.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <link rel="icon" href="/favicon.ico">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <script type="module" src="/config.js"></script>
+    <title>cacp-demo-client</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 73 - 0
government-demo-web/nginx.conf

@@ -0,0 +1,73 @@
+access_log  /data/logs/customs/tsf-access.log  main;
+error_log  /data/logs/customs/tsf-error.log  notice;
+
+gzip_static on;
+gzip on;
+gzip_proxied expired no-cache no-store private auth; #启用压缩的响应头
+gzip_min_length 1k; #文件大于1k压缩
+gzip_buffers 4 16k; #设置压缩所需要的缓冲区大小
+gzip_http_version 1.1; #http协议版本
+gzip_comp_level 2; #压缩级别 1-9
+gzip_types text/plain application/javascript text/css #文件类型
+gzip_vary on; #是否在http  header 中添加 Vary: Accept-Encoding 建议开启
+
+server {
+  	listen	8080;
+
+	# html类 不缓存
+    location ~ \.(html|htm)$ {
+        root	/usr/share/nginx/html;
+        add_header Cache-Control no-cache;
+    }
+
+    # css、js 缓存,仅客户端缓存
+    location ~ \.(css|js)$ {
+        root	/usr/share/nginx/html;
+        add_header Cache-Control private,max-age=28800;
+    }
+
+    # 图片类 缓存,仅客户端缓存
+    location ~ \.(gif|jpg|jpeg|png|bmp|ico)$ {
+        root	/usr/share/nginx/html;
+        add_header Cache-Control private,max-age=28800;
+    }
+
+    # 字体类 缓存,仅客户端缓存
+    location ~ \.(ttf|woff|woff2|otf|ttc|eot|svg)$ {
+        root	/usr/share/nginx/html;
+        add_header Cache-Control private,max-age=28800;
+    }
+
+  	location / {
+		try_files $uri $uri/ @router;
+		root	/usr/share/nginx/html;
+		index	index.html index.htm;
+ 	}
+
+	location /api/ {
+		proxy_set_header	Host	$host;
+		proxy_set_header	X-Real-IP	$remote_addr;
+		proxy_set_header	X-Forwarded-for	$remote_addr;
+		proxy_connect_timeout	300;
+		port_in_redirect off;
+
+		rewrite ^/api/(.*) /$1 break;
+    	proxy_pass	http://apigw:11000/;
+	}
+
+	location /static-resource/ {
+		proxy_set_header	Host	$host;
+		proxy_set_header	X-Real-IP	$remote_addr;
+		proxy_set_header	X-Forwarded-for	$remote_addr;
+		proxy_connect_timeout	300;
+		port_in_redirect off;
+
+		rewrite ^/static-resource/(.*) /$1 break;
+    	proxy_pass	http://static-resource-web:8080/;
+	}
+
+	location @router {
+		rewrite ^.*$ /index.html last;
+	}
+}
+

+ 1 - 0
government-demo-web/node_version

@@ -0,0 +1 @@
+20

+ 69 - 0
government-demo-web/package.json

@@ -0,0 +1,69 @@
+{
+  "name": "cacp-client-demo",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite --force",
+    "build": "run-p type-check \"build-only {@}\" --",
+    "preview": "vite preview",
+    "build-only": "vite build",
+    "type-check": "vue-tsc --build --force",
+    "lint": "eslint . --fix",
+    "format": "prettier --write src/"
+  },
+  "dependencies": {
+    "@antv/x6": "2.18.1",
+    "@antv/x6-plugin-dnd": "2.1.1",
+    "@antv/x6-plugin-keyboard": "2.2.3",
+    "@antv/x6-plugin-scroller": "2.0.10",
+    "@antv/x6-plugin-selection": "2.2.2",
+    "@antv/x6-plugin-snapline": "2.1.7",
+    "@wangeditor/editor": "5.1.23",
+    "@wangeditor/editor-for-vue": "5.1.12",
+    "@cacp/svg-icons": "file:./cacp/icon/svg-icons-0.1.2.tgz",
+    "@cacp/ui": "file:./cacp/ui/ui-0.3.17.tgz",
+    "axios": "1.7.7",
+    "crypto-js": "4.2.0",
+    "dayjs": "1.11.13",
+    "echarts": "5.5.1",
+    "element-plus": "2.8.6",
+    "js-file-download": "0.4.12",
+    "lodash-es": "4.17.21",
+    "nanoid": "5.0.8",
+    "normalize.css": "8.0.1",
+    "nprogress": "0.2.0",
+    "pinia": "2.2.5",
+    "pinia-plugin-persistedstate": "3.2.1",
+    "qs": "6.12.3",
+    "string-format": "2.0.0",
+    "vue": "3.5.12",
+    "vue-echarts": "7.0.3",
+    "vue-router": "4.4.5",
+    "vuedraggable": "2.24.3"
+  },
+  "devDependencies": {
+    "@rushstack/eslint-patch": "1.10.4",
+    "@tsconfig/node20": "20.1.4",
+    "@types/crypto-js": "4.2.2",
+    "@types/node": "22.8.4",
+    "@types/nprogress": "0.2.3",
+    "@types/qs": "6.9.15",
+    "@types/string-format": "2.0.3",
+    "@vitejs/plugin-vue": "4.1.0",
+    "@vitejs/plugin-vue-jsx": "3.0.1",
+    "@vue/eslint-config-prettier": "10.1.0",
+    "@vue/eslint-config-typescript": "14.1.3",
+    "@vue/tsconfig": "0.5.1",
+    "eslint": "9.13.0",
+    "eslint-plugin-vue": "9.30.0",
+    "less": "4.2.0",
+    "less-loader": "12.2.0",
+    "npm-run-all2": "7.0.1",
+    "prettier": "3.3.3",
+    "typescript": "5.4.5",
+    "vite": "^4.0.0",
+    "vite-plugin-vue-devtools": "7.6.1",
+    "vue-tsc": "2.1.10"
+  }
+}

+ 10 - 0
government-demo-web/public/config.js

@@ -0,0 +1,10 @@
+window.$config = {
+  SERVICE_ID: '{{service_id}}',
+  SERVICE_NAME: '{{service_name}}',
+  SERVICE_PAGESIZE: 20,
+  SERVICE_API: '/api/service-api-name',//当前应用api地址,上线需要修改
+  SERVICE_TIMEOUT: 10000, //请求超时时间
+  NEED_USER_AUTHORITY: true,//是否需要权限控制
+  FRAME_API: '/api/frame-api-service',//统一入口接口地址
+  AUTH_MODE: 'Cookie'
+}

BIN
government-demo-web/public/favicon.ico


+ 72 - 0
government-demo-web/src/App.vue

@@ -0,0 +1,72 @@
+<template>
+  <el-container class="container">
+    <!-- 开发测试使用 -->
+    <el-aside width="190px" class="aside" v-if="DEV">
+      <el-menu class="menu">
+        <router-link to="/gov-list">
+          <el-menu-item index="gov-list" class="menu-item">
+            <el-icon><House /></el-icon>
+            <span>政务表单列表</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/user-task-list">
+          <el-menu-item index="user-task-list" class="menu-item">
+            <el-icon><House /></el-icon>
+            <span>用户待办</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/process-list">
+          <el-menu-item index="process-list" class="menu-item">
+            <el-icon><House /></el-icon>
+            <span>流程列表</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/mock-user">
+          <el-menu-item index="mock-user" class="menu-item">
+            <el-icon><House /></el-icon>
+            <span>模拟用户</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/designer?mode=create">
+          <el-menu-item index="designer" class="menu-item">
+            <el-icon><House /></el-icon>
+            <span>设计</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/home">
+          <el-menu-item index="home" class="menu-item">
+            <el-icon><House /></el-icon>
+            <span>首页</span>
+          </el-menu-item>
+        </router-link>
+      </el-menu>
+    </el-aside>
+    
+    <el-container class="content">
+      <RouterView />
+    </el-container>
+  </el-container>
+</template>
+
+<script setup lang="ts">
+import { RouterView } from 'vue-router'
+const { DEV } = import.meta.env
+</script>
+
+<style scoped lang="less">
+.container {
+  height: 100vh;
+}
+.aside {
+  background: url(@/assets/images/left-menu-bg.png) no-repeat #055395;
+  --el-menu-item-height: 44px;
+  --el-menu-bg-color: transparent;
+  --el-menu-text-color: rgba(255, 255, 255, 0.8);
+  --el-menu-hover-bg-color: rgba(255, 255, 255, 0.15);
+  --el-menu-hover-text-color: rgb(255, 255, 255);
+}
+.el-menu-item.is-active {
+  color: var(--el-menu-text-color);
+  background-color: var(--el-menu-hover-bg-color);
+}
+</style>

+ 56 - 0
government-demo-web/src/apis/accessory.ts

@@ -0,0 +1,56 @@
+import type { AxiosResponse } from 'axios'
+import download from 'js-file-download'
+import config from '@/config'
+
+import request from '@/utils/request'
+import { SuccessResultCode, type Result, type Accessory } from '@cacp/ui'
+
+const contextPath = '/accessory'
+
+export function getUploadUrl(): string {
+  if (config.SERVICE_API.endsWith('/')) {
+    return `${config.SERVICE_API.substring(0, config.SERVICE_API.length - 1)}${contextPath}/upload-file`
+  } else {
+    return `${config.SERVICE_API}/${contextPath}/upload-file`
+  }
+}
+
+export function getDownloadUrl(accessoryId: string): string {
+  if (config.SERVICE_API.endsWith('/')) {
+    return `${config.SERVICE_API.substring(0, config.SERVICE_API.length - 1)}${contextPath}/download-file?accessoryId=${accessoryId}`
+  } else {
+    return `${config.SERVICE_API}${contextPath}/download-file?accessoryId=${accessoryId}`
+  }
+}
+
+export async function downloadFile(accessoryId: string, fileName: string): Promise<void> {
+  const res: AxiosResponse<Result<any>> = await request.get(`${contextPath}/download-file`, {
+    params: { accessoryId: accessoryId },
+    responseType: 'arraybuffer'
+  })
+
+  if (res.data.code === SuccessResultCode) {
+    download(res.data.data, fileName, 'application/octet-stream')
+  }
+}
+
+export async function deleteFile(accessoryId: string): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(`${contextPath}/delete-file`, {
+    params: { accessoryId: accessoryId }
+  })
+  return res.data
+}
+
+export async function getPersistListByBizId(bizId: string): Promise<Result<Array<Accessory>>> {
+  const res: AxiosResponse<Result<Array<Accessory>>> = await request.get(`${contextPath}/get-persist-list-by-biz-id`, {
+    params: { bizId: bizId }
+  })
+  return res.data
+}
+
+export async function getPersistListByRelId(bizId: string, relId: string): Promise<Result<Array<Accessory>>> {
+  const res: AxiosResponse<Result<Array<Accessory>>> = await request.get(`${contextPath}/get-persist-list-by-rel-id`, {
+    params: { bizId: bizId, relId: relId }
+  })
+  return res.data
+}

+ 10 - 0
government-demo-web/src/apis/authority.ts

@@ -0,0 +1,10 @@
+import type { AxiosResponse } from 'axios'
+import request from '@/utils/request'
+import type { Result, UserAuthorityInfo } from '@cacp/ui'
+
+const contextPath = '/cacp-authority'
+
+export async function getUserAuthority(): Promise<Result<UserAuthorityInfo>> {
+  const res: AxiosResponse<Result<UserAuthorityInfo>> = await request.get(`${contextPath}/get-user-authority`)
+  return res.data
+}

+ 10 - 0
government-demo-web/src/apis/frame.ts

@@ -0,0 +1,10 @@
+import type { AxiosResponse } from 'axios'
+import request from '@/utils/request'
+import type { Result, FrameUserInfo } from '@cacp/ui'
+
+const contextPath = '/auth'
+
+export async function getFrameUser(): Promise<Result<FrameUserInfo>> {
+  const res: AxiosResponse<Result<FrameUserInfo>> = await request.get(`${contextPath}/get-frame-user`)
+  return res.data
+}

+ 25 - 0
government-demo-web/src/apis/government.ts

@@ -0,0 +1,25 @@
+import type { AxiosResponse } from 'axios'
+import request from '@/utils/request'
+
+import type { PageInfo, Result, RuntimeTask } from '@cacp/ui'
+import type { GovernmentInfo, GovernmentQueryInfo, UserTaskQueryInfo } from '@/types/government'
+
+const contextPath = '/government'
+
+//获取列表
+export async function getPageList(query: GovernmentQueryInfo): Promise<Result<PageInfo<GovernmentInfo>>> {
+  const res: AxiosResponse<Result<PageInfo<GovernmentInfo>>> = await request.post(`${contextPath}/get-page-list`, query)
+  return res.data
+}
+
+export async function get(formId: string): Promise<Result<GovernmentInfo>> {
+  const res: AxiosResponse<Result<GovernmentInfo>> = await request.get(`${contextPath}/get`, {
+    params: { formId: formId }
+  })
+  return res.data
+}
+
+export async function getUserTaskListByPage(query: UserTaskQueryInfo): Promise<Result<PageInfo<RuntimeTask>>> {
+  const res: AxiosResponse<Result<PageInfo<RuntimeTask>>> = await request.post(`${contextPath}/get-user-task-list-by-page`, query)
+  return res.data
+}

+ 23 - 0
government-demo-web/src/apis/mock.ts

@@ -0,0 +1,23 @@
+import type { AxiosResponse } from 'axios'
+
+import request from '@/utils/request'
+import type { Result, FrameUserInfo } from '@cacp/ui'
+import config from '@/config'
+
+const contextPath = '/mock'
+
+export async function getFrameUser(): Promise<Result<FrameUserInfo>> {
+  const res: AxiosResponse<Result<FrameUserInfo>> = await request.get(`${contextPath}/get-frame-user`)
+  return res.data
+}
+
+export async function setMockUser(viewCode: string, fullPathName: string): Promise<Result<boolean>> {
+  const res: AxiosResponse<Result<boolean>> = await request.get(`${contextPath}/set-mock-user`, {
+    params: { viewCode: viewCode, appCode: config.AUTH_APP_CODE, fullPathName: fullPathName }
+  })
+  return res.data
+}
+
+export async function clearMockUser() {
+  await request.get(`${contextPath}/clear-mock-user`)
+}

+ 78 - 0
government-demo-web/src/apis/workflow/definition.ts

@@ -0,0 +1,78 @@
+import type { AxiosResponse } from 'axios'
+
+import request from '@/utils/request'
+import type { Result, ProcessDefinition, CommentTemplate, ProcessDescriptor, TypeDescriptor } from '@cacp/ui'
+
+const contextPath = '/process-definition'
+
+export async function getDefinition(
+  code: string,
+  version: number
+): Promise<Result<ProcessDescriptor>> {
+  const res: AxiosResponse<Promise<Result<ProcessDescriptor>>> = await request.get(`${contextPath}/get-definition`, {
+    params: { code: code, version: version }
+  })
+  return res.data
+}
+
+export async function getDefinitionList(): Promise<Result<Array<ProcessDefinition>>> {
+  const res: AxiosResponse<Promise<Result<Array<ProcessDefinition>>>> = await request.get(`${contextPath}/get-definition-list`)
+  return res.data
+}
+
+export async function saveDefinition(descriptor: ProcessDescriptor): Promise<Result<number>> {
+  const res: AxiosResponse<Promise<Result<number>>> = await request.post(`${contextPath}/save-definition`, descriptor)
+  return res.data
+}
+
+export async function deleteDefinition(code: string): Promise<Result<number>> {
+  const res: AxiosResponse<Promise<Result<number>>> = await request.post(`${contextPath}/delete-definition`, null, {
+    params: { code: code }
+  })
+  return res.data
+}
+
+export async function getCommentTemplateList(
+  processCode: string,
+  activityCode: string
+): Promise<Result<Array<CommentTemplate>>> {
+  const res: AxiosResponse<Promise<Result<Array<CommentTemplate>>>> = await request.get(
+    `${contextPath}/get-comment-template-list`,
+    {
+      params: { processCode: processCode, activityCode: activityCode }
+    }
+  )
+  return res.data
+}
+
+export async function insertCommentTemplate(info: CommentTemplate): Promise<Result<number>> {
+  const res: AxiosResponse<Promise<Result<number>>> = await request.post(`${contextPath}/insert-comment-template`, info)
+  return res.data
+}
+export async function updateCommentTemplate(info: CommentTemplate): Promise<Result<number>> {
+  const res: AxiosResponse<Promise<Result<number>>> = await request.post(`${contextPath}/update-comment-template`, info)
+  return res.data
+}
+export async function deleteCommentTemplate(commentId: string): Promise<Result<number>> {
+  const res: AxiosResponse<Promise<Result<number>>> = await request.post(`${contextPath}/delete-comment-template`, null, {
+    params: { commentId: commentId }
+  })
+  return res.data
+}
+
+export async function getAssignmentTypeList(): Promise<Result<Array<TypeDescriptor>>> {
+  const res: AxiosResponse<Result<Array<TypeDescriptor>>> = await request.get(`${contextPath}/get-assignment-type-list`)
+  return res.data
+}
+export async function getStrategyList(): Promise<Result<Array<TypeDescriptor>>> {
+  const res: AxiosResponse<Result<Array<TypeDescriptor>>> = await request.get(`${contextPath}/get-strategy-list`)
+  return res.data
+}
+export async function getFlowTypeList(): Promise<Result<Array<TypeDescriptor>>> {
+  const res: AxiosResponse<Result<Array<TypeDescriptor>>> = await request.get(`${contextPath}/get-flow-type-list`)
+  return res.data
+}
+export async function getNodeTypeList(): Promise<Result<Array<TypeDescriptor>>> {
+  const res: AxiosResponse<Result<Array<TypeDescriptor>>> = await request.get(`${contextPath}/get-node-type-list`)
+  return res.data
+}

+ 129 - 0
government-demo-web/src/apis/workflow/engine.ts

@@ -0,0 +1,129 @@
+import type { AxiosResponse } from 'axios'
+import type {
+  Result,
+  CreateRequestDto,
+  RouteRequestDto,
+  SaveRequestDto,
+  AgreeAutoRequestDto,
+  AgreeRequestDto,
+  OperateRequestDto,
+  StartResponseDto,
+  RuntimeRouteNode,
+  BaseWorkflowForm
+} from '@cacp/ui'
+
+import request from '@/utils/request'
+
+export async function createStart<T extends BaseWorkflowForm>(
+  contextPath: string,
+  dto: CreateRequestDto
+): Promise<Result<StartResponseDto<T>>> {
+  const res: AxiosResponse<Promise<Result<StartResponseDto<T>>>> = await request.post(
+    `${contextPath}/create-start`,
+    dto
+  )
+  return res.data
+}
+
+export async function start<T extends BaseWorkflowForm>(
+  contextPath: string,
+  taskId: string,
+  formId: string
+): Promise<Result<StartResponseDto<T>>> {
+  const res: AxiosResponse<Promise<Result<StartResponseDto<T>>>> = await request.get(`${contextPath}/start`, {
+    params: { taskId: taskId, formId: formId }
+  })
+  return res.data
+}
+
+export async function route(contextPath: string, dto: RouteRequestDto): Promise<Result<Array<RuntimeRouteNode>>> {
+  const res: AxiosResponse<Promise<Result<Array<RuntimeRouteNode>>>> = await request.post(`${contextPath}/route`, dto)
+  return res.data
+}
+
+export async function save<T extends BaseWorkflowForm>(
+  contextPath: string,
+  dto: SaveRequestDto<T>
+): Promise<Result<boolean>> {
+  const res: AxiosResponse<Promise<Result<boolean>>> = await request.post(`${contextPath}/save`, dto)
+  return res.data
+}
+
+export async function autoAgree<T extends BaseWorkflowForm>(
+  contextPath: string,
+  dto: AgreeAutoRequestDto<T>
+): Promise<Result<boolean>> {
+  const res: AxiosResponse<Promise<Result<boolean>>> = await request.post(`${contextPath}/auto-agree`, dto)
+  return res.data
+}
+
+export async function agree<T extends BaseWorkflowForm>(
+  contextPath: string,
+  dto: AgreeRequestDto<T>
+): Promise<Result<boolean>> {
+  const res: AxiosResponse<Promise<Result<boolean>>> = await request.post(`${contextPath}/agree`, dto)
+  return res.data
+}
+
+export async function back<T extends BaseWorkflowForm>(
+  contextPath: string,
+  dto: OperateRequestDto<T>
+): Promise<Result<boolean>> {
+  const res: AxiosResponse<Promise<Result<boolean>>> = await request.post(`${contextPath}/back`, dto)
+  return res.data
+}
+
+export async function revoke(contextPath: string, taskId: string, formId: string): Promise<Result<boolean>> {
+  const res: AxiosResponse<Promise<Result<boolean>>> = await request.get(`${contextPath}/revoke`, {
+    params: { taskId: taskId, formId: formId }
+  })
+  return res.data
+}
+
+export async function reject<T extends BaseWorkflowForm>(
+  contextPath: string,
+  dto: OperateRequestDto<T>
+): Promise<Result<boolean>> {
+  const res: AxiosResponse<Promise<Result<boolean>>> = await request.post(`${contextPath}/reject`, dto)
+  return res.data
+}
+
+export async function suspend<T extends BaseWorkflowForm>(
+  contextPath: string,
+  dto: OperateRequestDto<T>
+): Promise<Result<boolean>> {
+  const res: AxiosResponse<Promise<Result<boolean>>> = await request.post(`${contextPath}/suspend`, dto)
+  return res.data
+}
+
+export async function resume(contextPath: string, processId: string, formId: string): Promise<Result<boolean>> {
+  const res: AxiosResponse<Promise<Result<boolean>>> = await request.get(`${contextPath}/resume`, {
+    params: { processId: processId, formId: formId }
+  })
+  return res.data
+}
+
+export async function remove(contextPath: string, taskId: string, formId: string): Promise<Result<boolean>> {
+  const res: AxiosResponse<Promise<Result<boolean>>> = await request.post(`${contextPath}/delete`, null, {
+    params: { taskId: taskId, formId: formId }
+  })
+  return res.data
+}
+
+export async function forceDelete(contextPath: string, formId: string): Promise<Result<boolean>> {
+  const res: AxiosResponse<Promise<Result<boolean>>> = await request.post(`${contextPath}/force-delete`, null, {
+    params: { formId: formId }
+  })
+  return res.data
+}
+
+export async function show<T extends BaseWorkflowForm>(
+  contextPath: string,
+  taskId: string,
+  formId: string
+): Promise<Result<StartResponseDto<T>>> {
+  const res: AxiosResponse<Promise<Result<StartResponseDto<T>>>> = await request.get(`${contextPath}/show`, {
+    params: { taskId: taskId, formId: formId }
+  })
+  return res.data
+}

+ 54 - 0
government-demo-web/src/apis/workflow/runtime.ts

@@ -0,0 +1,54 @@
+import type { AxiosResponse } from 'axios'
+
+import request from '@/utils/request'
+import type { Result, TypeDescriptor, RuntimeProcess, RuntimeTask, TraceInfo, TaskType } from '@cacp/ui'
+
+const contextPath = '/process-runtime'
+
+export async function getRunningProcessList(): Promise<Result<Array<RuntimeProcess>>> {
+  const res: AxiosResponse<Result<Array<RuntimeProcess>>> = await request.get(`${contextPath}/get-running-process-list`)
+  return res.data
+}
+
+export async function getUserTaskList(type: TaskType): Promise<Result<Array<RuntimeTask>>> {
+  const res: AxiosResponse<Result<Array<RuntimeTask>>> = await request.get(`${contextPath}/get-user-task-list`, {
+    params: { type: type }
+  })
+  return res.data
+}
+
+export async function rollback(processId: string): Promise<Result<boolean>> {
+  const res: AxiosResponse<Result<boolean>> = await request.get(`${contextPath}/rollback`, {
+    params: { processId: processId }
+  })
+  return res.data
+}
+
+export async function trace(processId: string): Promise<Result<TraceInfo>> {
+  const res: AxiosResponse<Promise<Result<TraceInfo>>> = await request.get(`${contextPath}/trace`, {
+    params: { processId: processId }
+  })
+  return res.data
+}
+
+export async function traceBranch(assignId: string): Promise<Result<TraceInfo>> {
+  const res: AxiosResponse<Promise<Result<TraceInfo>>> = await request.get(`${contextPath}/trace-branch`, {
+    params: { assignId: assignId }
+  })
+  return res.data
+}
+
+export async function getFormRuntimeStatusList(): Promise<Result<Array<TypeDescriptor>>> {
+  const res: AxiosResponse<Result<Array<TypeDescriptor>>> = await request.get(`${contextPath}/get-form-runtime-status-list`)
+  return res.data
+}
+
+export async function getProcessRuntimeStatusList(): Promise<Result<Array<TypeDescriptor>>> {
+  const res: AxiosResponse<Result<Array<TypeDescriptor>>> = await request.get(`${contextPath}/get-process-runtime-status-list`)
+  return res.data
+}
+
+export async function getTaskTypeList(): Promise<Result<Array<TypeDescriptor>>> {
+  const res: AxiosResponse<Result<Array<TypeDescriptor>>> = await request.get(`${contextPath}/get-task-type-list`)
+  return res.data
+}

+ 36 - 0
government-demo-web/src/assets/base.less

@@ -0,0 +1,36 @@
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+  margin: 0;
+  font-weight: normal;
+}
+
+body {
+  min-height: 100vh;
+  transition:
+    color 0.5s,
+    background-color 0.5s;
+  line-height: 1.6;
+  font-family:
+    Inter,
+    -apple-system,
+    BlinkMacSystemFont,
+    'Segoe UI',
+    Roboto,
+    Oxygen,
+    Ubuntu,
+    Cantarell,
+    'Fira Sans',
+    'Droid Sans',
+    'Helvetica Neue',
+    sans-serif;
+  font-size: 15px;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+  text-decoration: none;
+}

BIN
government-demo-web/src/assets/images/404.png


BIN
government-demo-web/src/assets/images/left-menu-bg.png


+ 12 - 0
government-demo-web/src/assets/main.less

@@ -0,0 +1,12 @@
+@import './base.less';
+
+.cacp-request-message-box.el-message-box {
+  width: 60vw;
+  min-width: 60vw;
+  max-width: 60vw;
+}
+
+.cacp-request-message-box.el-message-box .el-message-box__message {
+  overflow: auto;
+  max-height: 60vh;
+}

+ 154 - 0
government-demo-web/src/components/accessory/AccessoryItem.vue

@@ -0,0 +1,154 @@
+<template>
+  <li class="file-item" @mouseenter="onMouseenter" @mouseleave="onMouseleave">
+    <el-icon :class="{ 'is-loading': isDownLoading }"><component :is="statusIcon" /></el-icon>
+    <span class="file-item-name">
+      <el-tooltip effect="dark" placement="top-start" :persistent="false">
+        <template #content>
+          <div class="uploader-tag-tooltip">
+            <span class="tooltip-file-name">{{ accessory.fileName }}</span>
+            <el-space class="cacp-ml-m" size="small" v-if="!disabled">
+              <el-link :disabled="!canSortLeft" @click.prevent="onSortLeft">
+                <el-icon>
+                  <Top />
+                </el-icon>
+              </el-link>
+              <el-link :disabled="!canSortRight" @click.prevent="onSortRight">
+                <el-icon>
+                  <Bottom />
+                </el-icon>
+              </el-link>
+            </el-space>
+          </div>
+        </template>
+        <a @click.prevent="downloadFile" class="upload-tag-inner">
+          <span>{{ accessory.fileName }}</span>
+        </a>
+      </el-tooltip>
+    </span>
+    <el-icon v-if="!disabled" @click.stop="onDelete" size="16" class="close"><Close /></el-icon>
+  </li>
+</template>
+<script lang="ts" setup>
+import { computed, inject, ref } from 'vue'
+import { type Accessory } from '@cacp/ui'
+import { accessoryInjectionKey, SortOrder } from './constants'
+import { min, max } from 'lodash-es'
+
+const props = withDefaults(
+  defineProps<{
+    accessory: Accessory
+    disabled?: boolean
+  }>(),
+  {
+    disabled: true
+  }
+)
+
+const emits = defineEmits<{
+  (e: 'on-delete', value: Accessory, isTemp: boolean): void
+  (e: 'on-sort', value: Accessory, sortOrder: SortOrder): void
+  (e: 'on-download', value: Accessory): void
+}>()
+const { downloadingList, accessoryList } = inject(accessoryInjectionKey)!
+
+const mouseenter = ref(false)
+
+const statusIcon = computed(() => {
+  if (isDownLoading.value) {
+    return 'Loading'
+  } else {
+    if (mouseenter.value) {
+      return 'Download'
+    } else {
+      return 'Document'
+    }
+  }
+})
+
+const isDownLoading = computed<boolean>(() => {
+  return downloadingList.value.includes(props.accessory.accessoryId)
+})
+
+const canSortLeft = computed<boolean>(() => {
+  const minValue = min(accessoryList.value.map((a) => a.sortOrder)) ?? 0
+  return minValue !== props.accessory.sortOrder
+})
+
+const canSortRight = computed<boolean>(() => {
+  const maxValue = max(accessoryList.value.map((a) => a.sortOrder)) ?? 0
+  return maxValue !== props.accessory.sortOrder
+})
+
+function onSortLeft() {
+  if (canSortLeft.value) {
+    emits('on-sort', props.accessory, SortOrder.left)
+  }
+}
+
+function onSortRight() {
+  if (canSortRight.value) {
+    emits('on-sort', props.accessory, SortOrder.right)
+  }
+}
+
+function downloadFile() {
+  emits('on-download', props.accessory)
+}
+
+function onDelete() {
+  emits('on-delete', props.accessory, !props.accessory.persisted)
+}
+
+function onMouseenter() {
+  mouseenter.value = true
+}
+
+function onMouseleave() {
+  mouseenter.value = false
+}
+</script>
+<style lang="less" scoped>
+.file-item {
+  height: 30px;
+  line-height: 30px;
+  font-size: var(--font-size-s);
+  padding: 0 var(--cacp-padding-space-s);
+  display: flex;
+  align-items: center;
+  &:hover {
+    background-color: var(--el-fill-color-light);
+  }
+  &.is-error {
+    background-color: var(--el-color-error-light-9);
+    .file-item-name {
+      color: var(--el-color-error);
+    }
+  }
+}
+.file-item-name {
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  margin: 0 var(--cacp-margin-space-s);
+  cursor: pointer;
+}
+.close {
+  cursor: pointer;
+  &:hover {
+    color: var(--el-color-primary);
+  }
+}
+.uploader-tag-tooltip {
+  display: flex;
+  align-items: center;
+  .el-icon {
+    color: rgba(255, 255, 255, 0.8);
+    font-size: 16px;
+    &:not(.is-disabled):hover {
+      color: #fff;
+    }
+  }
+}
+
+// .file-item
+</style>

+ 272 - 0
government-demo-web/src/components/accessory/AccessoryUploader.vue

@@ -0,0 +1,272 @@
+<template>
+  <ul class="file-list" v-if="readonly">
+    <accessory-item
+      :key="accessory.accessoryId"
+      v-for="accessory in accessoryList"
+      :accessory="accessory"
+      :disabled="true"
+      @on-download="downloadFile"
+    ></accessory-item>
+  </ul>
+  <el-upload
+    v-else
+    ref="uploadRef"
+    :action="uploadUrl"
+    :data="uploadData"
+    :accept="accept"
+    :auto-upload="true"
+    :multiple="true"
+    :disabled="isDisabled"
+    :limit="uploadRemain"
+    :show-file-list="false"
+    v-model:file-list="uploadingFiles"
+    :on-change="onUploadChange"
+    class="uploader"
+  >
+    <template #trigger>
+      <el-button size="small" type="primary" :disabled="isDisabled" class="uploader-add">
+        <el-icon>
+          <Plus />
+        </el-icon>
+        <span>添加文件 {{ uploadingText }}</span>
+      </el-button>
+    </template>
+    <ul class="file-list cacp-mt-m" v-if="accessoryList.length > 0 || uploadingFiles.length > 0">
+      <accessory-item
+        :key="accessory.accessoryId"
+        v-for="accessory in accessoryList"
+        :accessory="accessory"
+        @on-delete="onDelete"
+        @on-sort="onSort"
+        @on-download="downloadFile"
+        :disabled="props.disabled"
+      ></accessory-item>
+      <upload-file-item
+        v-for="file in uploadingFiles"
+        :key="file.uid"
+        :file="file"
+        @on-abort="onAbort"
+        :disabled="props.disabled"
+      ></upload-file-item>
+    </ul>
+  </el-upload>
+</template>
+<script setup lang="ts">
+import { ref, computed, provide } from 'vue'
+import {
+  type UploadFile,
+  type UploadFiles,
+  type UploadUserFile,
+  type UploadProps,
+  type UploadContentInstance,
+  ElMessage,
+  ElMessageBox
+} from 'element-plus'
+import { max, min } from 'lodash-es'
+import { accessoryInjectionKey, SortOrder } from './constants'
+import { SuccessResultCode, type Result, type Accessory, type AccessoryDirection, type UploadData } from '@cacp/ui'
+import UploadFileItem from './UploadFileItem.vue'
+import AccessoryItem from './AccessoryItem.vue'
+import * as apis from '@/apis/accessory'
+
+const props = withDefaults(
+  defineProps<{
+    modelValue?: Array<Accessory>
+    category: string
+    bizId?: string
+    bizTag?: string
+    relId?: string
+    persisted?: boolean
+    previewable?: boolean
+    readonly?: boolean
+    limit?: number
+    accept?: string
+    tooltip?: string
+    ellipsis?: number
+    direction?: AccessoryDirection
+    disabled?: boolean
+  }>(),
+  {
+    modelValue: () => [],
+    previewable: true,
+    persisted: false,
+    readonly: false,
+    limit: 0,
+    direction: 'horizontal',
+    disabled: false
+  }
+)
+const emits = defineEmits<{
+  (e: 'update:modelValue', value: Array<Accessory>): void
+}>()
+
+const uploadRef = ref<UploadContentInstance>()
+
+const uploadUrl = apis.getUploadUrl()
+const uploadingFiles = ref<Array<UploadUserFile>>([])
+const uploadData = ref<UploadData>({
+  category: props.category,
+  bizId: props.bizId ?? '',
+  bizTag: props.bizTag ?? '',
+  relId: props.relId ?? '',
+  fileName: '',
+  persisted: props.persisted
+})
+const downloadingList = ref<Array<string>>([])
+
+const isDisabled = computed<boolean>(() => {
+  return props.disabled || !canUpload.value
+})
+
+const modelList = computed<Array<Accessory>>(() => {
+  return props.modelValue ?? []
+})
+// ref<Array<Accessory>>(_.cloneDeep(props.modelValue))
+const accessoryList = computed<Array<Accessory>>(() => {
+  if (props.bizTag) {
+    return modelList.value
+      .filter((a) => !a.deleted && a.bizTag === props.bizTag)
+      .sort((a, b) => a.sortOrder - b.sortOrder)
+  }
+  return modelList.value.filter((a) => !a.deleted).sort((a, b) => a.sortOrder - b.sortOrder)
+})
+const uploadingText = computed<string>(() => {
+  return props.limit ? `(${accessoryList.value.length + uploadingFiles.value.length}/${props.limit})` : ''
+})
+const canUpload = computed<boolean>(() => {
+  return !props.limit || (!!props.limit && accessoryList.value.length + uploadingFiles.value.length < props.limit)
+})
+const uploadRemain = computed<number | undefined>(() => {
+  if (!props.limit) {
+    return undefined
+  }
+  const count = props.limit - accessoryList.value.length
+  return count < 1 ? 0 : count
+})
+
+async function downloadFile(accessory: Accessory): Promise<void> {
+  downloadingList.value.push(accessory.accessoryId)
+  await apis.downloadFile(accessory.accessoryId, accessory.fileName)
+  const idx = downloadingList.value.indexOf(accessory.accessoryId)
+  downloadingList.value.splice(idx, 1)
+}
+
+async function onDelete(accessory: Accessory, isTemp: boolean): Promise<void> {
+  if (isTemp) {
+    accessory.deleted = true
+  } else {
+    try {
+      await ElMessageBox.confirm('删除该文件,是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+      accessory.deleted = true
+    } catch {
+      // 捕获点击取消时候的错误,避免抛出
+    }
+  }
+}
+
+function onSort(accessory: Accessory, sortOrder: SortOrder) {
+  if (sortOrder === SortOrder.left) {
+    onSortLeft(accessory)
+  } else {
+    onSortRight(accessory)
+  }
+}
+
+function onAbort(file: UploadUserFile): void {
+  uploadRef.value.abort(file as UploadFile)
+  uploadingFiles.value = uploadingFiles.value.filter((uploadFile) => uploadFile !== file)
+}
+
+const onUploadChange: UploadProps['onChange'] = (file: UploadFile, files: UploadFiles): void => {
+  if (file.status === 'ready') {
+    uploadData.value.fileName = file.name
+  } else if (file.status === 'success' || file.status === 'fail') {
+    uploadData.value.fileName = ''
+    const uindex = uploadingFiles.value.findIndex((f) => f.uid === file.uid)
+    if (uindex > -1) {
+      uploadingFiles.value.splice(uindex, 1)
+    }
+    if (file.status === 'success' && file.response) {
+      const res = file.response as Result<Accessory>
+      if (res.code === SuccessResultCode && res.data) {
+        const accessory: Accessory = res.data
+        const sortOrder = max(modelList.value.map((a) => a.sortOrder))
+        accessory.sortOrder = (sortOrder ?? 0) + 1
+        modelList.value.push(accessory)
+        emits('update:modelValue', modelList.value)
+      } else {
+        ElMessage.error({
+          message: `文件【${file.name}】上传失败:${res.message}`,
+          showClose: true
+        })
+      }
+    } else {
+      ElMessage.error({
+        message: `文件【${file.name}】上传失败:${file.status}`,
+        showClose: true
+      })
+    }
+  }
+}
+
+function onSortLeft(accessory: Accessory): void {
+  const sortOrder = accessory.sortOrder
+  const maxValue =
+    max(accessoryList.value.filter((a) => !a.deleted && a.sortOrder < sortOrder).map((a) => a.sortOrder)) ?? 0
+  if (maxValue < 1) {
+    return
+  }
+  for (const item of accessoryList.value.filter((a) => a.sortOrder === maxValue)) {
+    item.sortOrder = sortOrder
+  }
+  accessory.sortOrder = maxValue
+  emits('update:modelValue', modelList.value)
+}
+
+function onSortRight(accessory: Accessory): void {
+  const sortOrder = accessory.sortOrder
+  const minValue =
+    min(accessoryList.value.filter((a) => !a.deleted && a.sortOrder > sortOrder).map((a) => a.sortOrder)) ?? 0
+  console.log(minValue)
+  if (minValue < 1) {
+    return
+  }
+  for (const item of accessoryList.value.filter((a) => a.sortOrder === minValue)) {
+    item.sortOrder = sortOrder
+  }
+  accessory.sortOrder = minValue
+  emits('update:modelValue', modelList.value)
+}
+
+provide(accessoryInjectionKey, {
+  downloadingList,
+  accessoryList
+})
+</script>
+
+<style lang="less" scoped>
+.uploader {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  width: 100%;
+  &-tag {
+    &-inner {
+      a {
+        cursor: pointer;
+
+        &:hover {
+          text-decoration: underline;
+        }
+      }
+    }
+  }
+}
+.file-list {
+  width: 100%;
+}
+</style>

+ 69 - 0
government-demo-web/src/components/accessory/UploadFileItem.vue

@@ -0,0 +1,69 @@
+<template>
+  <li class="file-item" :class="{ 'is-error': file.status === 'fail' }">
+    <el-icon :class="{ 'is-loading': file.status === 'uploading' }"><component :is="statusIcon" /></el-icon>
+    <span class="file-item-name">{{ file.name }}</span>
+    <el-icon v-if="!disabled" @click="onDelete" size="16" class="close"><Close /></el-icon>
+  </li>
+</template>
+<script lang="ts" setup>
+import { computed } from 'vue'
+import { type UploadUserFile } from 'element-plus'
+
+const props = withDefaults(
+  defineProps<{
+    file: UploadUserFile
+    disabled?: boolean
+  }>(),
+  {
+    disabled: true
+  }
+)
+// 取消文件上传
+const emits = defineEmits<{
+  (e: 'on-abort', value: UploadUserFile): void
+}>()
+const statusIcon = computed(() => {
+  if (props.file.status === 'ready' || props.file.status === 'uploading') {
+    return 'Loading'
+  } else if (props.file.status === 'fail') {
+    return 'Warning'
+  } else {
+    return 'Document'
+  }
+})
+function onDelete() {
+  emits('on-abort', props.file)
+}
+</script>
+<style lang="less" scoped>
+.file-item {
+  height: 30px;
+  line-height: 30px;
+  font-size: var(--font-size-s);
+  padding: 0 var(--cacp-padding-space-s);
+  display: flex;
+  align-items: center;
+  &:hover {
+    background-color: var(--el-fill-color-light);
+  }
+  &.is-error {
+    background-color: var(--el-color-error-light-9);
+    .file-item-name {
+      color: var(--el-color-error);
+    }
+  }
+}
+.file-item-name {
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  margin: 0 var(--cacp-margin-space-s);
+}
+.close {
+  cursor: pointer;
+  &:hover {
+    color: var(--el-color-primary);
+  }
+}
+// .file-item
+</style>

+ 14 - 0
government-demo-web/src/components/accessory/constants.ts

@@ -0,0 +1,14 @@
+import type { Accessory } from '@cacp/ui'
+import type { Ref, InjectionKey, ComputedRef } from 'vue'
+
+export type AccessoryUploaderContext = {
+  downloadingList: Ref<Array<string>>
+  accessoryList: ComputedRef<Array<Accessory>>
+}
+
+export const accessoryInjectionKey: InjectionKey<AccessoryUploaderContext> = Symbol('AccessoryUploader')
+
+export enum SortOrder {
+  left,
+  right
+}

+ 40 - 0
government-demo-web/src/components/editTable/EditTableColumn.vue

@@ -0,0 +1,40 @@
+<template>
+    <el-table-column v-bind="props">
+        <template #append="scoped">
+            <slot name="append" v-bind="scoped"></slot>
+        </template>
+        <template #default="scoped">
+            <slot v-bind="scoped">
+                <template v-if="!isEdit || !scoped.row?.isEdit">
+                    {{ defaultRenderCell(scoped) }}
+                </template>
+                <template v-else>
+                    <component :is="comp" v-bind="inputProps" :scoped="scoped"
+                        v-model="scoped.row[scoped.column.property || scoped.column.prop]">
+                        <template v-if="inputChild?.length">
+                            <component v-for="(child, index) in inputChild" :key="index" :is="child"></component>
+                        </template>
+                    </component>
+                </template>
+            </slot>
+        </template>
+        <template #filterIcon="scoped">
+            <slot name="filterIcon" v-bind="scoped"></slot>
+        </template>
+    </el-table-column>
+</template>
+<script lang="ts" setup>
+import { computed } from 'vue';
+import type { EditTableColumnProps } from './type';
+import { defaultRenderCell } from 'element-plus/es/components/table/src/config'
+import { getComp } from './registerComp';
+
+const props = withDefaults(defineProps<Partial<EditTableColumnProps>>(), {
+    inputName: 'input',
+    inputProps: () => ({}),
+    isEdit: true
+})
+
+const comp = computed(() => getComp(props.inputName))
+
+</script>

+ 5 - 0
government-demo-web/src/components/editTable/index.ts

@@ -0,0 +1,5 @@
+export { registerComp } from './registerComp'
+export * from './type'
+import EditTableColumn from './EditTableColumn.vue'
+
+export default EditTableColumn

+ 23 - 0
government-demo-web/src/components/editTable/registerComp.ts

@@ -0,0 +1,23 @@
+import { ElInput } from 'element-plus'
+import type { Component } from 'vue'
+
+const compMap = new Map<string, Component>()
+
+export function getComp(name: string) {
+  const comp = compMap.get(name)
+  if (comp) {
+    return comp
+  }
+  throw Error(`editTableColumn:没有注册${name}组件`)
+}
+
+export function registerComp(name: string, comp: Component) {
+  if (compMap.has(name)) {
+    throw Error(`editTableColumn:${name}已注册`)
+  }
+  compMap.set(name, comp)
+}
+
+(() => {
+  registerComp('input', ElInput)
+})()

+ 9 - 0
government-demo-web/src/components/editTable/type.ts

@@ -0,0 +1,9 @@
+import type { TableColumnCtx } from 'element-plus'
+import type { VNode } from 'vue'
+
+export interface EditTableColumnProps<T = object> extends Omit<TableColumnCtx<T>, 'children'> {
+  inputName: string
+  inputProps: object
+  inputChild: VNode[]
+  isEdit: boolean
+}

+ 85 - 0
government-demo-web/src/components/workflow/ProcessComments.vue

@@ -0,0 +1,85 @@
+<template>
+  <div>
+    <el-form ref="commentRef" :model="cmts">
+      <el-form-item label="个人意见">
+        <el-input
+          type="textarea"
+          :model-value="cmts?.userComments"
+          :rows="5"
+          show-word-limit
+          :maxlength="1000"
+          :readonly="!props.canEdit"
+          @update:modelValue="onUpdateUserComments"
+        ></el-input>
+      </el-form-item>
+      <el-form-item label="意见附件:">
+        <accessory-uploader :model-value="cmts?.userAccessoryList" @update:modelValue="onUpdateUserAccessories" category="workflow" :readonly="!props.canEdit" />
+      </el-form-item>
+
+      <el-form-item label="部门意见" v-if="props.deptShow">
+        <el-input
+          type="textarea"
+          :model-value="cmts?.deptComments"
+          :rows="5"
+          show-word-limit
+          :maxlength="1000"
+          :readonly="!props.canEdit"
+          @update:modelValue="onUpdateDeptComments"
+        ></el-input>
+      </el-form-item>
+      <el-form-item label="部门附件:" v-if="props.deptShow">
+        <accessory-uploader :model-value="cmts?.deptAccessoryList" @update:modelValue="onUpdateDeptAccessories" category="workflow" :readonly="!props.canEdit" />
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watchEffect } from 'vue'
+import type { FormInstance } from 'element-plus'
+import { cloneDeep } from 'lodash-es'
+
+import type { Accessory, CommentsDto } from '@cacp/ui'
+import AccessoryUploader from '@/components/accessory/AccessoryUploader.vue'
+
+const commentRef = ref<FormInstance>()
+
+const props = defineProps<{
+  comments: CommentsDto
+  canEdit: boolean
+  deptShow: boolean
+}>()
+const emits = defineEmits<{
+  (e: 'onUpdateComments', comments: CommentsDto): void
+}>()
+
+const cmts = ref<CommentsDto>()
+watchEffect(() => {
+  cmts.value = cloneDeep(props.comments)
+  // cmts.value = props.comments
+})
+
+function onUpdateUserComments(val: string) {
+  cmts.value!.userComments = val
+  cmts.value!.userModified = true
+  emits('onUpdateComments', cmts.value!)
+}
+
+function onUpdateDeptComments(val: string) {
+  cmts.value!.deptComments = val
+  cmts.value!.deptModified = true
+  emits('onUpdateComments', cmts.value!)
+}
+
+function onUpdateUserAccessories(val: Array<Accessory>) {
+  cmts.value!.userAccessoryList = val
+  cmts.value!.userModified = true
+  emits('onUpdateComments', cmts.value!)
+}
+
+function onUpdateDeptAccessories(val: Array<Accessory>) {
+  cmts.value!.deptAccessoryList = val
+  cmts.value!.deptModified = true
+  emits('onUpdateComments', cmts.value!)
+}
+</script>

+ 478 - 0
government-demo-web/src/components/workflow/ProcessOperation.vue

@@ -0,0 +1,478 @@
+<template>
+  <el-card class="operation" shadow="hover">
+    <template #header>
+      <div class="operation-head">
+        <el-icon><Stamp /></el-icon>
+        <span>{{ state.resp?.activityDescriptor.name }}</span>
+      </div>
+    </template>
+
+    <el-form v-if="canCopy">
+      <el-form-item label="抄送">
+        <el-checkbox-group v-model="state.copyOguList">
+          <el-checkbox v-for="item in state.resp.copyAssigns" :key="item.fullPathName" :label="item.oguName" :value="item.fullPathName" :title="item.fullPathName" />
+        </el-checkbox-group>
+      </el-form-item>
+    </el-form>
+
+    <comments
+      v-if="state.comments"
+      :comments="state.comments"
+      :can-edit="editable"
+      :dept-show="deptCommentsEditable"
+      @on-update-comments="onUpdateComments"
+    />
+
+    <div class="operation-body">
+      <el-button
+        v-if="canAgree"
+        type="primary"
+        plain
+        :disabled="loading"
+        :loading="loading && state.currOperation === 'agree'"
+        @click="onAgree"
+      >
+        <el-icon><Right /></el-icon>
+        <span>流转</span>
+      </el-button>
+      <el-button
+        v-if="canSave"
+        type="success"
+        plain
+        :disabled="loading"
+        :loading="loading && state.currOperation === 'save'"
+        @click="onSave"
+      >
+        <el-icon><DocumentChecked /></el-icon>
+        <span>保存</span>
+      </el-button>
+      <el-button
+        v-if="canBack"
+        type="warning"
+        plain
+        :disabled="loading"
+        :loading="loading && state.currOperation === 'back'"
+        @click="onBack"
+      >
+        <el-icon><Back /></el-icon>
+        <span>退回</span>
+      </el-button>
+      <el-button
+        v-if="canRevoke"
+        type="warning"
+        plain
+        :disabled="loading"
+        :loading="loading && state.currOperation === 'revoke'"
+        @click="onRevoke"
+      >
+        <el-icon><Back /></el-icon>
+        <span>撤回</span>
+      </el-button>
+      <el-button
+        v-if="canSuspend"
+        type="warning"
+        plain
+        :disabled="loading"
+        :loading="loading && state.currOperation === 'suspend'"
+        @click="onSuspend"
+      >
+        <el-icon><Back /></el-icon>
+        <span>挂起</span>
+      </el-button>
+      <el-button
+        v-if="canReject"
+        type="warning"
+        plain
+        :disabled="loading"
+        :loading="loading && state.currOperation === 'reject'"
+        @click="onReject"
+      >
+        <el-icon><Close /></el-icon>
+        <span>驳回</span>
+      </el-button>
+      <el-button
+        v-if="canDelete"
+        type="warning"
+        plain
+        :disabled="loading"
+        :loading="loading && state.currOperation === 'delete'"
+        @click="onDelete"
+      >
+        <el-icon><Close /></el-icon>
+        <span>删除</span>
+      </el-button>
+    </div>
+    <operation-route
+      :visible="state.routeVisible"
+      :get-route-list="getRouteList"
+      @on-close="onCloseRoute"
+      @on-save="onSaveRoute"
+    />
+  </el-card>
+</template>
+
+<script setup lang="ts" generic="T extends BaseWorkflowForm">
+import { computed, reactive, onMounted } from 'vue'
+import { ElMessage, type FormInstance } from 'element-plus'
+
+import { SuccessResultCode, type Result, type FrameUserInfo, type CreateRequestDto,
+  type AgreeAutoRequestDto,
+  type OperateRequestDto,
+  type StartResponseDto,
+  type BaseWorkflowForm,
+  type OperationType,
+  type RouteRequestDto,
+  type RuntimeRouteNode,
+  type AgreeRequestDto,
+  type CommentsDto,
+  type SaveRequestDto,
+  type NextRouteData,
+  type OperMode, useLoading, 
+  type RuntimeOgu} from '@cacp/ui'
+
+  import * as apis from '@/apis/workflow/engine'
+import OperationRoute from './ProcessOperationRoute.vue'
+import Comments from './ProcessComments.vue'
+
+const props = defineProps<{
+  contextPath: string
+  mode: OperMode
+  taskId?: string
+  formId?: string
+  processCode: string
+  urlFormat: string
+  range: number
+  user: FrameUserInfo
+  variables?: Record<string, unknown>
+  nextBranchList: Array<RuntimeOgu>
+  form?: T
+  formRef?: FormInstance,
+}>()
+
+const emits = defineEmits<{
+  (e: 'on-command', flag: OperationType): void
+  (e: 'on-load', form: T, processCode: string, activityCode: string, editable: boolean): void
+}>()
+
+const { loading, setLoading } = useLoading()
+
+const state = reactive<{
+  currOperation?: OperationType
+  resp?: Omit<StartResponseDto<T>, 'form' | 'comments'>
+  comments?: CommentsDto
+  routeVisible: boolean,
+  copyOguList: Array<RuntimeOgu>
+}>({
+  routeVisible: false,
+  copyOguList: []
+})
+
+const editable = computed(() => props.mode !== 'show' && !!state.resp?.activityDescriptor && !!state.resp?.canApprove)
+const canAgree = computed(() => editable.value && !!state.resp?.canOperate.canAgree)
+const canBack = computed(() => editable.value && !!state.resp?.canOperate.canBack)
+const canSave = computed(() => editable.value && !!state.resp?.canOperate.canSave)
+const canDelete = computed(() => editable.value && !!state.resp?.canOperate.canDelete)
+const canSuspend = computed(() => editable.value && !!state.resp?.canOperate.canSuspend)
+const canRevoke = computed(() => !!state.resp?.canOperate.canRevoke)
+const canReject = computed(() => editable.value && !!state.resp?.canOperate.canReject)
+const canRoute = computed(() => editable.value && !!state.resp?.activityDescriptor.routeOptional)
+const canCopy = computed(() => editable.value && !!state.resp?.canCopy)
+const deptCommentsEditable = computed(() => editable.value && !!state.resp?.activityDescriptor.options.operation_deptComments && props.form?.processId !== state.resp?.task.processId)
+
+onMounted(async () => {
+  setLoading(true)
+
+  let res: Result<StartResponseDto<T>>
+  if (props.mode === 'oper') {
+    res = await apis.start<T>(props.contextPath, props.taskId!, props.formId!)
+  } else if (props.mode === 'create') {
+    const req: CreateRequestDto = {
+      urlFormat: props.urlFormat!,
+      processCode: props.processCode!,
+      range: props.range!
+    }
+    res = await apis.createStart<T>(props.contextPath, req)
+  } else {
+    res = await apis.show<T>(props.contextPath, props.taskId!, props.formId!)
+  }
+  
+  if (res.code === SuccessResultCode && res.data) {
+    state.resp = {
+      task: res.data.task,
+      processCode: res.data.processCode,
+      activityDescriptor: res.data.activityDescriptor,
+      canApprove: res.data.canApprove,
+      canOperate: res.data.canOperate,
+      canCopy: res.data.canCopy,
+      copyNodeCode: res.data.copyNodeCode,
+      copyAssigns: res.data.copyAssigns
+    }
+    state.copyOguList = res.data.copyAssigns
+    
+    state.comments = res.data.comments
+    emits('on-load', res.data.form, res.data.processCode, res.data.activityDescriptor.code, editable.value)
+  }
+
+  setLoading(false)
+})
+
+async function onAgree() {
+  state.currOperation = 'agree'
+
+  const valid = await props.formRef!.validate()
+  if (!valid) {
+    return
+  }
+
+  if (canRoute.value) {
+    state.routeVisible = true
+  } else {
+    state.copyOguList.forEach(a => a.copyFlag = true)
+    const req: AgreeAutoRequestDto<T> = {
+      taskId: state.resp!.task.taskId,
+      comments: state.comments,
+      copyNodeCode: state.resp!.copyNodeCode,
+      copyOguList: state.copyOguList,
+      nextBranchList: props.nextBranchList,
+      form: props.form!,
+      variables: props.variables
+    }
+
+    setLoading(true)
+    const res: Result<boolean> = await apis.autoAgree(props.contextPath, req)
+    setLoading(false)
+
+    if (res.code !== SuccessResultCode) {
+      ElMessage.error({
+        message: res.message,
+        showClose: true
+      })
+
+      return
+    }
+
+    emits('on-command', state.currOperation)
+  }
+}
+
+async function onSave() {
+  state.currOperation = 'save'
+
+  const valid = await props.formRef!.validate()
+  if (!valid) {
+    return
+  }
+
+  const req: SaveRequestDto<T> = {
+    taskId: state.resp!.task.taskId,
+    comments: state.comments,
+    form: props.form!
+  }
+
+  setLoading(true)
+  const res: Result<boolean> = await apis.save(props.contextPath, req)
+  setLoading(false)
+
+  if (res.code !== SuccessResultCode) {
+    ElMessage.error({
+      message: res.message,
+      showClose: true
+    })
+
+    return
+  }
+
+  emits('on-command', state.currOperation)
+}
+
+async function onBack() {
+  state.currOperation = 'back'
+
+  const valid = await props.formRef!.validate()
+  if (!valid) {
+    return
+  }
+
+  const req: OperateRequestDto<T> = {
+    taskId: state.resp!.task.taskId,
+    comments: state.comments,
+    form: props.form!
+  }
+
+  setLoading(true)
+  const res: Result<boolean> = await apis.back(props.contextPath, req)
+  setLoading(false)
+
+  if (res.code !== SuccessResultCode) {
+    ElMessage.error({
+      message: res.message,
+      showClose: true
+    })
+
+    return
+  }
+
+  emits('on-command', state.currOperation)
+}
+
+async function onRevoke() {
+  state.currOperation = 'revoke'
+
+  setLoading(true)
+  const res: Result<boolean> = await apis.revoke(props.contextPath, state.resp!.task.taskId, state.resp!.task.bizId)
+  setLoading(false)
+
+  if (res.code !== SuccessResultCode) {
+    ElMessage.error({
+      message: res.message,
+      showClose: true
+    })
+
+    return
+  }
+
+  emits('on-command', state.currOperation)
+}
+
+async function onReject() {
+  state.currOperation = 'reject'
+
+  const valid = await props.formRef!.validate()
+  if (!valid) {
+    return
+  }
+
+  const req: OperateRequestDto<T> = {
+    taskId: state.resp!.task.taskId,
+    comments: state.comments,
+    form: props.form!
+  }
+
+  setLoading(true)
+  const res: Result<boolean> = await apis.reject(props.contextPath, req)
+  setLoading(false)
+
+  if (res.code !== SuccessResultCode) {
+    ElMessage.error({
+      message: res.message,
+      showClose: true
+    })
+
+    return
+  }
+
+  emits('on-command', state.currOperation)
+}
+
+async function onSuspend() {
+  state.currOperation = 'suspend'
+
+  const valid = await props.formRef!.validate()
+  if (!valid) {
+    return
+  }
+
+  const req: OperateRequestDto<T> = {
+    taskId: state.resp!.task.taskId,
+    comments: state.comments,
+    form: props.form!
+  }
+
+  setLoading(true)
+  const res: Result<boolean> = await apis.suspend(props.contextPath, req)
+  setLoading(false)
+
+  if (res.code !== SuccessResultCode) {
+    ElMessage.error({
+      message: res.message,
+      showClose: true
+    })
+
+    return
+  }
+
+  emits('on-command', state.currOperation)
+}
+
+async function onDelete() {
+  state.currOperation = 'delete'
+
+  setLoading(true)
+  const res: Result<boolean> = await apis.remove(props.contextPath, state.resp!.task.taskId, state.resp!.task.bizId)
+  setLoading(false)
+
+  if (res.code !== SuccessResultCode) {
+    ElMessage.error({
+      message: res.message,
+      showClose: true
+    })
+
+    return
+  }
+
+  emits('on-command', state.currOperation)
+}
+
+async function getRouteList(): Promise<Array<RuntimeRouteNode>> {
+  const valid = await props.formRef!.validate()
+  if (!valid) {
+    return []
+  }
+
+  const req: RouteRequestDto = {
+    taskId: state.resp!.task.taskId,
+    variables: props.variables
+  }
+
+  const res = await apis.route(props.contextPath, req)
+  if (res.code !== SuccessResultCode) {
+    ElMessage.error({
+      message: res.message,
+      showClose: true
+    })
+
+    return []
+  }
+
+  return res.data ?? []
+}
+
+function onCloseRoute() {
+  state.routeVisible = false
+}
+
+async function onSaveRoute(routeData: NextRouteData) {
+  state.currOperation = 'agree'
+
+  state.copyOguList.forEach(a => a.copyFlag = true)
+  const req: AgreeRequestDto<T> = {
+    taskId: state.resp!.task.taskId,
+    comments: state.comments,
+    routeNodeCode: routeData.nextNodeCode,
+    routeOguList: props.nextBranchList && props.nextBranchList.length > 0 ? props.nextBranchList : routeData.nextOguList,
+    copyNodeCode: state.resp!.copyNodeCode,
+    copyOguList: state.copyOguList,
+    form: props.form!,
+    variables: props.variables
+  }
+
+  setLoading(true)
+  const res: Result<boolean> = await apis.agree(props.contextPath, req)
+  setLoading(false)
+
+  if (res.code !== SuccessResultCode) {
+    ElMessage.error({
+      message: res.message,
+      showClose: true
+    })
+
+    return
+  }
+
+  emits('on-command', state.currOperation)
+}
+
+function onUpdateComments(comments: CommentsDto) {
+  state.comments = comments
+}
+</script>

+ 117 - 0
government-demo-web/src/components/workflow/ProcessOperationRoute.vue

@@ -0,0 +1,117 @@
+<template>
+  <el-dialog
+    :model-value="visible"
+    title="选择审批节点"
+    :width="600"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    :destroy-on-close="true"
+    :append-to-body="true"
+    @open="onOpen"
+    @close="onClose"
+  >
+    <el-form ref="routeRef" :model="state.routeForm">
+      <el-form-item label="流转节点" prop="nextNodeCode">
+        <el-select v-model="state.routeForm.nextNodeCode" @change="onChangeNextNode">
+          <el-option v-for="item in state.routeList" :key="item.code" :value="item.code" :label="item.name" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="审批用户" prop="nextOguList">
+        <el-select
+          v-model="state.routeForm.nextOguList"
+          multiple
+          :disabled="!state.routeForm.nextNodeCode"
+          filterable
+        >
+          <el-option v-for="ogu in oguList" :key="ogu.fullPathName" :value="ogu.fullPathName" :label="ogu.oguName" />
+        </el-select>
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <el-button type="primary" @click="onSave">确定</el-button>
+      <el-button @click="onClose">取消</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed } from 'vue'
+import { type FormInstance } from 'element-plus'
+
+import { type NextRouteData, type RuntimeOgu, type RuntimeRouteNode, useLoading } from '@cacp/ui'
+
+const props = defineProps<{
+  visible: boolean
+  getRouteList: () => Promise<Array<RuntimeRouteNode>>
+}>()
+
+const emits = defineEmits<{
+  (e: 'on-save', res: NextRouteData): void
+  (e: 'on-close'): void
+}>()
+
+const { loading, setLoading } = useLoading()
+const routeRef = ref<FormInstance>()
+const state = reactive<{
+  routeList: Array<RuntimeRouteNode>
+  routeForm: {
+    nextNodeCode: string
+    nextOguList: Array<string>
+  }
+}>({
+  routeList: [],
+  routeForm: {
+    nextNodeCode: '',
+    nextOguList: []
+  }
+})
+
+const oguList = computed(() => {
+  if (!state.routeForm.nextNodeCode) {
+    return []
+  }
+
+  const r = state.routeList.find((r) => r.code === state.routeForm.nextNodeCode)
+  return r?.assigns ?? []
+})
+
+async function onOpen() {
+  state.routeList = []
+  state.routeForm.nextNodeCode = ''
+  state.routeForm.nextOguList = []
+
+  await loadData()
+}
+
+function onClose() {
+  emits('on-close')
+}
+
+function onSave() {
+  const r = state.routeList.find(i => i.code === state.routeForm.nextNodeCode)
+  const nextOguList = r?.assigns.filter((a: RuntimeOgu) => state.routeForm.nextOguList.some(no => no === a.fullPathName)) ?? []
+  const res: NextRouteData = {
+    nextNodeCode: state.routeForm.nextNodeCode,
+    nextOguList: nextOguList
+  }
+
+  emits('on-save', res)
+}
+
+async function loadData() {
+  setLoading(true)
+  const res = await props.getRouteList()
+  state.routeList = res
+  setLoading(false)
+}
+
+function onChangeNextNode(code: string) {
+  if (!code) {
+    state.routeForm.nextOguList = []
+  } else {
+    const r = state.routeList.find(i => i.code === code)
+    state.routeForm.nextOguList = r?.assigns.map((a: RuntimeOgu) => a.fullPathName) ?? []
+  }
+}
+</script>

+ 56 - 0
government-demo-web/src/components/workflow/ProcessTraceContainer.vue

@@ -0,0 +1,56 @@
+<template>
+  <el-card shadow="hover" :body-style="{ padding: 0 }">
+    <template #header>
+      <span>
+        <el-icon><LocationFilled /></el-icon>
+        <span style="margin-left: 5px">流程跟踪</span>
+      </span>
+    </template>
+    <div>
+      <el-skeleton v-if="loading" :rows="5" animated />
+      <trace-list v-else :trace="state.trace" @on-trace-child="onTraceChild" />
+      <trace-detail :visible="state.trace && state.trace.activities.length > 0 && state.visible" :assign-id="state.assignId" @on-close="state.visible = false" />
+    </div>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { onBeforeMount, reactive } from 'vue'
+
+import { SuccessResultCode, type TraceInfo, useLoading } from '@cacp/ui'
+
+import * as apis from '@/apis/workflow/runtime'
+import TraceList from './ProcessTraceList.vue'
+import TraceDetail from './ProcessTraceDetail.vue'
+
+const { loading, setLoading } = useLoading()
+
+const props = defineProps<{
+  processId: string
+}>()
+
+const state = reactive<{
+  trace?: TraceInfo
+  visible: boolean
+  assignId: string
+}>({
+  visible: false,
+  assignId: ''
+})
+
+onBeforeMount(async () => {
+  setLoading(true)
+
+  const res = await apis.trace(props.processId)
+  if (res.code === SuccessResultCode && res.data) {
+    state.trace = res.data!
+  }
+
+  setLoading(false)
+})
+
+function onTraceChild(assignId: string) {
+  state.visible = true
+  state.assignId = assignId
+}
+</script>

+ 70 - 0
government-demo-web/src/components/workflow/ProcessTraceDetail.vue

@@ -0,0 +1,70 @@
+<template>
+  <el-dialog
+    :model-value="visible"
+    title="查看子流程"
+    :width="600"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    :destroy-on-close="true"
+    :append-to-body="true"
+    @open="onOpen"
+    @close="onClose"
+  >
+    <el-skeleton v-if="loading" :rows="5" animated />
+    <trace-list v-else :process="state.trace" @on-trace-child="onTraceChild" />
+    <trace-detail
+      :visible="state.trace && state.trace.activities.length > 0 && state.visible"
+      :assign-id="state.assignId"
+      @on-close="state.visible = false"
+    />
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive } from 'vue'
+
+import { SuccessResultCode, type TraceInfo, useLoading } from '@cacp/ui'
+
+import * as apis from '@/apis/workflow/runtime'
+import TraceList from './ProcessTraceList.vue'
+import TraceDetail from './ProcessTraceDetail.vue'
+
+const { loading, setLoading } = useLoading()
+
+const props = defineProps<{
+  visible: boolean
+  assignId: string
+}>()
+const emits = defineEmits<{
+  (e: 'on-close'): void
+}>()
+
+const state = reactive<{
+  trace?: TraceInfo
+  visible: boolean
+  assignId: string
+}>({
+  visible: false,
+  assignId: ''
+})
+
+async function onOpen() {
+  setLoading(true)
+
+  const res = await apis.traceBranch(props.assignId)
+  if (res.code === SuccessResultCode && res.data) {
+    state.trace = res.data!
+  }
+
+  setLoading(false)
+}
+
+function onClose() {
+  emits('on-close')
+}
+
+function onTraceChild(assignId: string) {
+  state.visible = true
+  state.assignId = assignId
+}
+</script>

+ 115 - 0
government-demo-web/src/components/workflow/ProcessTraceList.vue

@@ -0,0 +1,115 @@
+<template>
+  <el-timeline>
+    <el-timeline-item
+      v-for="activity in props.trace.activities"
+      :key="activity.activityId"
+      :timestamp="displayTime(activity.completeTime)"
+      :hide-timestamp="!displayTime(activity.completeTime)"
+    >
+      <div>
+        <el-tag :effect="activity.nodeType === 'VIRTUAL' ? 'plain' : 'light'">
+          <el-icon><component :is="getIcon(activity.nodeType)" /></el-icon>
+          {{ activity.nodeName }}
+        </el-tag>
+
+        <div style="margin-top: 10px">
+          <el-table :data="activity.assignments" border stripe row-key="assignId">
+            <el-table-column prop="receiverName" label="审批人" :width="120">
+              <template #default="scope">
+                <span :title="scope.row.receiverFullPath">{{ scope.row.receiverName }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="operateTime" label="审批时间" :width="120">
+              <template #default="scope">{{ displayTime(scope.row.operateTime) }}</template>
+            </el-table-column>
+            <el-table-column prop="operateStatus" label="操作" :width="120">
+              <template #default="scope">
+                <el-tag v-if="scope.row.operateStatus && scope.row.operateStatus !== 'NONE'" :type="getNodeStatusType(scope.row.operateStatus)">
+                  {{ getNodeStatusName(scope.row.operateStatus) }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="comments" label="审批意见" show-overflow-tooltip :width="120"></el-table-column>
+            <el-table-column prop="accessoryList" label="意见附件" :width="120">
+              <template #default="scope">
+                <accessory-uploader :modelValue="scope.row.accessoryList" category="workflow" readonly />
+              </template>
+            </el-table-column>
+            <el-table-column :width="80">
+              <template #default="scope">
+                <el-button v-if="activity.nodeType === 'BRANCH'" @click="onTraceChild(scope.row.assignId)">
+                  <el-icon title="查看子流程"><ZoomIn /></el-icon>
+                </el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </div>
+    </el-timeline-item>
+  </el-timeline>
+</template>
+
+<script setup lang="ts">
+import dayjs from 'dayjs'
+import type { TagProps } from 'element-plus'
+
+import type { TraceInfo, NodeDescriptorType, OperateStatus } from '@cacp/ui'
+
+import AccessoryUploader from '@/components/accessory/AccessoryUploader.vue'
+
+type TagType = TagProps['type']
+
+const props = defineProps<{
+  trace?: TraceInfo
+}>()
+const emits = defineEmits<{
+  (e: 'on-trace-child', assignId: string): void
+}>()
+
+function displayTime(dt: string) {
+  return dt ? dayjs(dt).format('YYYY-MM-DD HH:mm') : ''
+}
+
+function getIcon(nodeType: NodeDescriptorType) {
+  if (nodeType === 'START') {
+    return 'VideoPlay'
+  } else if (nodeType === 'END') {
+    return 'CircleCheck'
+  } else if (nodeType === 'BRANCH') {
+    return 'FolderAdd'
+  } else {
+    return 'Memo'
+  }
+}
+
+function getNodeStatusType(status: OperateStatus): TagType | undefined {
+  if (status === 'AGREE') {
+    return 'primary'
+  } else if (status === 'BACK') {
+    return 'warning'
+  } else if (status === 'REJECT') {
+    return 'danger'
+  } else if (status === 'REVOKE') {
+    return 'info'
+  }
+}
+
+function getNodeStatusName(status: OperateStatus) {
+  if (status === 'AGREE') {
+    return '流转'
+  } else if (status === 'BACK') {
+    return '退回'
+  } else if (status === 'REJECT') {
+    return '驳回'
+  } else if (status === 'REVOKE') {
+    return '撤回'
+  } else {
+    return ''
+  }
+}
+
+function onTraceChild(assignId: string) {
+  emits('on-trace-child', assignId)
+}
+</script>
+

+ 18 - 0
government-demo-web/src/config.ts

@@ -0,0 +1,18 @@
+import type { CacpConfig } from '@cacp/ui'
+
+const devConfig: CacpConfig = {
+  SERVICE_ID: 'demo',
+  SERVICE_NAME: 'Demo',
+//  SERVICE_API: 'http://10.200.73.47:18003',
+  SERVICE_API: 'http://127.0.0.1:18003',
+  SERVICE_PAGESIZE: 20,
+  SERVICE_TIMEOUT: 10000,
+//  FRAME_API: 'http://10.200.73.47:15090',
+  FRAME_API: 'http://127.0.0.1:15090',
+  NEED_USER_AUTHORITY: true,
+  AUTH_MODE: 'Cookie'
+}
+
+const $config = (window as any).$config as CacpConfig
+const { DEV } = import.meta.env
+export default DEV ? devConfig : $config

+ 8 - 0
government-demo-web/src/directives/index.ts

@@ -0,0 +1,8 @@
+import type { App } from 'vue'
+import permission from './permission'
+
+export default {
+  install(Vue: App) {
+    Vue.directive('permission', permission)
+  },
+}

+ 35 - 0
government-demo-web/src/directives/permission.ts

@@ -0,0 +1,35 @@
+import { useCoreStore } from '@/stores'
+import type { DirectiveBinding, Directive } from 'vue'
+
+function check(el: HTMLElement, binding: DirectiveBinding<string>) {
+  const { value } = binding
+  const coreStore = useCoreStore()
+  const currentUser = coreStore.currentUser
+  const authorities = coreStore.userAuthority?.permissions
+
+  let flag: boolean = false
+
+  if (!value) {
+    flag = true
+  } else {
+    const permissions = value.split(',')
+    if (currentUser && authorities) {
+      flag = authorities.some((a) => permissions.includes(a.code))
+    }
+  }
+
+  if (!flag) {
+    el.parentNode?.removeChild(el)
+  }
+}
+
+const permission: Directive = {
+  mounted(el: HTMLElement, binding: DirectiveBinding<string>) {
+    check(el, binding)
+  },
+  updated(el: HTMLElement, binding: DirectiveBinding<string>) {
+    check(el, binding)
+  },
+}
+
+export default permission

+ 0 - 0
government-demo-web/src/index.d.ts


+ 34 - 0
government-demo-web/src/main.ts

@@ -0,0 +1,34 @@
+import { createApp } from 'vue'
+import ElementPlus from 'element-plus'
+import zhCn from 'element-plus/es/locale/lang/zh-cn'
+import CacpUI from '@cacp/ui'
+
+import 'normalize.css/normalize.css'
+import 'element-plus/dist/index.css' //先引入element plus样式
+import '@cacp/ui/dist/index.css' //后引入@cacp/ui样式
+import './assets/main.less'
+
+
+import App from './App.vue'
+import router from './router'
+import plugins from './plugins'
+import directives from './directives'
+import { pinia } from './stores'
+import { addEventListener } from './utils/frame'
+
+
+const app = createApp(App)
+app.use(router)
+app.use(plugins)
+app.use(directives)
+app.use(pinia)
+
+// addEventListener
+addEventListener()
+
+app.use(CacpUI)
+app.use(ElementPlus, {
+  locale: zhCn,
+})
+
+app.mount('#app')

+ 10 - 0
government-demo-web/src/plugins/icon.ts

@@ -0,0 +1,10 @@
+import type { App } from 'vue'
+import * as IconsVue from '@cacp/svg-icons'
+
+export default {
+  install: (app: App<Element>) => {
+    for (const [key, component] of Object.entries(IconsVue)) {
+      app.component(key, component)
+    }
+  },
+}

+ 1 - 0
government-demo-web/src/plugins/index.ts

@@ -0,0 +1 @@
+export { default } from './icon'

+ 75 - 0
government-demo-web/src/router/app-routers.ts

@@ -0,0 +1,75 @@
+import type { RouteRecordRaw } from 'vue-router'
+
+const routers: Array<RouteRecordRaw> = [
+  {
+    path: 'designer',
+    name: 'Designer',
+    component: () => import('@/views/designer/ProcessDesignContainer.vue'),
+    meta: {
+      title: '模拟用户',
+      anonymous: false,
+      keepAlive: true
+    }
+  },
+  {
+    path: 'mock-user',
+    name: 'MockUser',
+    component: () => import('@/views/workflow/MockUserSelector.vue'),
+    meta: {
+      title: '模拟用户',
+      anonymous: false,
+      keepAlive: true
+    }
+  },
+  {
+    path: 'user-task-list',
+    name: 'UserTask',
+    component: () => import('@/views/workflow/UserTaskList.vue'),
+    meta: {
+      title: '用户待办',
+      anonymous: false,
+      keepAlive: true
+    }
+  },
+  {
+    path: 'process-list',
+    name: 'Process',
+    component: () => import('@/views/workflow/ProcessList.vue'),
+    meta: {
+      title: '当前流程列表',
+      anonymous: false,
+      keepAlive: true
+    }
+  },
+  {
+    path: 'gov-list',
+    name: 'Government',
+    component: () => import('@/views/government/GovernmentList.vue'),
+    meta: {
+      title: '政务表单列表',
+      anonymous: false,
+      keepAlive: true
+    }
+  },
+  {
+    path: 'gov-detail-oper',
+    name: 'GovernmentDetailOper',
+    component: () => import('@/views/government/GovernmentDetailOper.vue'),
+    meta: {
+      title: '政务表单流转',
+      anonymous: false,
+      keepAlive: true
+    }
+  },
+  {
+    path: 'gov-detail-show',
+    name: 'GovernmentDetailShow',
+    component: () => import('@/views/government/GovernmentDetailShow.vue'),
+    meta: {
+      title: '政务表单查看',
+      anonymous: false,
+      keepAlive: true
+    }
+  }
+]
+export default routers

+ 83 - 0
government-demo-web/src/router/index.ts

@@ -0,0 +1,83 @@
+import { createRouter, createWebHashHistory, RouterView } from 'vue-router'
+import appRouters from './app-routers'
+import { useCoreStore } from '@/stores'
+import HomeView from '@/views/HomeView.vue'
+
+const { DEV } = import.meta.env
+
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes: [
+    {
+      path: '/',
+      component: RouterView,
+      children: [
+        {
+          path: '/',
+          redirect: '/home',
+        },
+        {
+          path: '/home',
+          component: HomeView,
+          meta: {
+            title: '首页',
+            keepAlive: true,
+          },
+        },
+        // 应用中其他路由
+        ...appRouters,
+        {
+          path: 'error',
+          name: 'error',
+          component: () => import('@/views/ErrorView.vue'),
+          meta: {
+            title: '错误页面',
+            keepAlive: true,
+          },
+        },
+        {
+          path: ':pathMatch(.*)*',
+          name: 'NotFound',
+          component: () => import('@/views/ErrorView.vue'),
+          meta: {
+            title: '404 Not Found',
+            keepAlive: true,
+          },
+        },
+      ],
+    },
+  ],
+})
+
+// 全局路由守卫(登录拦截)
+router.beforeEach(async (to, from, next) => {
+  const coreStore = useCoreStore()
+  await coreStore.init()
+  const user = coreStore.currentUser
+  if (!user) {
+    next({
+      path: '/error', // login
+      query: {
+        redirect: to.fullPath,
+      },
+    })
+    return
+  }
+
+  if (DEV || !to.meta?.permissions) {
+    next()
+    return
+  }
+
+  const permissions: Array<string> = (to.meta.permissions as string).split(',')
+  const authorities = coreStore.userAuthority?.permissions ?? []
+  if (authorities.some((a) => permissions.includes(a.code))) {
+    next()
+  } else {
+    next({
+      path: '/error',
+    })
+  }
+})
+
+export default router

+ 42 - 0
government-demo-web/src/stores/app-stores.ts

@@ -0,0 +1,42 @@
+import { defineStore } from 'pinia'
+
+import { SuccessResultCode, type TypeDescriptor } from '@cacp/ui'
+import * as apis from '@/apis/workflow/runtime'
+
+interface GovernmentState {
+  isInited: boolean
+  formRuntimeStatusList: Array<TypeDescriptor>
+  processRuntimeStatusList: Array<TypeDescriptor>
+  taskTypeList: Array<TypeDescriptor>
+}
+
+export const useGovernmentStore = defineStore('government', {
+  state: (): GovernmentState => ({
+    isInited: false,
+    formRuntimeStatusList: [],
+    processRuntimeStatusList: [],
+    taskTypeList: []
+  }),
+  actions: {
+    async init() {
+      if (this.isInited) {
+        return
+      }
+
+      let res = await apis.getFormRuntimeStatusList()
+      if (res.code === SuccessResultCode && res.data) {
+        this.formRuntimeStatusList = res.data
+      }
+      res = await apis.getProcessRuntimeStatusList()
+      if (res.code === SuccessResultCode && res.data) {
+        this.processRuntimeStatusList = res.data
+      }
+      res = await apis.getTaskTypeList()
+      if (res.code === SuccessResultCode && res.data) {
+        this.taskTypeList = res.data
+      }
+      
+      this.isInited = true
+    }
+  }
+})

+ 55 - 0
government-demo-web/src/stores/core.ts

@@ -0,0 +1,55 @@
+//获取用户信息与权限
+import { defineStore } from 'pinia'
+import { getUserAuthority } from '@/apis/authority'
+// import { getFrameUser } from '@/apis/frame'
+import { type Result, type FrameUserInfo, type UserAuthorityInfo, SuccessResultCode, setTheme } from '@cacp/ui'
+import config from '@/config'
+import { getFrameUser } from '@/apis/mock' 
+
+interface CoreState {
+  isInited: boolean
+  isAuthInited: boolean
+  currentUser: FrameUserInfo | undefined
+  userAuthority: UserAuthorityInfo | undefined
+}
+
+export const useCoreStore = defineStore('core', {
+  state: (): CoreState => ({
+    isInited: false,
+    isAuthInited: false,
+    currentUser: undefined,
+    userAuthority: undefined
+  }),
+  actions: {
+    async init() {
+      if (config.NEED_USER_AUTHORITY || !this.isAuthInited) {
+        const res: Result<UserAuthorityInfo> = await getUserAuthority()
+        if (res.code === SuccessResultCode) {
+          this.$patch({
+            isAuthInited: true,
+            userAuthority: res.data
+          })
+        }
+      }
+      if (!this.isInited) {
+        const res: Result<FrameUserInfo> = await getFrameUser()
+        console.log('res', res)
+        if (res.code === SuccessResultCode) {
+          this.$patch({
+            isInited: true,
+            currentUser: res.data
+          })
+          setTheme(res.data.theme)
+        }
+      }
+    },
+    clear() {
+      this.$patch({
+        isInited: false,
+        isAuthInited: false,
+        currentUser: undefined,
+        userAuthority: undefined
+      })
+    }
+  }
+})

+ 48 - 0
government-demo-web/src/stores/designer-stores.ts

@@ -0,0 +1,48 @@
+import { defineStore } from 'pinia'
+
+import { SuccessResultCode, type TypeDescriptor } from '@cacp/ui'
+import * as apis from '@/apis/workflow/definition'
+
+interface ProcessDesignState {
+  isInited: boolean
+  assignmentTypeList: Array<TypeDescriptor>
+  strategyList: Array<TypeDescriptor>
+  flowTypeList: Array<TypeDescriptor>
+  nodeTypeList: Array<TypeDescriptor>
+}
+
+export const useDesignerStore = defineStore('designer', {
+  state: (): ProcessDesignState => ({
+    isInited: false,
+    assignmentTypeList: [],
+    strategyList: [],
+    flowTypeList: [],
+    nodeTypeList: []
+  }),
+  actions: {
+    async init() {
+      if (this.isInited) {
+        return
+      }
+
+      let res = await apis.getAssignmentTypeList()
+      if (res.code === SuccessResultCode && res.data) {
+        this.assignmentTypeList = res.data
+      }
+      res = await apis.getStrategyList()
+      if (res.code === SuccessResultCode && res.data) {
+        this.strategyList = res.data
+      }
+      res = await apis.getFlowTypeList()
+      if (res.code === SuccessResultCode && res.data) {
+        this.flowTypeList = res.data
+      }
+      res = await apis.getNodeTypeList()
+      if (res.code === SuccessResultCode && res.data) {
+        this.nodeTypeList = res.data
+      }
+      
+      this.isInited = true
+    }
+  }
+})

+ 4 - 0
government-demo-web/src/stores/index.ts

@@ -0,0 +1,4 @@
+export * from './core'
+export * from './app-stores'
+export * from './designer-stores'
+export { default as pinia } from './pinia'

+ 20 - 0
government-demo-web/src/stores/pinia.ts

@@ -0,0 +1,20 @@
+import { createPinia } from 'pinia'
+import { createPersistedState } from 'pinia-plugin-persistedstate'
+
+import config from '@/config'
+import { deserialize, serialize } from '@cacp/ui'
+
+const pinia = createPinia()
+pinia.use(
+  createPersistedState({
+    auto: true,
+    storage: sessionStorage,
+    key: (id) => `__${config.SERVICE_ID}__${id}`,
+    serializer: {
+      deserialize: deserialize,
+      serialize: serialize
+    }
+  })
+)
+
+export default pinia

+ 43 - 0
government-demo-web/src/types/government.ts

@@ -0,0 +1,43 @@
+import type { BaseWorkflowForm } from '@cacp/ui'
+
+export const contextPath = '/government'
+
+// const processCode = 'DemoSimpleProcess'
+export const processCode = 'DemoParentProcess'
+export const urlFormat = 'http://localhost:3000/#/gov-detail-oper?mode=oper'
+export const range = 2
+
+export interface GovernmentInfo extends BaseWorkflowForm {
+  formContent?: string
+}
+
+export const initGovernment: GovernmentInfo = {
+  formId: '',
+  appCode: '',
+  formTitle: '',
+  formStatus: 'NOSTART',
+  exists: true,
+  processId: '',
+  processCode: '',
+  processRange: 0,
+  issueUserId: '',
+  issueUserName: '',
+  issueUserParentId: '',
+  issueUserFullPathName: '',
+  issueDeptFullPathName: '',
+  createTime: ''
+}
+
+export interface GovernmentQueryInfo {
+  title: string
+  startDate: string
+  endDate: string
+  pageSize: number
+  pageIndex: number
+}
+
+export interface UserTaskQueryInfo {
+  type: string
+  pageSize: number
+  pageIndex: number
+}

+ 27 - 0
government-demo-web/src/types/process.ts

@@ -0,0 +1,27 @@
+import type { ProcessDescriptor } from '@cacp/ui'
+
+export const initProcessDescriptor: ProcessDescriptor = {
+  code: '',
+  name: '',
+  branch: false,
+  expireDays: 0,
+  nodes: [
+    {
+      type: 'START',
+      code: 'ISSUE',
+      name: '拟稿',
+      listeners: [],
+      options: {},
+      attrs: { x: 100, y: 80 }
+    },
+    {
+      type: 'END',
+      code: 'END',
+      name: '结束',
+      listeners: [],
+      options: {},
+      attrs: { x: 600, y: 80 }
+    },
+  ],
+  flows: []
+}

+ 10 - 0
government-demo-web/src/utils/authhelper.ts

@@ -0,0 +1,10 @@
+export function noAuth(code: string, addtionalMessage: string) {
+  if (top === window) {
+    return
+  } else {
+    top.postMessage(
+      { type: 'noAuth', code: code, addtionalMessage: addtionalMessage },
+      '*',
+    )
+  }
+}

+ 16 - 0
government-demo-web/src/utils/frame.ts

@@ -0,0 +1,16 @@
+// 监听门户发送过来的用户信息消息,
+//import { FRAME_REFRESH } from '@/cacp/ui'
+//import { useCoreStore } from '@/stores'
+
+// 对cookie和userInfo进行监听
+export function addEventListener() {
+//  const coreStore = useCoreStore()
+  window.addEventListener('message', (event: MessageEvent) => {
+    if (event.data !== null && !Array.isArray(event.data) && typeof event.data === 'object') {
+      // if (event.data?.type === FRAME_REFRESH) {
+      //   coreStore.clear()
+      //   window.location.reload()
+      // }
+    }
+  })
+}

+ 97 - 0
government-demo-web/src/utils/http.ts

@@ -0,0 +1,97 @@
+// 引用axios
+import axios from 'axios'
+import { h } from 'vue'
+import { ElLink, ElMessage, ElMessageBox } from 'element-plus'
+import { SuccessResultCode, SystemFailResultCode, WarningResultCode } from '@cacp/ui'
+import config from '@/config'
+
+const { DEV } = import.meta.env
+const Axios = axios.create({
+  // API 请求的默认前缀
+  timeout: config.SERVICE_TIMEOUT, // 请求超时时间
+  withCredentials: !DEV, // 自动携带cookie
+  headers: {
+    'Content-Type': 'application/json;charset=UTF-8'
+  }
+})
+
+// 请求拦截
+Axios.interceptors.request.use(
+  (req) => {
+    return req
+  },
+  (err) => {
+    ElMessage.error({ message: err.message, duration: 0, showClose: true })
+    return Promise.reject(err)
+  }
+)
+
+// 响应拦截
+Axios.interceptors.response.use(
+  (res) => {
+    const contentType = res.headers['content-type']
+    if (typeof contentType === 'string' && contentType.startsWith('application/json')) {
+      // 只处理后端返回Resut<T>JSON对象时的错误提示,其他格式原样返回(例如:mime是未知文件流下载或已知文件等类型 )
+      if (res.data && (res.data.code != SuccessResultCode || res.data.code != WarningResultCode) && res.data.message) {
+        ElMessage.error({
+          message: res.data.message,
+          duration: 0,
+          showClose: true
+        })
+      }
+    }
+    return res
+  },
+  (err) => {
+    if (err.response && err.response.data) {
+      const msg = err.response.data.message ?? '网络异常'
+      ElMessage.error({
+        message: h('div', [
+          msg,
+          h(
+            ElLink,
+            {
+              type: 'danger',
+              style: 'margin-left:12px',
+              onClick: () => {
+                ElMessageBox({
+                  title: msg,
+                  message: () =>
+                    h('div', { style: 'white-space:normal;word-break:break-word;' }, JSON.stringify(err.response.data)),
+                  // h(JsonViewer, {
+                  //   data: JSON.parse(err.response.data)
+                  // }),
+                  customClass: 'cacp-request-message-box',
+                  showConfirmButton: false,
+                }).catch(() => {})
+              }
+            },
+            () => '详情'
+          )
+        ]),
+        duration: 0,
+        showClose: true
+      })
+      return {
+        ...err,
+        data: {
+          code: SystemFailResultCode,
+          data: err.response.data,
+          message: msg
+        }
+      }
+    }
+
+    ElMessage.error({
+      message: err.message || '网络异常,请稍后重试!',
+      duration: 0,
+      showClose: true
+    })
+    return {
+      ...err,
+      data: { code: SystemFailResultCode, data: null, message: err.message }
+    }
+  }
+)
+
+export default Axios

+ 6 - 0
government-demo-web/src/utils/nanoid.ts

@@ -0,0 +1,6 @@
+import { customAlphabet } from 'nanoid'
+
+const alphabet = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz')
+export function nanoid() {
+  return alphabet()
+}

+ 114 - 0
government-demo-web/src/utils/request.ts

@@ -0,0 +1,114 @@
+// 引用axios
+import axios from 'axios'
+import { h } from 'vue'
+import { ElLink, ElMessage, ElMessageBox } from 'element-plus'
+import { SuccessResultCode, SystemFailResultCode, WarningResultCode } from '@cacp/ui'
+import config from '@/config'
+import { noAuth } from './authhelper'
+
+const JWT_KEY = 'x-customs-jwt'
+
+const Axios = axios.create({
+  // API 请求的默认前缀
+  baseURL: `${config.SERVICE_API}`,
+  timeout: config.SERVICE_TIMEOUT, // 请求超时时间
+  withCredentials: true, // 自动携带cookie
+  headers: {
+    'Content-Type': 'application/json;charset=UTF-8',
+    'x-auth-mode': config.AUTH_MODE ?? 'JWT'
+  }
+})
+
+// 请求拦截
+Axios.interceptors.request.use(
+  (req) => {
+    // 头部如需带上token,在此处配置
+    if (!config.AUTH_MODE || config.AUTH_MODE === 'JWT') {
+      req.headers.set(JWT_KEY, sessionStorage.getItem(JWT_KEY))
+    }
+    return req
+  },
+  (err) => {
+    ElMessage.error({ message: err.message, duration: 0, showClose: true })
+    return Promise.reject(err)
+  }
+)
+
+// 响应拦截
+Axios.interceptors.response.use(
+  (res) => {
+    if (res.headers[JWT_KEY]) {
+      if (res.headers[JWT_KEY] === 'CLEAN') {
+        sessionStorage.removeItem(JWT_KEY)
+      } else {
+        sessionStorage.setItem(JWT_KEY, res.headers[JWT_KEY])
+      }
+    }
+    const contentType = res.headers['content-type']
+    if (typeof contentType === 'string' && contentType.startsWith('application/json')) {
+      // 只处理后端返回Resut<T>JSON对象时的错误提示,其他格式原样返回(例如:mime是未知文件流下载或已知文件等类型 )
+      if (res.data && (res.data.code != SuccessResultCode || res.data.code != WarningResultCode) && res.data.message) {
+        ElMessage.error({
+          message: res.data.message,
+          duration: 0,
+          showClose: true
+        })
+      }
+    }
+    return res
+  },
+  (err) => {
+    if (err.response && err.response.status === 401) {
+      const { code, additionalMessage } = err.response.data
+      // console.log(code, additionalMessage, err)
+      noAuth(code, additionalMessage)
+      return
+    }
+
+    if (err.response && err.response.data) {
+      const msg = err.response.data.message ?? '网络异常'
+      ElMessage.error({
+        message: h('div', [
+          msg,
+          h(
+            ElLink,
+            {
+              type: 'danger',
+              style: 'margin-left:12px',
+              onClick: () => {
+                ElMessageBox({
+                  title: msg,
+                  message: () =>
+                    h('div', { style: 'white-space:normal;word-break:break-word;' }, JSON.stringify(err.response.data)),
+                  customClass: 'cacp-request-message-box',
+                  showConfirmButton: false
+                }).catch(() => {})
+              }
+            },
+            () => '详情'
+          )
+        ]),
+        duration: 0,
+        showClose: true
+      })
+      return {
+        data: {
+          code: SystemFailResultCode,
+          data: err.response.data,
+          message: msg
+        }
+      }
+    }
+
+    ElMessage.error({
+      message: err.message || '网络异常,请稍后重试!',
+      duration: 0,
+      showClose: true
+    })
+    return {
+      data: { code: SystemFailResultCode, data: null, message: err.message }
+    }
+  }
+)
+
+export default Axios

+ 25 - 0
government-demo-web/src/views/ErrorView.vue

@@ -0,0 +1,25 @@
+<template>
+  <el-empty :image="image" :image-size="226" description="抱歉,页面无法访问..."> </el-empty>
+</template>
+
+<script setup lang="ts">
+import { useRoute } from 'vue-router'
+import image from '@/assets/images/404.png'
+
+// 获取路由地址根据不同地址处理展示
+const route = useRoute()
+console.log('route---', route.query.data)
+</script>
+
+<style scoped>
+.el-empty {
+  margin: 25vh auto 0;
+  --el-text-color-secondary: #000;
+  --el-font-size-base: 18px;
+  height: 260px;
+}
+.el-empty :deep(p) {
+  font-weight: bold;
+  margin-top: 16px;
+}
+</style>

+ 12 - 0
government-demo-web/src/views/HomeView.vue

@@ -0,0 +1,12 @@
+<template>
+  <div class="cacp-pd-20">
+    <pre>
+    执行流程:
+    1、打开mock-user页面,按照mock-user提示切换用户,注意查看当前用户信息是否正确
+    2、新开gov-list页面显示所有表单,点击新增,然后编辑表单后流转
+    3、在mock-user页面模拟下一用户,新开user-task-list页面显示所有工作任务,点击操作
+    4、一直重复第三步骤    
+    </pre>
+  </div>
+</template>
+<script setup lang="ts"></script>

+ 33 - 0
government-demo-web/src/views/ResultView.vue

@@ -0,0 +1,33 @@
+<template>
+  <el-result class="mainPadding" :icon="query.type as IconType" :title="query.title as string">
+    <template #sub-title>
+      <p>{{ query.content }}</p>
+    </template>
+    <template #extra>
+      <el-button v-if="query.mode === 'back'" type="primary" @click="onBack">返回</el-button>
+      <el-button v-else type="primary" @click="onClose">关闭</el-button>
+    </template>
+  </el-result>
+</template>
+
+<script setup lang="ts">
+import { toRefs } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import type { ResultProps } from 'element-plus'
+
+type IconType = ResultProps['icon']
+
+const route = useRoute()
+const router = useRouter()
+const { query } = toRefs(route)
+
+function onBack() {
+  router.replace({
+    path: query.value.backUrl as string
+  })
+}
+
+function onClose() {
+  window.close()
+}
+</script>

+ 517 - 0
government-demo-web/src/views/designer/ProcessDesignContainer.vue

@@ -0,0 +1,517 @@
+<template>
+  <div class="designer">
+    <div class="designer-aside">
+      <el-space direction="vertical">
+        <span>节点类型</span>
+        <el-button type="success" @click="onAddApprove">
+          <template #icon>
+            <el-icon><Avatar /></el-icon>
+          </template>
+          <span>审批</span>
+        </el-button>
+        <el-button type="success" @click="onAddBranch">
+          <template #icon>
+            <el-icon><Share /></el-icon>
+          </template>
+          <span>分支</span>
+        </el-button>
+        <el-button type="success" @click="onAddVirtual">
+          <template #icon>
+            <el-icon><Finished /></el-icon>
+          </template>
+          <span>虚拟</span>
+        </el-button>
+        <el-button type="success" @click="onAddCopy">
+          <template #icon>
+            <el-icon><CopyDocument /></el-icon>
+          </template>
+          <span>抄送</span>
+        </el-button>
+      </el-space>
+    </div>
+
+    <div class="designer-main">
+      <div class="designer-main-toolbar">
+        <el-button type="primary" @click="onZoomIn">
+          <template #icon>
+            <el-icon><ZoomIn /></el-icon>
+          </template>
+        </el-button>
+        <el-button type="primary" @click="onZoomOut">
+          <template #icon>
+            <el-icon><ZoomOut /></el-icon>
+          </template>
+        </el-button>
+        <el-button type="primary" @click="onCenter">
+          <template #icon>
+            <el-icon><FullScreen /></el-icon>
+          </template>
+        </el-button>
+        <el-button type="primary" @click="onZoomFit">
+          <template #icon>
+            <el-icon><Rank /></el-icon>
+          </template>
+        </el-button>
+        <el-button type="primary" @click="onView">
+          <template #icon>
+            <el-icon><View /></el-icon>
+          </template>
+        </el-button>
+        <el-button type="primary" @click="onSave">
+          <template #icon>
+            <el-icon><Download /></el-icon>
+          </template>
+        </el-button>
+      </div>
+      <div class="designer-main-canvas" ref="canvasRef">
+      </div>
+    </div>
+
+    <div class="designer-property">
+      <design-property :graph="graphInstance" :mode="state.mode" :current="state.current" @on-save-process="onSaveProcess" @on-save-node="onSaveNode" @on-save-flow="onSaveFlow" />
+    </div>
+
+    <design-json :visible="state.visible" :mode="state.mode" :json="state.json" @on-save="onSaveJson" @on-close="onCloseJson"></design-json>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { nextTick, onMounted, reactive, ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import { useRoute } from 'vue-router'
+import { Graph, Node, Edge, Cell, Model } from '@antv/x6'
+import { cloneDeep } from 'lodash-es'
+
+import { type OperMode, type FlowDescriptor, type NodeDescriptor, type ProcessDescriptor, SuccessResultCode, useLoading } from '@cacp/ui'
+
+import * as apis from '@/apis/workflow/definition'
+import { useDesignerStore } from '@/stores'
+import { initProcessDescriptor } from '@/types/process'
+import * as utils from './utils'
+import DesignProperty from './ProcessDesignProperty.vue'
+import DesignJson from './ProcessDesignJson.vue'
+
+const $route = useRoute()
+const graphInstance = ref<Graph>()
+const canvasRef = ref<HTMLDivElement>()
+
+const { loading, setLoading } = useLoading()
+const designerStore = useDesignerStore()
+
+const state = reactive<{
+  visible: boolean,
+  mode: OperMode,
+  descriptor: ProcessDescriptor,
+  json: string
+  current: {
+    type: 'PROCESS' | 'NODE' | 'FLOW',
+    data: ProcessDescriptor | NodeDescriptor | FlowDescriptor
+  }
+}>({
+  visible: false,
+  mode: 'show',
+  descriptor: initProcessDescriptor,
+  json: '',
+  current: {
+    type: 'PROCESS',
+    data: initProcessDescriptor
+  }
+})
+
+onMounted(async () => {
+  setLoading(true)
+
+  await designerStore.init()
+  await initProcessData()
+  initRenderGraph()
+
+  setLoading(false)
+})
+
+async function initProcessData() {
+  const processCode: string = $route.query.processCode as string
+  const mode = $route.query.mode as string
+
+  if (mode === 'create') {
+    state.mode = mode
+    state.descriptor = cloneDeep(initProcessDescriptor)
+  } else if (mode === 'oper') {
+    const res = await apis.getDefinition(processCode, 0)
+    if (res.code === SuccessResultCode && res.data) {
+      state.descriptor = res.data!
+    }
+    state.mode = 'oper'
+  } else {
+    const res = await apis.getDefinition(processCode, 0)
+    if (res.code === SuccessResultCode && res.data) {
+      state.descriptor = res.data!
+    }
+    state.mode = 'show'
+  }
+
+  state.current.data = state.descriptor
+  state.current.type = 'PROCESS'
+}
+
+function initRenderGraph() {
+  graphInstance.value = utils.initGraph(canvasRef.value!, (graph) => {
+    graph.on("selection:changed", handleSelectionChange)
+      .on("node:mouseenter", handleMouseEnter)
+      .on("node:mouseleave", handleMouseLeave)
+      .on("edge:connected", handleConnected)
+      .on("node:removed", handleDelete)
+      .on("edge:removed", handleDelete)
+
+    const json = toGraphData(state.descriptor)
+    graph.fromJSON(json)
+  })
+}
+
+function toGraphData(descriptor: ProcessDescriptor) : Model.FromJSONData {
+  const data: Model.FromJSONData = {
+    nodes: [],
+    edges: []
+  }
+
+  for (const item of descriptor.nodes) {
+    const node: Node.Metadata = {
+      id: item.code,
+      shape: item.type.toUpperCase(),
+      position: { x: (item.attrs?.x ?? 50) as number, y: (item.attrs?.y ?? 5) as number },
+      label: item.name,
+      data: {...item}
+    }
+
+    data.nodes.push(node)
+  }
+
+  for (const item of descriptor.flows) {
+    const edge: Edge.Metadata = {
+      id: item.code,
+      shape: 'FLOW',
+      source: item.source,
+      target: item.target,
+      label: (item.name ?? '') + (item.type === 'CONDITION' ? '<条件>': ''),
+      data: {...item}
+    }
+
+    data.edges.push(edge)
+  }
+
+  return data
+}
+function toDescriptor(data: Model.ToJSONData): ProcessDescriptor {
+  const cells = data.cells
+
+  const nodes = cells.filter(c => c.shape !== 'FLOW')
+    .map(c => {
+      const node: NodeDescriptor = cloneDeep(c.data)
+      node.attrs = Object.assign({}, node.attrs, {
+        x: c.position.x, y: c.position.y
+      })
+      return node
+    })
+  const flows = cells.filter(c => c.shape === 'FLOW')
+    .map(c => {
+      const flow: FlowDescriptor = cloneDeep(c.data)
+      Object.assign(flow, {
+        source: c.source.cell,
+        target: c.target.cell
+      })
+      return flow
+    })
+
+  for (const node of nodes) {
+    if (node.backCode && !nodes.some(n => n.code === node.backCode)) {
+      node.backCode = ''
+    }
+  }
+
+  return Object.assign({}, state.descriptor, { nodes: nodes, flows: flows })
+}
+
+function onAddApprove() {
+  if (!graphInstance.value) {
+    return
+  }
+
+  const id = `n_${utils.nanoId()}`
+  const nodeData: NodeDescriptor = {
+    type: 'APPROVE',
+    code: id,
+    name: '审批节点',
+    backCode: '',
+    seceneCode: '',
+    routeOptional: false,
+    completeStrategy: {
+      strategy: 'ONE'
+    },
+    expireDays: 0,
+    assignment: {
+      type: 'NONE'
+    },
+    keepTrace: false,
+    listeners: [],
+    options: {},
+    attrs: { x: 300, y: 30 }
+  }
+
+  graphInstance.value.addNode({
+    id: id,
+    data: nodeData,
+    shape: nodeData.type.toUpperCase(),
+    position: { x: (nodeData.attrs?.x ?? 50) as number, y: (nodeData.attrs?.y ?? 5) as number },
+    label: nodeData.name
+  })
+}
+
+function onAddBranch() {
+  if (!graphInstance.value) {
+    return
+  }
+
+  const id = `n_${utils.nanoId()}`
+  const nodeData: NodeDescriptor = {
+    type: 'BRANCH',
+    code: id,
+    name: '分支节点',
+    branchCode: '',
+    completeStrategy: {
+      strategy: 'BRANCH'
+    },
+    assignment: {
+      type: 'BRANCH_DEPT',
+      branchDepts: ''
+    },
+    listeners: [],
+    options: {},
+    attrs: { x: 300, y: 30 }
+  }
+
+  graphInstance.value.addNode({
+    id: id,
+    data: nodeData,
+    shape: nodeData.type.toUpperCase(),
+    position: { x: (nodeData.attrs?.x ?? 50) as number, y: (nodeData.attrs?.y ?? 5) as number },
+    label: nodeData.name
+  })
+}
+
+function onAddVirtual() {
+  if (!graphInstance.value) {
+    return
+  }
+
+  const id = `n_${utils.nanoId()}`
+  const nodeData: NodeDescriptor = {
+    type: 'VIRTUAL',
+    code: id,
+    name: '虚拟节点',
+    returnable: false,
+    listeners: [],
+    options: {},
+    attrs: { x: 300, y: 30 }
+  }
+
+  graphInstance.value.addNode({
+    id: id,
+    data: nodeData,
+    shape: nodeData.type.toUpperCase(),
+    position: { x: (nodeData.attrs?.x ?? 50) as number, y: (nodeData.attrs?.y ?? 5) as number },
+    label: nodeData.name
+  })
+}
+
+function onAddCopy() {
+  if (!graphInstance.value) {
+    return
+  }
+
+  const id = `n_${utils.nanoId()}`
+  const nodeData: NodeDescriptor = {
+    type: 'COPY',
+    code: id,
+    name: '抄送节点',
+    routeOptional: false,
+    assignment: {
+      type: 'NONE'
+    },
+    listeners: [],
+    options: {},
+    attrs: { x: 300, y: 30 }
+  }
+
+  graphInstance.value.addNode({
+    id: id,
+    data: nodeData,
+    shape: nodeData.type.toUpperCase(),
+    position: { x: (nodeData.attrs?.x ?? 50) as number, y: (nodeData.attrs?.y ?? 5) as number },
+    label: nodeData.name
+  })
+}
+
+function onZoomIn() {
+  graphInstance.value?.zoom(0.2)
+}
+function onZoomOut() {
+  graphInstance.value?.zoom(-0.2)
+}
+function onZoomFit() {
+  graphInstance.value?.zoomToFit()
+}
+function onCenter() {
+  graphInstance.value?.centerContent()
+}
+function onView() {
+  state.visible = true
+
+  debugger
+  const data = graphInstance.value!.toJSON()
+  const descriptor = toDescriptor(data)
+  state.json = JSON.stringify(descriptor, null, '  ')
+}
+function onSaveJson(json: string) {
+  state.visible = false
+
+  const descriptor: ProcessDescriptor = JSON.parse(json)
+  const data = toGraphData(descriptor)
+  graphInstance.value!.fromJSON(data)
+}
+function onCloseJson() {
+  state.visible = false
+}
+async function onSave() {
+  const data = graphInstance.value!.toJSON()
+  state.descriptor = toDescriptor(data)
+
+  if (state.mode === 'create' || state.mode === 'oper') {
+    const res = await apis.saveDefinition(state.descriptor)
+    if (res.code === SuccessResultCode && res.data > 0) {
+      ElMessage.success('保存成功')
+    }
+  }
+}
+
+
+async function handleSelectionChange(args: { selected: Cell[] }) {
+  const selected: Cell[] = args.selected
+
+  if (!selected || selected.length === 0) {
+    state.current.type = 'PROCESS'
+    state.current.data = state.descriptor
+  } else {
+    state.current.type = 'PROCESS'
+    state.current.data = state.descriptor
+
+    await nextTick()
+
+    const item = selected[0]
+    if (item.shape === 'FLOW') {
+      state.current.type = 'FLOW'
+      state.current.data = item.data as FlowDescriptor
+    } else {
+      state.current.type = 'NODE'
+      state.current.data = item.data as NodeDescriptor
+    }
+  }
+}
+function handleMouseEnter(args: { node: Node }) {
+  const node: Node = args.node
+  for (const port of node.getPorts()) {
+    node.setPortProp(port.id!, 'attrs/circle/style/visibility', 'visible')
+  }
+}
+function handleMouseLeave(args: { node: Node }) {
+  const node: Node = args.node
+  for (const port of node.getPorts()) {
+    node.setPortProp(port.id!, 'attrs/circle/style/visibility', 'hidden')
+  }
+}
+function handleConnected(args: { edge: Edge, currentCell?: Cell | null }) {
+  const edge: Edge = args.edge
+  const source = edge.getSourceCell()
+  const target = args.currentCell
+
+  Object.assign(edge.data, {
+    source: source!.data.code,
+    target: target!.data.code
+  })
+}
+function handleDelete() {
+  state.current.type = 'PROCESS'
+  state.current.data = state.descriptor
+}
+
+function onSaveProcess(info: ProcessDescriptor) {
+  state.descriptor = Object.assign({}, info, { nodes: state.descriptor.nodes, flows: state.descriptor.nodes })
+}
+function onSaveNode(info: NodeDescriptor) {
+  const item = graphInstance.value!.getCellById(info.code) as Node
+  if (!item) {
+    return
+  }
+
+  item.setData(info, {
+    deep: false,
+    silent: true
+  })
+  item.setAttrs({
+    text: { text: info.name }
+  })
+}
+function onSaveFlow(info: FlowDescriptor) {
+  const item = graphInstance.value!.getCellById(info.code) as Edge
+  if (!item) {
+    return
+  }
+
+  item.setData(info, {
+    deep: false,
+    silent: true
+  })
+  item.setLabels({
+    attrs: {
+      label: {
+        fill: utils.colors.flowColor,
+        fontSize: 12
+      },
+      text: {
+        text: (info.name ?? '') + (info.type === 'CONDITION' ? '<条件>' : '')
+      }
+    }
+  })
+}
+</script>
+<style lang="less" scoped>
+  .designer {
+    margin: 5px;
+    display: flex;
+    width: 100%;
+    overflow: hidden;
+    
+    &-aside {
+      padding: 0 5px;
+    }
+
+    &-main {
+      padding: 0 5px;
+      overflow: hidden;
+      flex: 1;
+
+      &-canvas {
+        height: 100%;
+      }
+    }
+
+    &-property {
+      padding: 0 5px;
+      width: 400px;
+      height: 100%;
+      overflow: auto;
+      scrollbar-width: none;
+      &::-webkit-scrollbar {
+        display: none;
+      }
+    }
+  }
+</style>

+ 76 - 0
government-demo-web/src/views/designer/ProcessDesignFlowProperty.vue

@@ -0,0 +1,76 @@
+<template>
+  <el-form v-if="form" :model="form" ref="formRef" label-width="80px" :rules="formRules">
+    <el-form-item label="连线代码">
+      <el-input v-model="form.code" readonly />
+    </el-form-item>
+    <el-form-item label="连线名称" prop="name">
+      <el-input v-model="form.name" :readonly="props.mode === 'show'" />
+    </el-form-item>
+    <el-form-item label="连线类型" prop="type">
+      <el-select v-model="form.type" :disabled="props.mode === 'show'">
+        <el-option label="普通" value="NORMAL" />
+        <el-option label="条件" value="CONDITION" />
+      </el-select>
+    </el-form-item>
+    <el-form-item label="条件脚本" v-if="form.type === 'CONDITION'" prop="condition">
+      <el-input type="textarea" :rows="3" v-model="form.condition" :readonly="props.mode === 'show'" />
+      <div>变量类似{#var}写法,结果为boolean类型,例如判断天数=10的写法是{#days} == 10,人名是张三的写法是'{#personName}' == '张三'</div>
+    </el-form-item>
+    <el-form-item>
+      <el-button type="warning" @click="onSave">
+        <template #icon>
+          <el-icon><Download /></el-icon>
+        </template>
+        保存
+      </el-button>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { ref, watchEffect } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { cloneDeep } from 'lodash-es'
+
+import type { OperMode, FlowDescriptor } from '@cacp/ui'
+
+const props = defineProps<{
+  mode: OperMode
+  info?: FlowDescriptor
+}>()
+const emits = defineEmits<{
+  (e: 'on-save', flow: FlowDescriptor): void
+}>()
+
+const formRules: FormRules = {
+  type: [
+    { required: true, message: '类型必填' }
+  ],
+  condition: [
+    {
+      validator: (rule, value, callback) => {
+        if (form.value.type === 'CONDITION' && !value) {
+          return callback('条件脚本必填')
+        }
+
+        return callback()
+      }
+    }
+  ]
+}
+
+const formRef = ref<FormInstance>()
+const form = ref<FlowDescriptor>()
+
+watchEffect(() => {
+  form.value = cloneDeep(props.info)
+})
+
+function onSave() {
+  formRef.value!.validate((valid) => {
+    if (valid) {
+      emits('on-save', form.value)
+    }
+  })
+}
+</script>

+ 43 - 0
government-demo-web/src/views/designer/ProcessDesignJson.vue

@@ -0,0 +1,43 @@
+<template>
+  <el-dialog
+    :model-value="visible"
+    title="JSON详情"
+    width="600px"
+    :append-to-body="true"
+    :destroy-on-close="true"
+    @open="onOpen"
+    @close="onClose"
+  >
+    <el-input type="textarea" :rows="20" v-model="val" :readonly="mode === 'show'" />
+    <template #footer>
+      <el-button type="primary" @click="onSave" v-if="mode !== 'show'">确定</el-button>
+      <el-button @click="onClose">取消</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+
+const props = defineProps<{
+  visible: boolean
+  mode: string
+  json: string
+}>()
+const emits = defineEmits<{
+  (e: 'on-close'): void
+  (e: 'on-save', val: string): void
+}>()
+
+const val = ref(props.json)
+
+function onOpen() {
+  val.value = props.json
+}
+function onSave() {
+  emits('on-save', val.value)
+}
+function onClose() {
+  emits('on-close')
+}
+</script>

+ 259 - 0
government-demo-web/src/views/designer/ProcessDesignNodeProperty.vue

@@ -0,0 +1,259 @@
+<template>
+  <el-form v-if="form" :model="form" ref="formRef" label-width="80px" :rules="formRules">
+    <el-form-item label="节点代码">
+      <el-input v-model="form.code" readonly />
+    </el-form-item>
+    <el-form-item label="节点名称" prop="name">
+      <el-input v-model="form.name" :readonly="props.mode === 'show'" />
+    </el-form-item>
+    <el-form-item label="节点类型" prop="type">
+      <el-select v-model="form.type" :disabled="props.mode === 'show'">
+        <el-option v-for="item in designerStore.nodeTypeList" :key="item.code" :label="item.name" :value="item.code" />
+      </el-select>
+    </el-form-item>
+
+    <el-form-item label="场景代码" v-if="form.type === 'START' || form.type === 'APPROVE'">
+      <el-input v-model="form.seceneCode" :readonly="props.mode === 'show'" />
+    </el-form-item>
+    <el-form-item label="过期天数" v-if="form.type === 'START' || form.type === 'APPROVE'">
+      <el-input v-model.number="form.expireDays" :readonly="props.mode === 'show'" />
+    </el-form-item>
+    <el-form-item label="选择下步" v-if="form.type === 'START' || form.type === 'APPROVE' || form.type === 'COPY'">
+      <el-switch v-model="form.routeOptional" :disabled="props.mode === 'show'" />
+    </el-form-item>
+    <el-form-item label="分支流程" v-if="form.type === 'BRANCH'" prop="branchCode">
+      <el-input v-model="form.branchCode" :readonly="props.mode === 'show'" />
+    </el-form-item>
+    <el-form-item label="退回节点" v-if="form.type === 'APPROVE'" prop="backCode">
+      <el-input v-model="form.backCode" :readonly="props.mode === 'show'" />
+    </el-form-item>
+    <el-form-item label="保存待办" v-if="form.type === 'APPROVE'">
+      <el-switch v-model="form.keepTrace" :disabled="props.mode === 'show'" />
+    </el-form-item>
+    <el-form-item label="是否返回" v-if="form.type === 'VIRTUAL'">
+      <el-switch v-model="form.returnable" :disabled="props.mode === 'show'" />
+    </el-form-item>
+
+    <el-form-item label="节点描述">
+      <el-input type="textarea" :rows="3" v-model="form.description" :readonly="props.mode === 'show'" />
+    </el-form-item>
+
+    <template v-if="form.type === 'APPROVE' || form.type === 'BRANCH'">
+      <el-form-item label="结束策略">
+        <el-select v-model="form.completeStrategy.strategy" :disabled="props.mode === 'show'">
+          <el-option v-for="item in designerStore.strategyList" :key="item.code" :value="item.code"
+            :label="item.name" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="策略计数"
+        v-if="form.completeStrategy.strategy === 'COUNT' || form.completeStrategy.strategy === 'PERCENT'">
+        <el-input v-model="form.completeStrategy.value" :readonly="props.mode === 'show'" />
+      </el-form-item>
+    </template>
+
+    <template v-if="form.type === 'START' || form.type === 'APPROVE' || form.type === 'BRANCH' || form.type === 'COPY'">
+      <el-form-item label="接收类型">
+        <el-select v-model="form.assignment.type" :disabled="props.mode === 'show'">
+          <el-option v-for="item in designerStore.assignmentTypeList" :key="item.code" :value="item.code"
+            :label="item.name" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="分支部门" v-if="form.assignment.type === 'BRANCH_DEPT'">
+        <el-input v-model="form.assignment.branchDepts" :readonly="props.mode === 'show'" />
+      </el-form-item>
+      <el-form-item label="部门路径" v-if="form.assignment.type === 'DEPT' || form.assignment?.type === 'ROLE_DEPT'">
+        <el-input v-model="form.assignment.deptPaths" :readonly="props.mode === 'show'" />
+      </el-form-item>
+      <el-form-item label="扩展类名" v-if="form.assignment.type === 'EXTEND'">
+        <el-input v-model="form.assignment.className" :readonly="props.mode === 'show'" />
+      </el-form-item>
+      <el-form-item label="扩展参数" v-if="form.assignment.type === 'EXTEND'">
+        <el-input v-model="form.assignment.branchDepts" :readonly="props.mode === 'show'" />
+      </el-form-item>
+      <el-form-item label="角色代码" v-if="
+        form.assignment.type === 'ROLE' ||
+        form.assignment.type === 'ROLE_DEPT' ||
+        form.assignment.type === 'ROLE_LEVEL'
+      ">
+        <el-input v-model="form.assignment.roleCode" :readonly="props.mode === 'show'" />
+      </el-form-item>
+      <el-form-item label="部门层级" v-if="form.assignment.type === 'ROLE_LEVEL'">
+        <el-input v-model.number="form.assignment.deptLevel" :readonly="props.mode === 'show'" />
+      </el-form-item>
+      <el-form-item label="用户路径" v-if="form.assignment.type === 'USER'">
+        <el-input v-model="form.assignment.userPaths" :readonly="props.mode === 'show'" />
+      </el-form-item>
+    </template>
+    <div class="cacp-pv-sm cacp-ar"><el-button @click="useOptions.create" type="primary">新增</el-button></div>
+    <el-table :data="tableOptions">
+      <edit-table-column property="key" label="键" />
+      <edit-table-column property="value" label="值" />
+      <el-table-column label="操作" width="160" fixed="right">
+        <template #default="{ row, $index }">
+          <el-button link type="danger" @click="useOptions.del($index)">删除</el-button>
+          <template v-if="row.isEdit">
+            <el-button link type="primary" @click="useOptions.save($index)">保存</el-button>
+            <el-button link @click="useOptions.cancel($index)">取消</el-button>
+          </template>
+          <template v-else>
+            <el-button link type="primary" @click="useOptions.edit($index)">编辑</el-button>
+          </template>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="cacp-pv-sm cacp-ar"><el-button @click="useListeners.create" type="primary">新增</el-button></div>
+    <el-table :data="tableListeners">
+      <edit-table-column property="key" label="监听器" />
+      <el-table-column label="操作" width="160" fixed="right">
+        <template #default="{ row, $index }">
+          <el-button link type="danger" @click="useListeners.del($index)">删除</el-button>
+          <template v-if="row.isEdit">
+            <el-button link type="primary" @click="useListeners.save($index)">保存</el-button>
+            <el-button link @click="useListeners.cancel($index)">取消</el-button>
+          </template>
+          <template v-else>
+            <el-button link type="primary" @click="useListeners.edit($index)">编辑</el-button>
+          </template>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- options attrs listeners-->
+    <el-form-item class="cacp-mt-lg">
+      <el-button type="warning" @click="onSave">
+        <template #icon>
+          <el-icon>
+            <Download />
+          </el-icon>
+        </template>
+        保存
+      </el-button>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, watchEffect } from 'vue'
+import { type FormInstance, type FormRules } from 'element-plus'
+import { cloneDeep } from 'lodash-es'
+
+import type { OperMode, NodeDescriptor } from '@cacp/ui'
+
+import EditTableColumn from '@/components/editTable'
+import { useDesignerStore } from '@/stores'
+import type { Graph } from '@antv/x6'
+import { useEditTable } from './useEditTable'
+
+const props = defineProps<{
+  graph?: Graph
+  mode: OperMode
+  info?: NodeDescriptor
+}>()
+const emits = defineEmits<{
+  (e: 'on-save', node: NodeDescriptor): void
+}>()
+
+const designerStore = useDesignerStore()
+
+const formRules: FormRules = {
+  name: [
+    {
+      required: true,
+      message: '名称必填'
+    }
+  ],
+  type: [
+    {
+      required: true,
+      message: '类型必填'
+    }
+  ],
+  backCode: [
+    {
+      validator: (rule, value, callback) => {
+        if (!value || value === '#LAST#') {
+          return callback()
+        }
+        if (!backCodeList.value.some((n) => n.code === value)) {
+          return callback('退回环节无效')
+        }
+
+        return callback()
+      }
+    }
+  ]
+}
+const formRef = ref<FormInstance>()
+const form = ref<NodeDescriptor>()
+
+const tableListeners = ref<Array<{ key: string; isEdit?: boolean }>>([])
+const tableOptions = ref<{
+  key: string
+  value: unknown
+  isEdit?: boolean
+}[]>()
+
+const useOptions = useEditTable(tableOptions)
+const useListeners = useEditTable(tableListeners)
+
+function getOptions() {
+  if (!form.value.options) {
+    return []
+  }
+  return Object.keys(form.value!.options).map((o) => {
+    return {
+      key: o,
+      value: form.value!.options[o]
+    }
+  })
+}
+
+function getListeners() {
+  if (!form.value.listeners) {
+    return []
+  }
+  return form.value!.listeners.map((o) => {
+    return {
+      key: o
+    }
+  })
+}
+
+function writeOptions() {
+  form.value.options = tableOptions.value.reduce(
+    (pre, v) => {
+      pre[v.key] = v.value
+      return pre
+    },
+    {} as Record<string, unknown>
+  )
+}
+
+function writeListeners() {
+  form.value.listeners = tableListeners.value.map((v) => v.key)
+}
+
+const backCodeList = computed(() => {
+  return props.graph
+    .getNodes()
+    .map((n) => n.data as NodeDescriptor)
+    .filter((n) => n.code !== props.info.code && (n.type === 'START' || n.type === 'APPROVE'))
+})
+
+watchEffect(() => {
+  form.value = cloneDeep(props.info)
+  tableOptions.value = getOptions()
+  tableListeners.value = getListeners()
+})
+
+function onSave() {
+  formRef.value!.validate((valid) => {
+    if (valid) {
+      writeOptions()
+      writeListeners()
+      emits('on-save', form.value)
+    }
+  })
+}
+
+
+</script>

+ 72 - 0
government-demo-web/src/views/designer/ProcessDesignProcessProperty.vue

@@ -0,0 +1,72 @@
+<template>
+  <el-form v-if="form" :model="form" ref="formRef" label-width="80px" :rules="formRules">
+    <el-form-item label="流程代码" prop="code">
+      <el-input v-model="form.code" :readonly="props.mode !== 'create'" />
+    </el-form-item>
+    <el-form-item label="流程名称" prop="name">
+      <el-input v-model="form.name" :readonly="props.mode === 'show'" />
+    </el-form-item>
+    <el-form-item label="是否分支">
+      <el-switch v-model="form.branch" :disabled="props.mode === 'show'" />
+    </el-form-item>
+    <el-form-item label="过期天数">
+      <el-input v-model.number="form.expireDays" :readonly="props.mode === 'show'" />
+    </el-form-item>
+    <el-form-item label="流程描述">
+      <el-input type="textarea" :rows="3" v-model="form.description" :readonly="props.mode === 'show'" />
+    </el-form-item>
+    <el-form-item>
+      <el-button type="warning" @click="onSave">
+        <template #icon>
+          <el-icon><Download /></el-icon>
+        </template>
+        保存
+      </el-button>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { ref, watchEffect } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { cloneDeep } from 'lodash-es'
+
+import type { OperMode, ProcessDescriptor } from '@cacp/ui'
+
+const props = defineProps<{
+  mode: OperMode
+  info?: ProcessDescriptor
+}>()
+const emits = defineEmits<{
+  (e: 'on-save', process: ProcessDescriptor): void
+}>()
+
+const formRules: FormRules = {
+  code: [
+    {
+      required: true,
+      message: '编码必填'
+    }
+  ],
+  name: [
+    {
+      required: true,
+      message: '名称必填'
+    }
+  ]
+}
+const formRef = ref<FormInstance>()
+const form = ref<ProcessDescriptor>()
+
+watchEffect(() => {
+  form.value = cloneDeep(props.info)
+})
+
+function onSave() {
+  formRef.value!.validate((valid) => {
+    if (valid) {
+      emits('on-save', form.value)
+    }
+  })
+}
+</script>

+ 73 - 0
government-demo-web/src/views/designer/ProcessDesignProperty.vue

@@ -0,0 +1,73 @@
+<template>
+  <div class="designer-property-header">
+    <el-icon><Setting /></el-icon>
+    <span>{{ title }}</span>
+  </div>
+
+  <process-property v-if="current.type === 'PROCESS'" :mode="mode" :info="state.processDescriptor" @on-save="onSaveProcess"></process-property>
+  <node-property v-if="current.type === 'NODE'" :graph="graph" :mode="mode" :info="state.nodeDescriptor" @on-save="onSaveNode"></node-property>
+  <flow-property v-if="current.type === 'FLOW'" :mode="mode" :info="state.flowDescriptor" @on-save="onSaveFlow"></flow-property>
+</template>
+
+<script setup lang="ts">
+import { computed, reactive, watchEffect } from 'vue'
+import type { Graph } from '@antv/x6'
+import { cloneDeep } from 'lodash-es'
+
+import type { OperMode, ProcessDescriptor, NodeDescriptor, FlowDescriptor } from '@cacp/ui'
+
+import ProcessProperty from './ProcessDesignProcessProperty.vue'
+import NodeProperty from './ProcessDesignNodeProperty.vue'
+import FlowProperty from './ProcessDesignFlowProperty.vue'
+
+const props = defineProps<{
+  graph?: Graph
+  mode: OperMode
+  current: {
+    type: 'PROCESS' | 'NODE' | 'FLOW'
+    data: ProcessDescriptor | NodeDescriptor | FlowDescriptor
+  }
+}>()
+const emits = defineEmits<{
+  (e: 'on-save-process', process: ProcessDescriptor): void
+  (e: 'on-save-node', node: NodeDescriptor): void
+  (e: 'on-save-flow', flow: FlowDescriptor): void
+}>()
+
+const state = reactive<{
+  processDescriptor?: ProcessDescriptor,
+  nodeDescriptor?: NodeDescriptor,
+  flowDescriptor?: FlowDescriptor,
+}>({
+})
+
+watchEffect(() => {
+  if (props.current.type === 'NODE') {
+    state.nodeDescriptor = cloneDeep(props.current.data as NodeDescriptor)
+  } else if (props.current.type === 'FLOW') {
+    state.flowDescriptor = cloneDeep(props.current.data as FlowDescriptor)
+  } else {
+    state.processDescriptor = cloneDeep(props.current.data as ProcessDescriptor)
+  }
+}) 
+
+const title = computed(() => {
+  if (props.current.type === 'NODE') {
+    return '节点属性 - ' + props.current.data.name
+  } else if (props.current.type === 'FLOW') {
+    return '连线属性 - ' + props.current.data.name
+  } else {
+    return '流程属性'
+  }
+})
+
+function onSaveProcess(data: ProcessDescriptor) {
+  emits('on-save-process', data)
+}
+function onSaveNode(data: NodeDescriptor) {
+  emits('on-save-node', data)
+}
+function onSaveFlow(data: FlowDescriptor) {
+  emits('on-save-flow', data)
+}
+</script>

+ 46 - 0
government-demo-web/src/views/designer/useEditTable.ts

@@ -0,0 +1,46 @@
+import { ElMessage } from 'element-plus'
+import type { Ref } from 'vue'
+
+interface EditTableRow {
+  key: string
+  isEdit?: boolean
+}
+
+export function useEditTable<T extends EditTableRow>(arr: Ref<Array<T>>) {
+  function edit(index: number) {
+    arr.value[index].isEdit = true
+  }
+
+  function cancel(index: number) {
+    const row = arr.value[index]
+    if (row.key) {
+      row.isEdit = false
+      return
+    }
+    arr.value.splice(index, 1)
+  }
+
+  function create() {
+    arr.value.unshift({ key: '', isEdit: true } as T)
+  }
+
+  function del(index: number) {
+    arr.value.splice(index, 1)
+  }
+
+  function save(index: number) {
+    const row = arr.value[index]
+    if (!row.key) {
+      ElMessage.error('请输入key再保存')
+      return
+    }
+    row.isEdit = false
+  }
+  return {
+    edit,
+    cancel,
+    del,
+    save,
+    create
+  }
+}

+ 474 - 0
government-demo-web/src/views/designer/utils.ts

@@ -0,0 +1,474 @@
+import { Graph, Shape } from '@antv/x6'
+import { Selection } from '@antv/x6-plugin-selection'
+import { Scroller } from '@antv/x6-plugin-scroller'
+import { Keyboard } from '@antv/x6-plugin-keyboard'
+import type { PortManager } from '@antv/x6/lib/model/port'
+import { customAlphabet } from 'nanoid'
+
+import { Avatar, Share, Finished, CopyDocument } from '@cacp/svg-icons'
+import type { FlowDescriptor, NodeDescriptor, ProcessDescriptor } from '@cacp/ui'
+
+export const colors = {
+  flowColor: '#A2B1C3',
+  nodeColor: '#262626',
+  nodeFill: '#E7F7FE',
+  nodeStroke: '#69C0FF',
+  portStroke: '#5F95FF',
+  portFill: '#FFF'
+}
+const ports: PortManager.Metadata = {
+  groups: {
+    top: {
+      position: 'top',
+      attrs: {
+        circle: {
+          r: 4,
+          magnet: true,
+          stroke: colors.portStroke,
+          strokeWidth: 1,
+          fill: colors.portFill,
+          style: {
+            visiblility: 'hidden'
+          }
+        }
+      }
+    },
+    right: {
+      position: 'right',
+      attrs: {
+        circle: {
+          r: 4,
+          magnet: true,
+          stroke: colors.portStroke,
+          strokeWidth: 1,
+          fill: colors.portFill,
+          style: {
+            visiblility: 'hidden'
+          }
+        }
+      }
+    },
+    bottom: {
+      position: 'bottom',
+      attrs: {
+        circle: {
+          r: 4,
+          magnet: true,
+          stroke: colors.portStroke,
+          strokeWidth: 1,
+          fill: colors.portFill,
+          style: {
+            visiblility: 'hidden'
+          }
+        }
+      }
+    },
+    left: {
+      position: 'left',
+      attrs: {
+        circle: {
+          r: 4,
+          magnet: true,
+          stroke: colors.portStroke,
+          strokeWidth: 1,
+          fill: colors.portFill,
+          style: {
+            visiblility: 'hidden'
+          }
+        }
+      }
+    }
+  },
+  items: [
+    {
+      group: 'top'
+    },
+    {
+      group: 'right'
+    },
+    {
+      group: 'bottom'
+    },
+    {
+      group: 'left'
+    }
+  ]
+}
+
+function registerGraph() {
+  Graph.registerNode(
+    'START',
+    {
+      inherit: 'circle',
+      x: 0,
+      y: 0,
+      width: 60,
+      height: 60,
+      attrs: {
+        body: {
+          strokeWidth: 1,
+          fill: '#FEF7E7',
+          stroke: '#FFC069'
+        },
+        label: {
+          fill: colors.nodeColor,
+          fontSize: 12
+        }
+      },
+      ports: ports
+    },
+    true
+  )
+
+  Graph.registerNode(
+    'END',
+    {
+      inherit: 'circle',
+      x: 0,
+      y: 0,
+      width: 60,
+      height: 60,
+      attrs: {
+        body: {
+          strokeWidth: 1,
+          fill: '#EBF2FF',
+          stroke: '#BFCFEE'
+        },
+        label: {
+          fill: colors.nodeColor,
+          fontSize: 12
+        }
+      },
+      ports: ports
+    },
+    true
+  )
+
+  Graph.registerNode(
+    'APPROVE',
+    {
+      inherit: 'rect',
+      x: 0,
+      y: 0,
+      width: 100,
+      height: 60,
+      markup: [
+        {
+          tagName: 'rect',
+          selector: 'body'
+        },
+        {
+          tagName: 'image',
+          selector: 'img'
+        },
+        {
+          tagName: 'text',
+          selector: 'label'
+        }
+      ],
+      attrs: {
+        body: {
+          rx: 6,
+          ry: 6,
+          strokeWidth: 1,
+          fill: colors.nodeFill,
+          stroke: colors.nodeStroke
+        },
+        img: {
+          x: 6,
+          y: 6,
+          width: 16,
+          height: 16,
+          'xlink:href': Avatar
+        },
+        label: {
+          fill: colors.nodeColor,
+          fontSize: 12
+        }
+      },
+      ports: ports
+    },
+    true
+  )
+
+  Graph.registerNode(
+    'BRANCH',
+    {
+      inherit: 'rect',
+      x: 0,
+      y: 0,
+      width: 100,
+      height: 60,
+      markup: [
+        {
+          tagName: 'rect',
+          selector: 'body'
+        },
+        {
+          tagName: 'image',
+          selector: 'img'
+        },
+        {
+          tagName: 'text',
+          selector: 'label'
+        }
+      ],
+      attrs: {
+        body: {
+          rx: 6,
+          ry: 6,
+          strokeWidth: 1,
+          fill: colors.nodeFill,
+          stroke: colors.nodeStroke
+        },
+        img: {
+          x: 6,
+          y: 6,
+          width: 16,
+          height: 16,
+          'xlink:href': Share
+        },
+        label: {
+          fill: colors.nodeColor,
+          fontSize: 12
+        }
+      },
+      ports: ports
+    },
+    true
+  )
+
+  Graph.registerNode(
+    'VIRTUAL',
+    {
+      inherit: 'rect',
+      x: 0,
+      y: 0,
+      width: 100,
+      height: 60,
+      markup: [
+        {
+          tagName: 'rect',
+          selector: 'body'
+        },
+        {
+          tagName: 'image',
+          selector: 'img'
+        },
+        {
+          tagName: 'text',
+          selector: 'label'
+        }
+      ],
+      attrs: {
+        body: {
+          rx: 6,
+          ry: 6,
+          strokeWidth: 1,
+          fill: colors.nodeFill,
+          stroke: colors.nodeStroke
+        },
+        img: {
+          x: 6,
+          y: 6,
+          width: 16,
+          height: 16,
+          'xlink:href': Finished
+        },
+        label: {
+          fill: colors.nodeColor,
+          fontSize: 12
+        }
+      },
+      ports: ports
+    },
+    true
+  )
+
+  Graph.registerNode(
+    'COPY',
+    {
+      inherit: 'rect',
+      x: 0,
+      y: 0,
+      width: 100,
+      height: 60,
+      markup: [
+        {
+          tagName: 'rect',
+          selector: 'body'
+        },
+        {
+          tagName: 'image',
+          selector: 'img'
+        },
+        {
+          tagName: 'text',
+          selector: 'label'
+        }
+      ],
+      attrs: {
+        body: {
+          rx: 6,
+          ry: 6,
+          strokeWidth: 1,
+          fill: colors.nodeFill,
+          stroke: colors.nodeStroke
+        },
+        img: {
+          x: 6,
+          y: 6,
+          width: 16,
+          height: 16,
+          'xlink:href': CopyDocument
+        },
+        label: {
+          fill: colors.nodeColor,
+          fontSize: 12
+        }
+      },
+      ports: ports
+    },
+    true
+  )
+
+  Graph.registerEdge(
+    'FLOW',
+    {
+      inherit: 'edge',
+      attrs: {
+        line: {
+          stroke: colors.flowColor,
+          strokeWidth: 2
+        }
+      },
+      label: {
+        attrs: {
+          label: {
+            fill: colors.flowColor,
+            fontSize: 12
+          }
+        }
+      },
+      zIndex: 0
+    },
+    true
+  )
+}
+
+export function nanoId() {
+  return customAlphabet('123456789abcdefghjkmnpqrstuvwxyz', 20)()
+}
+
+export function initGraph(container: HTMLDivElement, postGraphInit: (graph: Graph) => void): Graph {
+  const graph: Graph = new Graph({
+    container: container,
+    grid: {
+      size: 10,
+      visible: true
+    },
+    panning: {
+      enabled: true
+    },
+    mousewheel: {
+      enabled: true,
+      zoomAtMousePosition: true,
+      modifiers: 'ctrl',
+      minScale: 0.5,
+      maxScale: 1.2
+    },
+    autoResize: true,
+    highlighting: {
+      magnetAdsorbed: {
+        name: 'stroke',
+        args: {
+          attrs: {
+            fill: '#5F95FF',
+            stroke: '#5F95FF'
+          }
+        }
+      }
+    },
+    connecting: {
+      router: 'orth',
+      snap: {
+        radius: 20
+      },
+      allowBlank: false,
+      allowLoop: false,
+      allowNode: false,
+      allowEdge: false,
+      allowPort: true,
+      allowMulti: false,
+      connector: {
+        name: 'rounded',
+        args: {
+          radius: 8
+        }
+      },
+      anchor: 'center',
+      createEdge() {
+        const id = `f_${nanoId()}`
+        return new Shape.Edge({
+          id: id,
+          shape: 'FLOW',
+          label: '',
+          attrs: {
+            line: {
+              stroke: colors.flowColor,
+              strokeWidth: 2,
+              targetMarker: {
+                name: 'block',
+                width: 12,
+                height: 8
+              }
+            }
+          },
+          data: {
+            type: 'NORMAL',
+            code: id,
+            name: '',
+            source: null,
+            target: null
+          }
+        })
+      }
+    }
+  })
+
+  graph
+    .use(new Selection({ enabled: true, multiple: false }))
+    .use(new Scroller({ pannable: true, enabled: true }))
+    .use(new Keyboard({ enabled: true }))
+
+  graph.bindKey('delete', () => {
+    const cells = graph.getSelectedCells()
+    if (cells) {
+      const cell = cells[0]
+      if (cell.shape === 'START' || cell.shape === 'END') {
+        return
+      }
+      cell.remove()
+    }
+
+    return false
+  })
+
+  registerGraph()
+  postGraphInit(graph)
+  graph.centerContent()
+
+  return graph
+}
+
+export function isProcessDescriptor(data: ProcessDescriptor | NodeDescriptor | FlowDescriptor): data is ProcessDescriptor {
+  return typeof (data as ProcessDescriptor)['branch'] !== undefined
+}
+
+export function isNodeDescriptor(data: ProcessDescriptor | NodeDescriptor | FlowDescriptor): data is NodeDescriptor {
+  return typeof (data as NodeDescriptor)['attrs'] !== undefined
+}
+
+export function isFlowDescriptor(data: ProcessDescriptor | NodeDescriptor | FlowDescriptor): data is FlowDescriptor {
+  return typeof (data as FlowDescriptor)['source'] !== undefined
+}

+ 122 - 0
government-demo-web/src/views/government/GovernmentDetailOper.vue

@@ -0,0 +1,122 @@
+<template>
+  <h3 class="mainTitle">政务表单</h3>
+  <div class="mainPadding">
+    <h3>基本信息</h3>
+    <el-form v-if="state.form" :model="state.form" ref="formRef" label-width="100px">
+      <el-form-item label="标题">
+        <el-input v-model="state.form.formTitle" :readonly="state.editable && state.mode !== 'create'"></el-input>
+      </el-form-item>
+      <el-form-item label="内容">
+        <el-input v-model="state.form.formContent" :readonly="state.editable && state.mode === 'show'"></el-input>
+      </el-form-item>
+      <el-form-item label="拟稿人">
+        <el-input v-model="state.form.issueUserName" readonly></el-input>
+      </el-form-item>
+      <el-form-item label="拟稿时间">
+        <el-input v-model="state.form.createTime" readonly></el-input>
+      </el-form-item>
+      <el-form-item label="状态">
+        <el-input v-model="state.form.formStatus" readonly></el-input>
+      </el-form-item>
+    </el-form>
+    <process-operation
+      :context-path="contextPath"
+      :mode="state.mode"
+      :task-id="state.taskId"
+      :form-id="state.formId"
+      :url-format="urlFormat"
+      :process-code="processCode"
+      :range="range"
+      :user="state.user"
+      :form="state.form"
+      :variables="state.variables"
+      :next-branch-list="state.nextBranchList"
+      :form-ref="formRef"
+      @on-load="onLoad"
+      @on-command="onCommand"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onBeforeMount } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import type { FormInstance } from 'element-plus'
+
+import { type FrameUserInfo, type OperationType, type OperMode, type RuntimeOgu, getOperationType } from '@cacp/ui'
+
+import { useCoreStore } from '@/stores/core'
+import { type GovernmentInfo, contextPath, processCode, urlFormat, range } from '@/types/government'
+import ProcessOperation from '@/components/workflow/ProcessOperation.vue'
+
+const route = useRoute()
+const router = useRouter()
+const coreStore = useCoreStore()
+
+const formRef = ref<FormInstance>()
+
+const state = reactive<{
+  mode: OperMode
+  editable: boolean
+  taskId?: string
+  formId?: string
+  form?: GovernmentInfo
+  user: FrameUserInfo
+  nextBranchList: Array<RuntimeOgu>
+  variables?: Record<string, unknown>
+}>({
+  mode: 'show',
+  editable: false,
+  user: coreStore.currentUser!,
+  nextBranchList: []
+})
+
+onBeforeMount(async () => {
+  state.mode = (route.query.mode as OperMode) ?? 'show'
+
+  if (state.mode !== 'create') {
+    state.taskId = route.query.taskId as string
+    state.formId = route.query.formId as string
+  }
+})
+
+function onLoad(form: GovernmentInfo, processCode: string, activityCode: string, editable: boolean) {
+  state.form = form
+  state.editable = editable
+
+  if (processCode === 'DemoParentProcess' && (activityCode === 'DISTRIBUTE' || activityCode === 'LINK')) {
+    state.nextBranchList = [
+      {
+        oguId: '',
+        oguName: '监管处',
+        oguType: 'ORGANIZATION',
+        parentId: '',
+        fullPathName: '中国海关\\青岛海关\\监管处',
+        copyFlag: false,
+        deptType: 'HUIQIAN',
+        delegateFlag: false
+      },
+      {
+        oguId: '',
+        oguName: '通关管理处',
+        oguType: 'ORGANIZATION',
+        parentId: '',
+        fullPathName: '中国海关\\青岛海关\\通关管理处',
+        copyFlag: false,
+        deptType: 'HUIQIAN',
+        delegateFlag: false
+      }
+    ]
+  }
+}
+
+function onCommand(flag: OperationType) {
+  router.push({
+    path: '/user-task-list',
+    query: {
+      title: getOperationType(flag),
+      content: '请回到待办事项页面'
+    }
+  })
+}
+</script>

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.