wangqian 1 місяць тому
батько
коміт
24130b90be
100 змінених файлів з 4921 додано та 0 видалено
  1. 17 0
      basic-demo-web/.eslintrc.js
  2. 78 0
      basic-demo-web/.gitignore
  3. 1 0
      basic-demo-web/.npmrc
  4. 8 0
      basic-demo-web/.prettierrc.json
  5. 8 0
      basic-demo-web/.vscode/extensions.json
  6. 20 0
      basic-demo-web/Dockerfile
  7. 12 0
      basic-demo-web/Dockerfile-arm
  8. 123 0
      basic-demo-web/README.md
  9. BIN
      basic-demo-web/cacp/icon/svg-icons-0.1.2.tgz
  10. BIN
      basic-demo-web/cacp/ui/ui-0.3.17.tgz
  11. 1 0
      basic-demo-web/env.d.ts
  12. 19 0
      basic-demo-web/eslint.config.js
  13. 14 0
      basic-demo-web/index.html
  14. 60 0
      basic-demo-web/mock/areacode.ts
  15. 0 0
      basic-demo-web/mock/city.json
  16. 64 0
      basic-demo-web/mock/editTable.ts
  17. 32 0
      basic-demo-web/mock/eight.ts
  18. 75 0
      basic-demo-web/mock/eleven.ts
  19. 95 0
      basic-demo-web/mock/four.ts
  20. 0 0
      basic-demo-web/mock/index.ts
  21. 113 0
      basic-demo-web/mock/nine.ts
  22. 53 0
      basic-demo-web/mock/one.ts
  23. 101 0
      basic-demo-web/mock/seven.ts
  24. 35 0
      basic-demo-web/mock/six.ts
  25. 95 0
      basic-demo-web/mock/ten.ts
  26. 121 0
      basic-demo-web/mock/three.ts
  27. 169 0
      basic-demo-web/mock/utils.ts
  28. 73 0
      basic-demo-web/nginx.conf
  29. 1 0
      basic-demo-web/node_version
  30. 73 0
      basic-demo-web/package.json
  31. 11 0
      basic-demo-web/public/config.js
  32. BIN
      basic-demo-web/public/favicon.ico
  33. 224 0
      basic-demo-web/src/App.vue
  34. 57 0
      basic-demo-web/src/apis/accessory.ts
  35. 19 0
      basic-demo-web/src/apis/audit.ts
  36. 61 0
      basic-demo-web/src/apis/auth.ts
  37. 10 0
      basic-demo-web/src/apis/authority.ts
  38. 19 0
      basic-demo-web/src/apis/counter.ts
  39. 0 0
      basic-demo-web/src/apis/flow.ts
  40. 71 0
      basic-demo-web/src/apis/form.ts
  41. 10 0
      basic-demo-web/src/apis/frame.ts
  42. 85 0
      basic-demo-web/src/apis/heai.ts
  43. 21 0
      basic-demo-web/src/apis/holiday.ts
  44. 163 0
      basic-demo-web/src/apis/mock.ts
  45. 61 0
      basic-demo-web/src/apis/order.ts
  46. 40 0
      basic-demo-web/src/assets/base.less
  47. BIN
      basic-demo-web/src/assets/images/404.png
  48. BIN
      basic-demo-web/src/assets/images/left-menu-bg.png
  49. 13 0
      basic-demo-web/src/assets/main.less
  50. 0 0
      basic-demo-web/src/components/.gitkeep
  51. 154 0
      basic-demo-web/src/components/accessory/AccessoryItem.vue
  52. 272 0
      basic-demo-web/src/components/accessory/AccessoryUploader.vue
  53. 69 0
      basic-demo-web/src/components/accessory/UploadFileItem.vue
  54. 14 0
      basic-demo-web/src/components/accessory/constants.ts
  55. 229 0
      basic-demo-web/src/components/auth/OrgSelector.vue
  56. 58 0
      basic-demo-web/src/components/auth/SelectorList.vue
  57. 321 0
      basic-demo-web/src/components/auth/UserSelector.vue
  58. 62 0
      basic-demo-web/src/components/dialog.vue
  59. 40 0
      basic-demo-web/src/components/editTable/EditTableColumn.vue
  60. 5 0
      basic-demo-web/src/components/editTable/index.ts
  61. 23 0
      basic-demo-web/src/components/editTable/registerComp.ts
  62. 9 0
      basic-demo-web/src/components/editTable/type.ts
  63. 5 0
      basic-demo-web/src/components/types.ts
  64. 18 0
      basic-demo-web/src/config.ts
  65. 8 0
      basic-demo-web/src/directives/index.ts
  66. 35 0
      basic-demo-web/src/directives/permission.ts
  67. 19 0
      basic-demo-web/src/hooks/previous.ts
  68. 0 0
      basic-demo-web/src/index.d.ts
  69. 30 0
      basic-demo-web/src/main.ts
  70. 9 0
      basic-demo-web/src/plugins/icon.ts
  71. 1 0
      basic-demo-web/src/plugins/index.ts
  72. 219 0
      basic-demo-web/src/router/app-routers.ts
  73. 89 0
      basic-demo-web/src/router/index.ts
  74. 56 0
      basic-demo-web/src/stores/category.ts
  75. 53 0
      basic-demo-web/src/stores/core.ts
  76. 5 0
      basic-demo-web/src/stores/index.ts
  77. 26 0
      basic-demo-web/src/stores/level.ts
  78. 20 0
      basic-demo-web/src/stores/pinia.ts
  79. 32 0
      basic-demo-web/src/types/accessory.ts
  80. 75 0
      basic-demo-web/src/types/auditLog.ts
  81. 11 0
      basic-demo-web/src/types/counter.ts
  82. 8 0
      basic-demo-web/src/types/editTable.ts
  83. 7 0
      basic-demo-web/src/types/eight.ts
  84. 34 0
      basic-demo-web/src/types/eleven.ts
  85. 37 0
      basic-demo-web/src/types/four.ts
  86. 8 0
      basic-demo-web/src/types/holiday.ts
  87. 5 0
      basic-demo-web/src/types/list.ts
  88. 27 0
      basic-demo-web/src/types/nine.ts
  89. 22 0
      basic-demo-web/src/types/one.ts
  90. 30 0
      basic-demo-web/src/types/order.ts
  91. 20 0
      basic-demo-web/src/types/seven.ts
  92. 24 0
      basic-demo-web/src/types/six.ts
  93. 11 0
      basic-demo-web/src/types/ten.ts
  94. 35 0
      basic-demo-web/src/types/three.ts
  95. 10 0
      basic-demo-web/src/utils/authhelper.ts
  96. 16 0
      basic-demo-web/src/utils/frame.ts
  97. 97 0
      basic-demo-web/src/utils/http.ts
  98. 114 0
      basic-demo-web/src/utils/request.ts
  99. 13 0
      basic-demo-web/src/utils/security.ts
  100. 110 0
      basic-demo-web/src/views/CategoryManage.vue

+ 17 - 0
basic-demo-web/.eslintrc.js

@@ -0,0 +1,17 @@
+module.exports = {
+    root: true,
+    env: {
+        node: true
+    },
+    extends: [
+        'plugin:vue/flat/essential',
+        'plugin:vue/vue3-recommended',
+        'eslint:recommended'
+    ],
+    plugins: [
+        'vue'
+    ],
+    rules: {
+
+    }
+}

+ 78 - 0
basic-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?

+ 1 - 0
basic-demo-web/.npmrc

@@ -0,0 +1 @@
+registry=https://registry.npmmirror.com/

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

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

+ 8 - 0
basic-demo-web/.vscode/extensions.json

@@ -0,0 +1,8 @@
+{
+  "recommendations": [
+    "vue.volar",
+    "dbaeumer.vscode-eslint",
+    "esbenp.prettier-vscode",
+    "antfu.vite"
+  ]
+}

+ 20 - 0
basic-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
basic-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
basic-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
basic-demo-web/cacp/icon/svg-icons-0.1.2.tgz


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


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

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

+ 19 - 0
basic-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', 'plugin:vue/vue3-recommended', 'eslint:recommended'],
+//   ...vueTsEslintConfig(),
+//   {
+//     name: 'app/files-to-lint',
+//     files: ['**/*.{ts,mts,tsx,vue}'],
+//     rules: { '@typescript-eslint/no-explicit-any': 0 } // todo: 孙老,此处可以不加,程序中不建议用any,如果确实无法判断建议采用unknown
+//   },
+//   {
+//     name: 'app/files-to-ignore',
+//     ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**']
+//   },
+  
+//   skipFormatting
+// ]

+ 14 - 0
basic-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>

+ 60 - 0
basic-demo-web/mock/areacode.ts

@@ -0,0 +1,60 @@
+import { MockMethod } from 'vite-plugin-mock'
+import { type TypeDescriptor } from '@cacp/ui'
+import mockjs from 'mockjs'
+
+const areaCodes: TypeDescriptor[] = [
+  { name: '北京', code: '110000' },
+  { name: '天津', code: '120000' },
+  { name: '河北', code: '130000' },
+  { name: '山西', code: '140000' },
+  { name: '内蒙古', code: '150000' },
+  { name: '辽宁', code: '210000' },
+  { name: '吉林', code: '220000' },
+  { name: '黑龙江', code: '230000' },
+  { name: '上海', code: '310000' },
+  { name: '江苏', code: '320000' },
+  { name: '浙江', code: '330000' },
+  { name: '安徽', code: '340000' },
+  { name: '福建', code: '350000' },
+  { name: '江西', code: '360000' },
+  { name: '山东', code: '370000' },
+  { name: '河南', code: '410000' },
+  { name: '湖北', code: '420000' },
+  { name: '湖南', code: '430000' },
+  { name: '广东', code: '440000' },
+  { name: '广西', code: '450000' },
+  { name: '海南', code: '460000' },
+  { name: '重庆', code: '500000' },
+  { name: '四川', code: '510000' },
+  { name: '贵州', code: '520000' },
+  { name: '云南', code: '530000' },
+  { name: '西藏', code: '540000' },
+  { name: '陕西', code: '610000' },
+  { name: '甘肃', code: '620000' },
+  { name: '青海', code: '630000' },
+  { name: '宁夏', code: '640000' },
+  { name: '新疆', code: '650000' },
+  { name: '香港', code: '810000' },
+  { name: '澳门', code: '820000' },
+  { name: '台湾', code: '850000' }
+]
+const mockMethodArea: MockMethod[] = [
+  {
+    url: '/mock/api/areaCodes',
+    method: 'get',
+    timeout: 1000,
+    response: () => {
+      return {
+        code: '0',
+        data: areaCodes,
+        message: ''
+      }
+    }
+  }
+]
+mockjs.Random.extend({
+  areacode: function () {
+    return this.pick(areaCodes).code
+  }
+})
+export default mockMethodArea

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
basic-demo-web/mock/city.json


+ 64 - 0
basic-demo-web/mock/editTable.ts

@@ -0,0 +1,64 @@
+import Mock from 'mockjs'
+import { MockMethod } from 'vite-plugin-mock'
+const data = Mock.mock({
+  'data|10': [
+    {
+      id: '@id',
+      userCode: '@integer(1000, 7060)',
+      userName: '@cname',
+      address: '@city(true)',
+      phone: '@phone',
+      sex: '@sex'
+    }
+  ]
+}).data
+const mockMethodSeven: MockMethod[] = [
+  {
+    url: '/mock/api/edit/table/query',
+    method: 'get',
+    timeout: 1000,
+    response: ({ query }: { query: any }) => ({
+      code: '0',
+      data: filterData(query.userName, query.userCode)
+    })
+  },
+  {
+    url: '/mock/api/edit/table/save',
+    method: 'post',
+    timeout: 1000,
+    response: ({ body }: { body: any }) => {
+      if (Array.isArray(body)) {
+        data.splice(0)
+        body.forEach((item, index) => {
+          delete item.isEdit
+          data[index] = { ...item, id: Mock.Random.id() }
+        })
+      } else {
+        delete body.isEdit
+        if (body.id) {
+          const index = data.findIndex((f: any) => f.id === body.id)
+          data[index] = body
+          return
+        }
+        data.unshift({ ...body, id: Mock.Random.id() })
+      }
+    }
+  },
+  {
+    url: '/mock/api/edit/table/delete',
+    method: 'post',
+    timeout: 1000,
+    response: ({ body }: { body: any }) => {
+      const { id } = body
+      const index = data.findIndex((i: any) => i.id === id)
+      data.splice(index, 1)
+      return {
+        code: '0'
+      }
+    }
+  }
+]
+function filterData(userName: string, userCode: string) {
+  return data.filter((f: any) => f.userName.includes(userName ?? '') && f.userCode.toString().includes(userCode ?? ''))
+}
+export default mockMethodSeven

+ 32 - 0
basic-demo-web/mock/eight.ts

@@ -0,0 +1,32 @@
+import Mock from 'mockjs'
+import { MockMethod } from 'vite-plugin-mock'
+
+const mockMethodEight: MockMethod[] = [
+  {
+    url: '/mock/api/eight',
+    method: 'get',
+    timeout: 1000,
+    response: () => {
+      return {
+        code: '0',
+        data: {
+          ...Mock.mock({
+            customsCode: '@integer(1000, 7060)',
+            startDate: '@date("yyyy-MM-dd")',
+            endDate: function () {
+              let endDate = Mock.Random.date()
+              while (endDate < this.startDate) {
+                endDate = Mock.Random.date()
+              }
+              return endDate
+            },
+            person: '@cname',
+            flag: '@pick(1,0)'
+          })
+        }
+      }
+    }
+  }
+]
+
+export default mockMethodEight

+ 75 - 0
basic-demo-web/mock/eleven.ts

@@ -0,0 +1,75 @@
+import Mock from 'mockjs'
+import { MockMethod } from 'vite-plugin-mock'
+
+const mockMethodElevent: MockMethod[] = [
+  {
+    url: '/mock/api/eleven',
+    method: 'get',
+    timeout: 1000,
+    response: () => {
+      return {
+        code: '0',
+        ...Mock.mock({
+          'data|20': [
+            {
+              id: '@id',
+              name: '@ctitle(4,6)',
+              reason: '@reason',
+              email: '@integer(10000000, 99999999)',
+              taxInvoice: '@string(upper, 8, 8)',
+              source: '@taxInvoiceSource',
+              payer: '@cname',
+              total: '@natural(100, 100000)',
+              dutiable: '@natural(10, 100)',
+              date: '@datetime("2024-MM-dd hh:mm:ss")',
+              printing: '@cname',
+              printDate: function () {
+                let date = Mock.Random.datetime('2024-MM-dd hh:mm:ss')
+                while (date < this.date) {
+                  date = Mock.Random.datetime('2024-MM-dd hh:mm:ss')
+                }
+                return date
+              }
+            }
+          ]
+        })
+      }
+    }
+  },
+  {
+    url: '/mock/api/eleven/get/child',
+    method: 'get',
+    timeout: 1000,
+    response: () => {
+      return {
+        code: '0',
+        ...Mock.mock({
+          'data|2-5': [
+            {
+              id: '@id',
+              name: '@goods("name")',
+              date: '@datetime',
+              total: '@natural(100, 100000)',
+              code: '@natural(100000, 200000)',
+              type: '@cword(1, 1)',
+              price: '@natural(100000, 200000)',
+              unit: '@unit',
+              taxRate: '@natural(0, 50)',
+              tax: '@natural(100000, 200000)',
+              thingName: function () {
+                return this.name
+              },
+              thingTotal: '@natural(100, 100000)',
+              thingPrice: '@natural(100000, 200000)',
+              thingCode: function () {
+                return this.code
+              }
+            }
+          ]
+        })
+      }
+    }
+  }
+]
+
+export default mockMethodElevent

+ 95 - 0
basic-demo-web/mock/four.ts

@@ -0,0 +1,95 @@
+import Mock from 'mockjs'
+import { MockMethod } from 'vite-plugin-mock'
+
+let saveinfo: {
+  employee: string
+  name: string
+  sex: string
+  area: string
+  section: string
+  birthday: string
+  profession: string
+  educational: string
+  universities: string
+  aptityde: string
+  beGoodAt: string
+  introduction: string
+} | null = null
+
+const mockMethodFour: MockMethod[] = [
+  {
+    url: '/mock/api/four/userinfo',
+    method: 'get',
+    timeout: 1000,
+    response: () => ({
+      code: '0',
+      ...Mock.mock({
+        data: saveinfo ?? {
+          employee: '@integer(0)',
+          name: '@cname',
+          sex: '@sex',
+          area: '@integer(1, 3)',
+          section: '@integer(1, 3)',
+          birthday: '@datetime',
+          profession: '@profession',
+          educational: '@educational',
+          universities: '@universities',
+          aptityde: '@jobQualifications',
+          beGoodAt: '@expertise',
+          introduction: '唱跳两年半练习生'
+        }
+      })
+    })
+  },
+  {
+    url: '/mock/api/four/query',
+    method: 'get',
+    timeout: 1000,
+    response: () => ({
+      code: '0',
+      ...Mock.mock({
+        'data|5-10': [
+          {
+            id: '@id',
+            type: '类型@integer(1, 10)',
+            product: '@goods',
+            profession: '@profession',
+            name: '@ctitle',
+            // intro: '@csentence(5, 10)',
+            createName: '@cname',
+            createTime: '@datetime("2024-MM-dd hh:mm:ss")',
+            modification: '@cname',
+            modifiTime: '@datetime("2024-MM-dd hh:mm:ss")'
+          }
+        ]
+      })
+    })
+  },
+  {
+    url: '/mock/api/four/tags',
+    method: 'get',
+    timeout: 1000,
+    response: () => ({
+      code: '0',
+      ...Mock.mock({
+        data: {
+          'product|2-3': [{ name: '@product', type: '@colorTypes' }],
+          'profession|2-3': [{ name: '@profession', type: '@colorTypes' }],
+          'expert|2-3': [{ name: '@expertTags', type: '@colorTypes' }]
+        }
+      })
+    })
+  },
+  {
+    url: '/mock/api/four/save',
+    method: 'post',
+    timeout: 1000,
+    response: ({ body }: { body: any }) => {
+      console.log(body)
+      saveinfo = body
+      return { code: '0' }
+    }
+  }
+]
+
+export default mockMethodFour

+ 0 - 0
basic-demo-web/mock/index.ts


+ 113 - 0
basic-demo-web/mock/nine.ts

@@ -0,0 +1,113 @@
+import Mock from 'mockjs'
+import { MockMethod } from 'vite-plugin-mock'
+const codeData = Mock.mock({
+  'data|15-20': [
+    {
+      id: '@id',
+      shipName: '@shipNames',
+      shipCode: '@integer(10000, 10100)',
+      areaCode: '@integer(1000, 2010)'
+    }
+  ]
+}).data
+const data = [
+  ...Mock.mock({
+    'data|150-200': [
+      {
+        id: '@id',
+        shipId: `@pick("${codeData.map((v: any) => v.id).join('","')}")`,
+        userName: '@cname',
+        userCode: '@integer(1000, 2000)'
+      }
+    ]
+  }).data
+]
+
+const mockMethodNine: MockMethod[] = [
+  {
+    url: '/mock/api/nine/code',
+    method: 'get',
+    timeout: 1000,
+    response: () => {
+      return {
+        code: '0',
+        data: codeData
+      }
+    }
+  },
+  {
+    url: '/mock/api/nine/user',
+    method: 'get',
+    timeout: 1000,
+    response: ({ query }: { query: any }) => {
+      const id = query.shipId || codeData[0]?.id
+      const shipName = codeData.find((f: any) => f.id === id).shipName
+      return {
+        code: '0',
+        data: data.filter(({ shipId }) => shipId === id).map((v) => ({ ...v, shipName }))
+      }
+    }
+  },
+  {
+    url: '/mock/api/nine/code/save',
+    method: 'post',
+    timeout: 1000,
+    response: ({ body }: { body: any }) => {
+      updateData(codeData, body)
+      return {
+        code: '0'
+      }
+    }
+  },
+  {
+    url: '/mock/api/nine/code/del',
+    method: 'post',
+    timeout: 1000,
+    response: ({ body }: { body: any }) => {
+      deleteData(codeData, body.id)
+      return {
+        code: '0'
+      }
+    }
+  },
+  {
+    url: '/mock/api/nine/user/save',
+    method: 'post',
+    timeout: 1000,
+    response: ({ body }: { body: any }) => {
+      updateData(data, body)
+      return {
+        code: '0'
+      }
+    }
+  },
+  {
+    url: '/mock/api/nine/user/del',
+    method: 'post',
+    timeout: 1000,
+    response: ({ body }: { body: any }) => {
+      console.log(body)
+      deleteData(data, body.id)
+      return {
+        code: '0'
+      }
+    }
+  }
+]
+function deleteData(data: any[], id: string) {
+  const ids: string[] = [id]
+  console.log(ids)
+  ids.forEach((i) => {
+    const index = data.findIndex((d) => d.id === i)
+    data.splice(index, 1)
+  })
+}
+function updateData(data: any[], params: any) {
+  let index = data.length
+  if (params.id) {
+    index = data.findIndex((f) => f.id === params.id)
+  }
+  data[index] = params
+}
+
+export default mockMethodNine

+ 53 - 0
basic-demo-web/mock/one.ts

@@ -0,0 +1,53 @@
+import { MockMethod } from 'vite-plugin-mock'
+import Mock from 'mockjs'
+
+const mockData = Array.from({ length: 56 }, () => {
+  return Mock.mock({
+    id: '@id',
+    status: '@pick(0,1)',
+    areaCode: '@city(true) @areacode',
+    code: '@integer(1000, 9000)',
+    funcType: '@types',
+    operator: '@cname',
+    createTime: '@datetime',
+    lastModified: '@datetime'
+  })
+})
+
+const mockMethodOne: MockMethod[] = [
+  {
+    url: '/mock/api/one',
+    method: 'get',
+    timeout: 1000,
+    response: ({ query }: { query: any }) => {
+      const { code, areaCode, pageIndex, pageSize } = query
+      let filterData = mockData
+      if (areaCode) {
+        filterData = mockData.filter((item) => {
+          return item.areaCode == areaCode
+        })
+      }
+      if (code) {
+        filterData = mockData.filter((item) => item.code == code)
+      }
+      const _pageSize = Number(pageSize) ?? 20
+      const total = filterData.length
+      const pages = Math.ceil(total / _pageSize)
+      const offset = (pageIndex - 1) * _pageSize
+      const list = filterData.slice(offset, offset + _pageSize)
+      return {
+        code: '0',
+        data: {
+          pageNum: Number(pageIndex),
+          pageSize: Number(pageSize) ?? 20,
+          size: list.length,
+          pages: pages,
+          total: total,
+          list: list
+        }
+      }
+    }
+  }
+]
+
+export default mockMethodOne

+ 101 - 0
basic-demo-web/mock/seven.ts

@@ -0,0 +1,101 @@
+import Mock from 'mockjs'
+import { MockMethod } from 'vite-plugin-mock'
+import city from './city.json'
+
+interface TypicalSevenTable {
+  id: string
+  jobTitle: string
+  jobCode: string
+  personName: string
+  personCode: string
+  lastModified: string
+}
+interface CacheData {
+  [key: string]: TypicalSevenTable[]
+}
+
+const mockData = function () {
+  return Mock.mock({
+    'data|10-30': [
+      {
+        id: '@id',
+        jobTitle: function () {
+          const job = Mock.Random.randomJob()
+          this.jobCode = job.code
+          return job.name
+        },
+        jobCode: '',
+        personCode: '@integer(1000, 7060)',
+        personName: '@cname',
+        lastModified: '@datetime("2024-MM-dd hh:mm:ss")'
+      }
+    ]
+  }).data
+}
+const cacheData: CacheData = {}
+
+const mockMethodSeven: MockMethod[] = [
+  {
+    url: '/mock/api/seven/tree',
+    method: 'get',
+    timeout: 1000,
+    response: () => {
+      return {
+        code: '0',
+        data: city
+      }
+    }
+  },
+  {
+    url: '/mock/api/seven/delete',
+    method: 'get',
+    timeout: 1000,
+    response: ({ query }: { query: any }) => {
+      const { areaCode } = query
+      const id = query['id[]']
+      const data = cacheData[areaCode] ?? []
+      cacheData[areaCode] = data.filter((item) => !id.includes(item.id))
+      return {
+        code: '0',
+        data: 1
+      }
+    }
+  },
+  {
+    url: '/mock/api/seven/query',
+    method: 'get',
+    timeout: 1000,
+    response: ({ query }: { query: any }) => {
+      const { jobCode, areaCode, pageIndex, pageSize } = query
+
+      if (!cacheData[areaCode]) {
+        cacheData[areaCode] = mockData()
+      }
+      let filterData = cacheData[areaCode]
+      if (jobCode) {
+        filterData = filterData.filter((item) => {
+          return item.jobCode == jobCode
+        })
+      }
+      const _pageSize = Number(pageSize) ?? 20
+      const total = filterData.length
+      const pages = Math.ceil(total / _pageSize)
+      const offset = (pageIndex - 1) * _pageSize
+      const list = filterData.slice(offset, offset + _pageSize)
+
+      return {
+        code: '0',
+        data: {
+          pageNum: Number(pageIndex),
+          pageSize: Number(pageSize) ?? 20,
+          size: list.length,
+          pages: pages,
+          total: total,
+          list: list
+        }
+      }
+    }
+  }
+]
+
+export default mockMethodSeven

+ 35 - 0
basic-demo-web/mock/six.ts

@@ -0,0 +1,35 @@
+import Mock from 'mockjs'
+import { MockMethod } from 'vite-plugin-mock'
+
+const mockMethodSix: MockMethod[] = [
+  {
+    url: '/mock/api/six',
+    method: 'get',
+    timeout: 1000,
+    response: () => {
+      return {
+        code: '0',
+        ...Mock.mock({
+          'charts|2-3': [
+            {
+              name: '@pick(["102241","5748512","1024541"])',
+              'value|7': ['@float(5, 30, 0, 1, 26, 13, 8, 16)']
+            }
+          ]
+        }),
+        ...Mock.mock({
+          'dataTable|10-15': [
+            {
+              id: '@id',
+              name: '@goods("name")',
+              type: '@unit',
+              profession: '@foreignTrade'
+            }
+          ]
+        })
+      }
+    }
+  }
+]
+
+export default mockMethodSix

+ 95 - 0
basic-demo-web/mock/ten.ts

@@ -0,0 +1,95 @@
+import Mock from 'mockjs'
+import { MockMethod } from 'vite-plugin-mock'
+
+const mockMethodTen: MockMethod[] = [
+  {
+    url: '/mock/api/ten',
+    method: 'get',
+    timeout: 1000,
+    response: () => ({
+      code: '0',
+      ...Mock.mock({
+        data: [
+          {
+            name: '签证管理',
+            check: false,
+            color: '#1481e0',
+            children: [
+              { name: '我的证书', check: false, icon: 'CertMine', num: 2 },
+              { name: '证书审核', check: false, icon: 'CertVerify', num: 167 },
+              { name: '证书缓审', check: false, icon: 'CertDeferredReview', num: 51 },
+              { name: '证书制证', check: false, icon: 'CertMake', num: 533 },
+              { name: '证书归档', check: false, icon: 'CertFile', num: 605 }
+            ]
+          },
+          {
+            name: '企业基本信息管理',
+            check: false,
+            children: [{ name: '企业所属机关调转确认', check: false, icon: 'CompanyTransfer', num: 2 }],
+            color: '#ff9228'
+          },
+          {
+            name: '原产地预先核实',
+            check: false,
+            color: '#f57171',
+            children: [
+              { name: '原产地预选核实', check: false, icon: 'OriginPreSelect', num: 2 },
+              { name: '白名单审核', check: false, icon: 'WhitelistReview', num: 2 }
+            ]
+          },
+          {
+            name: '经核准出口商管理',
+            check: false,
+            color: '#00b4b4',
+            children: [
+              { name: '经核准出口商经办', check: false, icon: 'ExporterHandle', num: 167 },
+              { name: '经核准出口商初审', check: false, icon: 'ExportVerify', num: 51 },
+              { name: '经核准出口商复审', check: false, icon: 'ExporterReview', num: 533 },
+              { name: '经核准出口商信息查询与管理', check: false, icon: 'ExportInfoManage', num: 605 }
+            ]
+          },
+          {
+            name: '签证管理',
+            check: false,
+            color: '#1481e0',
+            children: [
+              { name: '我的证书', check: false, icon: 'CertMine', num: 2 },
+              { name: '证书审核', check: false, icon: 'CertVerify', num: 167 },
+              { name: '证书缓审', check: false, icon: 'CertDeferredReview', num: 51 },
+              { name: '证书制证', check: false, icon: 'CertMake', num: 533 },
+              { name: '证书归档', check: false, icon: 'CertFile', num: 605 }
+            ]
+          },
+          {
+            name: '企业基本信息管理',
+            check: false,
+            children: [{ name: '企业所属机关调转确认', check: false, icon: 'CompanyTransfer', num: 2 }],
+            color: '#ff9228'
+          },
+          {
+            name: '原产地预先核实',
+            check: false,
+            color: '#f57171',
+            children: [
+              { name: '原产地预选核实', check: false, icon: 'OriginPreSelect', num: 2 },
+              { name: '白名单审核', check: false, icon: 'WhitelistReview', num: 2 }
+            ]
+          },
+          {
+            name: '经核准出口商管理',
+            check: false,
+            color: '#00b4b4',
+            children: [
+              { name: '经核准出口商经办', check: false, icon: 'ExporterHandle', num: 167 },
+              { name: '经核准出口商初审', check: false, icon: 'ExportVerify', num: 51 },
+              { name: '经核准出口商复审', check: false, icon: 'ExporterReview', num: 533 },
+              { name: '经核准出口商信息查询与管理', check: false, icon: 'ExportInfoManage', num: 605 }
+            ]
+          }
+        ]
+      })
+    })
+  }
+]
+
+export default mockMethodTen

+ 121 - 0
basic-demo-web/mock/three.ts

@@ -0,0 +1,121 @@
+import Mock from 'mockjs'
+import { MockMethod } from 'vite-plugin-mock'
+const list = Mock.mock({
+  'list|4-10': [
+    {
+      id: '@id',
+      num: function () {
+        const date = new Date()
+        const year = date.getFullYear().toString().slice(-2)
+        const month = (date.getMonth() + 1).toString().padStart(2, '0')
+        const day = date.getDate().toString().padStart(2, '0')
+        const random = Math.random().toString().slice(-10)
+        return `BA${year}${month}${day}${random}`
+      },
+      name: '@ctitle',
+      num2: '@integer(1, 100)',
+      num3: '@integer(1, 100)',
+      num4: '@integer(1, 100)',
+      productName: '@goods("name")',
+      specifications: function () {
+        return this.unit
+      },
+      price: '@integer(1, 100)',
+      unit: '@unit',
+      statistics: '@ctitle',
+      money: '@ctitle',
+      total: '@integer(1, 100)',
+      country: '@country',
+      mcountry: '@country',
+      way: '@ctitle',
+      sprice: '@ctitle',
+      msprice: '@ctitle'
+    }
+  ]
+}).list
+
+const mockMethodTen: MockMethod[] = [
+  {
+    url: '/mock/api/three',
+    method: 'get',
+    timeout: 1000,
+    response: () => ({
+      code: '0',
+      data: {
+        info: {
+          filed1: '010120201000005031',
+          filed2: '010120201000005031',
+          filed3: '无纸/通关一体化',
+          filed4: '进口',
+          filed5: '',
+          filed6: '',
+          filed7: '@date',
+          filed8: '@date',
+          filed9: '@date',
+          filed10: '',
+          filed11: '',
+          filed12: '',
+          filed13: '中国大恒(集团)有限公司/1108919038/86652412100006491000000916',
+          filed14: '中国大恒(集团)有限公司/110891903866524121000064931108919038',
+          filed15: '中国大恒(团)有限公司/1108919038/866524121000064931/110891903',
+          filed16: 'no',
+          filed17: '北京关区',
+          filed18: '北仑海关',
+          filed19: '',
+          filed20: '阿富汗',
+          filed21: '天津保税区',
+          filed22: '天津保税区',
+          filed23: '海关总署/000000',
+          filed24: '阿富汗',
+          filed25: '北京天竺综合保税区',
+          filed26: '',
+          filed27: '包/袋',
+          filed28: '',
+          filed29: '海关总署/000000',
+          filed30: '水路运输',
+          filed31: 'UN281924',
+          filed32: 'T281924',
+          filed33: 'W20200917048',
+          filed34: '进口',
+          filed35: '海关总署/000000',
+          filed36: '一般贸易',
+          filed37: '一般征税',
+          filed38: '',
+          filed39: '',
+          filed40: '1.0%',
+          filed41: '海关总署/000000',
+          filed42: 'CIF',
+          filed43: '1',
+          filed44: '1',
+          filed45: '1',
+          filed46: '',
+          filed47: 'A',
+          filed48: '',
+          filed49: '',
+          filed50: '',
+          filed51: 'NM',
+          filed52: '',
+          filed53: '使用人',
+          filed54: '两段准入申请',
+          filed55: ''
+        },
+        list
+      }
+    })
+  },
+  {
+    url: '/mock/api/three/detail',
+    method: 'get',
+    timeout: 1000,
+    response: ({ query }: { query: any }) => {
+      const id = query.id || list[0].id
+      const data = list.find((v: any) => v.id === id)
+      return {
+        code: '0',
+        data
+      }
+    }
+  }
+]
+
+export default mockMethodTen

+ 169 - 0
basic-demo-web/mock/utils.ts

@@ -0,0 +1,169 @@
+import mockjs from 'mockjs'
+
+export const icons = [
+  'CertMine',
+  'CertVerify',
+  'CertDeferredReview',
+  'CertMake',
+  'CertFile',
+  'CompanyTransfer',
+  'OriginPreSelect',
+  'WhitelistReview',
+  'ExporterHandle',
+  'ExportVerify',
+  'ExporterReview',
+  'ExportInfoManage'
+] as const
+
+export const colorTypes = ['primary', 'success', 'info', 'warning', 'danger']
+
+export const sex = ['男', '女', '未知']
+
+export const universities = [
+  '清华大学',
+  '北京大学',
+  '西安交通大学',
+  '西北工业大学',
+  '复旦大学',
+  '浙江大学',
+  '南京大学',
+  '四川大学',
+  '电子科技大学'
+]
+
+export const educational = ['小学', '初中', '高中', '大专', '本科', '研究生']
+
+export const profession = ['计算机', '土木', '电气', '幼教', '金融', '考古']
+
+const reasons = ['不再需要', '信息错误', '重复提交', '不符合条件', '审核未通过', '用户取消', '资料补全']
+
+const taxInvoiceSource = [
+  '客户提交',
+  '供应商提供',
+  '在线平台',
+  '手动录入',
+  '第三方代开',
+  '电子发票系统',
+  '纸质发票扫描',
+  '自动导入',
+  '其他'
+]
+
+const goods = [
+  { name: '笔记本电脑', desc: '轻薄便携笔记本电脑' },
+  { name: '智能电脑', desc: '高清智能4k电视' },
+  { name: '蓝牙耳机', desc: '无线蓝牙降噪耳机' },
+  { name: '吸尘器', desc: '多功能手持吸尘器' },
+  { name: '冰箱', desc: '高效节能家用冰箱' },
+  { name: '相机', desc: '拍立得相机' },
+  { name: '背包', desc: '防水户外登山包' }
+]
+
+const units = ['件', '个', '台', '套', '箱']
+
+const shipNames = [
+  '报关单与货物',
+  '报关单与提单',
+  '报关单与发票',
+  '海关税号与商品',
+  '进出口许可证与商品',
+  '海关查验与货物',
+  '关税与商品',
+  '海关统计与贸易数据'
+]
+
+const jobTitle = [
+  { name: '报关员', code: 'C001' },
+  { name: '审计员', code: 'C002' },
+  { name: '查验员', code: 'C003' },
+  { name: '监督员', code: 'C004' },
+  { name: '关税分析师', code: 'C005' },
+  { name: '统计分析师', code: 'C006' },
+  { name: '稽查员', code: 'C007' },
+  { name: '安全检查员', code: 'C008' },
+  { name: '海关事务顾问', code: 'C009' }
+]
+const foreignTrade = ['国际贸易', '机械与设备', '计算机设备', '消费电子', '化工产品']
+const cuntries = ['中国', '美国', '日本', '澳大利亚', '英国', '法国', '德国', '俄罗斯', '印度尼西亚']
+const expertTags = ['初级', '中级', '高级', '资深', '专家', '架构师']
+const product = ['儿童', '商务', '家庭', '学生', '专业级']
+const jobQualifications = [
+  '计算机科学学士学位',
+  '5年开发经验',
+  'CSM认证',
+  '产品经理认证',
+  '注册会计师(CPA)',
+  'CFA认证',
+  '销售管理和CRM'
+]
+const expertise = ['精通Java', '精通Python', '擅长UI/UX设计', '精通审计', '打乒乓球', '踢足球']
+mockjs.Random.extend({
+  sex: function () {
+    return this.pick(sex)
+  },
+
+  colorTypes: function () {
+    return this.pick(colorTypes)
+  },
+
+  icons: function () {
+    return this.pick(icons)
+  },
+
+  universities: function () {
+    return this.pick(universities)
+  },
+
+  educational: function () {
+    return this.pick(educational)
+  },
+
+  profession: function () {
+    return this.pick(profession)
+  },
+
+  phone: function () {
+    return this.integer(130, 199) + '' + this.integer(10000000, 99999999)
+  },
+  types: function () {
+    return this.pick(['更新', '创建', '修改'])
+  },
+  reason: function () {
+    return this.pick(reasons)
+  },
+  taxInvoiceSource: function () {
+    return this.pick(taxInvoiceSource)
+  },
+  goods: function (value?: string) {
+    const g = this.pick(goods)
+    if (value) return g[value]
+    return g
+  },
+  unit: function () {
+    return this.pick(units)
+  },
+  shipNames: function () {
+    return this.pick(shipNames)
+  },
+  randomJob: function () {
+    return this.pick(jobTitle)
+  },
+  foreignTrade: function () {
+    return this.pick(foreignTrade)
+  },
+  country: function () {
+    return this.pick(cuntries)
+  },
+  expertTags: function () {
+    return this.pick(expertTags)
+  },
+  product: function () {
+    return this.pick(product)
+  },
+  jobQualifications: function () {
+    return this.pick(jobQualifications)
+  },
+  expertise: function () {
+    return this.pick(expertise)
+  }
+})

+ 73 - 0
basic-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
basic-demo-web/node_version

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

+ 73 - 0
basic-demo-web/package.json

@@ -0,0 +1,73 @@
+{
+  "name": "cacp-client-demo",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "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",
+    "@cacp/svg-icons": "file:./cacp/icon/svg-icons-0.1.2.tgz",
+    "@cacp/ui": "file:./cacp/ui/ui-0.3.17.tgz",
+    "@wangeditor/editor": "5.1.23",
+    "@wangeditor/editor-for-vue": "5.1.12",
+    "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",
+    "xlsx": "^0.18.5"
+  },
+  "devDependencies": {
+    "@rushstack/eslint-patch": "1.10.4",
+    "@tsconfig/node20": "20.1.4",
+    "@types/crypto-js": "4.2.2",
+    "@types/mockjs": "^1.0.6",
+    "@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",
+    "mockjs": "^0.1.10",
+    "npm-run-all2": "7.0.1",
+    "prettier": "3.3.3",
+    "typescript": "5.4.5",
+    "vite": "^4.0.0",
+    "vite-plugin-mock": "^1.0.0",
+    "vite-plugin-vue-devtools": "7.6.1",
+    "vue-tsc": "2.1.10"
+  }
+}

+ 11 - 0
basic-demo-web/public/config.js

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

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


+ 224 - 0
basic-demo-web/src/App.vue

@@ -0,0 +1,224 @@
+<template>
+  <el-container class="container">
+    <!-- 开发测试使用 -->
+    <el-aside width="190px" class="aside" v-if="DEV">
+      <el-menu class="menu">
+        <router-link to="/home">
+          <el-menu-item index="home" class="menu-item">
+            <el-icon>
+              <House />
+            </el-icon>
+            <span>示例页面</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/category-manage">
+          <el-menu-item index="category-manage" class="menu-item">
+            <el-icon>
+              <Setting />
+            </el-icon>
+            <span>分类管理</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/flow-manage">
+          <el-menu-item index="flow-manage" class="menu-item">
+            <el-icon>
+              <Setting />
+            </el-icon>
+            <span>流程管理</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/heai/message">
+          <el-menu-item index="heai-message" class="menu-item">
+            <el-icon>
+              <Setting />
+            </el-icon>
+            <span>消息列表</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/heai/message-type">
+          <el-menu-item index="heai-message-type" class="menu-item">
+            <el-icon>
+              <Setting />
+            </el-icon>
+            <span>消息类型列表</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/org-manage">
+          <el-menu-item index="order-manage" class="menu-item">
+            <el-icon>
+              <Setting />
+            </el-icon>
+            <span>组织管理</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/edit-table">
+          <el-menu-item index="edit-table" class="menu-item">
+            <el-icon>
+              <Setting />
+            </el-icon>
+            <span>可编辑表格</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/typical-one">
+          <el-menu-item index="typical-one" class="menu-item">
+            <el-icon>
+              <ScaleToOriginal />
+            </el-icon>
+            <span>典型页面一</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/typical-two">
+          <el-menu-item index="typical-two" class="menu-item">
+            <el-icon>
+              <ScaleToOriginal />
+            </el-icon>
+            <span>典型页面二</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/typical-three">
+          <el-menu-item index="typical-three" class="menu-item">
+            <el-icon>
+              <ScaleToOriginal />
+            </el-icon>
+            <span>典型页面三</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/typical-four">
+          <el-menu-item index="typical-four" class="menu-item">
+            <el-icon>
+              <ScaleToOriginal />
+            </el-icon>
+            <span>典型页面四</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/typical-five">
+          <el-menu-item index="typical-five" class="menu-item">
+            <el-icon>
+              <ScaleToOriginal />
+            </el-icon>
+            <span>典型页面五</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/typical-six">
+          <el-menu-item index="typical-six" class="menu-item">
+            <el-icon>
+              <ScaleToOriginal />
+            </el-icon>
+            <span>典型页面六</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/typical-seven">
+          <el-menu-item index="typical-seven" class="menu-item">
+            <el-icon>
+              <ScaleToOriginal />
+            </el-icon>
+            <span>典型页面七</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/typical-eight">
+          <el-menu-item index="typical-eight" class="menu-item">
+            <el-icon>
+              <ScaleToOriginal />
+            </el-icon>
+            <span>典型页面八</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/typical-nine">
+          <el-menu-item index="typical-nine" class="menu-item">
+            <el-icon>
+              <ScaleToOriginal />
+            </el-icon>
+            <span>典型页面九</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/typical-ten">
+          <el-menu-item index="typical-ten" class="menu-item">
+            <el-icon>
+              <ScaleToOriginal />
+            </el-icon>
+            <span>典型页面十</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/typical-eleven">
+          <el-menu-item index="typical-ten" class="menu-item">
+            <el-icon>
+              <ScaleToOriginal />
+            </el-icon>
+            <span>典型页面十一</span>
+          </el-menu-item>
+        </router-link>
+
+        <router-link to="/holiday/list">
+          <el-menu-item index="holiday-list" class="menu-item">
+            <el-icon>
+              <ScaleToOriginal />
+            </el-icon>
+            <span>节假日</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/counter/list">
+          <el-menu-item index="counter-list" class="menu-item">
+            <el-icon>
+              <ScaleToOriginal />
+            </el-icon>
+            <span>计数器</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/audit-log/list">
+          <el-menu-item index="auditLog-list" class="menu-item">
+            <el-icon>
+              <ScaleToOriginal />
+            </el-icon>
+            <span>用户日志</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/form/form-type">
+          <el-menu-item index="form-type" class="menu-item">
+            <el-icon>
+              <ScaleToOriginal />
+            </el-icon>
+            <span>表单类型</span>
+          </el-menu-item>
+        </router-link>
+        <router-link to="/form/dept-sn">
+          <el-menu-item index="dept-sn" class="menu-item">
+            <el-icon>
+              <ScaleToOriginal />
+            </el-icon>
+            <span>表单代字</span>
+          </el-menu-item>
+        </router-link>
+      </el-menu>
+    </el-aside>
+    <el-container class="content">
+      <div style="width: 100%; height: 100%">
+        <router-view />
+      </div>
+    </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>

+ 57 - 0
basic-demo-web/src/apis/accessory.ts

@@ -0,0 +1,57 @@
+import type { AxiosResponse } from 'axios'
+import download from 'js-file-download'
+import config from '@/config'
+
+import request from '@/utils/request'
+import type { Accessory } from '@/types/accessory'
+import { SuccessResultCode, type Result } 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
+}

+ 19 - 0
basic-demo-web/src/apis/audit.ts

@@ -0,0 +1,19 @@
+import type { AxiosResponse } from 'axios'
+import type { PageInfo, Result } from '@cacp/ui'
+import request from '@/utils/request'
+import type { AuditLogInfo, AuditLogQuery } from '@/types/auditLog'
+const contextPath = '/auditlog'
+
+export async function getPage(query: AuditLogQuery): Promise<Result<PageInfo<AuditLogInfo>>> {
+  const res: AxiosResponse<Result<PageInfo<AuditLogInfo>>> = await request.post(`${contextPath}/get-page`, query)
+  return res.data
+}
+
+export async function get(logId: string): Promise<Result<AuditLogInfo>> {
+  const res: AxiosResponse<Result<AuditLogInfo>> = await request.get(`${contextPath}/get`, {
+    params: {
+      logId
+    }
+  })
+  return res.data
+}

+ 61 - 0
basic-demo-web/src/apis/auth.ts

@@ -0,0 +1,61 @@
+import type { AxiosResponse } from 'axios'
+import request from '@/utils/request'
+import type { CacpOrganization, CacpUser, Result } from '@cacp/ui'
+
+const contextPath = '/auth'
+
+export async function getRootOrganization(): Promise<Result<CacpOrganization>> {
+  const res: AxiosResponse<Result<CacpOrganization>> = await request.get(`${contextPath}/get-root-organization`)
+  return res.data
+}
+export async function getOrganization(orgId: string): Promise<Result<CacpOrganization>> {
+  const res: AxiosResponse<Result<CacpOrganization>> = await request.get(
+    `${contextPath}/get-organization?orgId=${orgId}`
+  )
+  return res.data
+}
+
+export async function getOrganizationByPath(orgPath: string): Promise<Result<CacpOrganization>> {
+  const res: AxiosResponse<Result<CacpOrganization>> = await request.get(
+    `${contextPath}/get-organization-by-path?orgPath=${encodeURIComponent(orgPath)}`
+  )
+  return res.data
+}
+
+export async function getOrganizationListByParent(
+  parentPath: string,
+  oneLevel: boolean
+): Promise<Result<Array<CacpOrganization>>> {
+  const res: AxiosResponse<Result<Array<CacpOrganization>>> = await request.get(
+    `${contextPath}/get-organization-list-by-parent?parentPath=${encodeURIComponent(parentPath)}&oneLevel=${oneLevel}`
+  )
+  return res.data
+}
+
+export async function getUser(parentId: string, userId: string): Promise<Result<CacpUser>> {
+  const res: AxiosResponse<Result<CacpUser>> = await request.get(
+    `${contextPath}/get-user?parentId=${parentId}&userId=${userId}`
+  )
+  return res.data
+}
+
+export async function getUserListById(userId: string): Promise<Result<Array<CacpUser>>> {
+  const res: AxiosResponse<Result<Array<CacpUser>>> = await request.get(
+    `${contextPath}/get-user-list-by-id?userId=${userId}`
+  )
+  return res.data
+}
+
+export async function getUserListByParent(parentPath: string, oneLevel: boolean): Promise<Result<Array<CacpUser>>> {
+  const res: AxiosResponse<Result<Array<CacpUser>>> = await request.get(
+    `${contextPath}/get-user-list-by-parent?parentPath=${encodeURIComponent(parentPath)}&oneLevel=${oneLevel}`
+  )
+  return res.data
+}
+
+export async function queryUserListByCondition(parentPath: string, keyword: string): Promise<Result<Array<CacpUser>>> {
+  const res: AxiosResponse<Result<Array<CacpUser>>> = await request.get(
+    `${contextPath}/query-user-list-by-condition?parentPath=${encodeURIComponent(parentPath)}&keyword=${keyword}`
+  )
+  return res.data
+}

+ 10 - 0
basic-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
+}

+ 19 - 0
basic-demo-web/src/apis/counter.ts

@@ -0,0 +1,19 @@
+import type { AxiosResponse } from 'axios'
+import type { Result } from '@cacp/ui'
+import request from '@/utils/request'
+import type { CacpCounter } from '@/types/counter'
+const contextPath = '/counter'
+
+export async function getList(appCode: string): Promise<Result<CacpCounter[]>> {
+  const res: AxiosResponse<Result<CacpCounter[]>> = await request.get(`${contextPath}/get-list`, {
+    params: {
+      appCode
+    }
+  })
+  return res.data
+}
+
+export async function del(appCode: string, counterPrefix: string): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(`${contextPath}/delete?appCode=${appCode}&counterPrefix=${counterPrefix}`)
+  return res.data
+}

+ 0 - 0
basic-demo-web/src/apis/flow.ts


+ 71 - 0
basic-demo-web/src/apis/form.ts

@@ -0,0 +1,71 @@
+import type { AxiosResponse } from 'axios'
+import type { Result } from '@cacp/ui'
+import type { DeptSn, FormType, WorkflowRelation } from '@/views/form/types'
+import request from '@/utils/request'
+
+const contextPath = '/form'
+
+export async function getDeptSnList(): Promise<Result<DeptSn[]>> {
+  const res: AxiosResponse<Result<DeptSn[]>> = await request.get(
+    `${contextPath}/dept-sn/get-list`
+  )
+  return res.data
+}
+export async function insertDeptSn(info: DeptSn): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(
+    `${contextPath}/dept-sn/insert`,
+    info
+  )
+  return res.data
+}
+export async function deleteDeptSn(deptId: string): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(
+    `${contextPath}/dept-sn/delete?deptId=${deptId}`
+  )
+  return res.data
+}
+
+export async function getFormTypeList(): Promise<Result<FormType[]>> {
+  const res: AxiosResponse<Result<FormType[]>> = await request.get(
+    `${contextPath}/form-type/get-list`
+  )
+  return res.data
+}
+export async function insertFormType(info: FormType): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(
+    `${contextPath}/form-type/insert`,
+    info
+  )
+  return res.data
+}
+export async function updateFormType(info: FormType): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(
+    `${contextPath}/form-type/update`,
+    info
+  )
+  return res.data
+}
+export async function deleteFormType(deptId: string): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(
+    `${contextPath}/form-type/delete?deptId=${deptId}`
+  )
+  return res.data
+}
+
+export async function getWorkflowRelationList(formCode: string): Promise<Result<WorkflowRelation[]>> {
+  const res: AxiosResponse<Result<WorkflowRelation[]>> = await request.get(
+    `${contextPath}/workflow-relation/get-list`,
+    { params: { formCode: formCode }}
+  )
+  return res.data
+}
+export async function insertWorkflowRelation(info: WorkflowRelation): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(`${contextPath}/workflow-relation/insert`, info)
+  return res.data
+}
+export async function deleteWorkflowRelation(formCode: string, deptId: string, processRange: number): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(
+    `${contextPath}/workflow-relation/delete?formCode=${formCode}&deptId=${deptId}&processRange=${processRange}`
+  )
+  return res.data
+}

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

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

+ 85 - 0
basic-demo-web/src/apis/heai.ts

@@ -0,0 +1,85 @@
+import type { AxiosResponse } from 'axios'
+import type {
+  Result,
+  PageInfo,
+  HeaiMessageTypeInfo,
+  HeaiMessageInfo,
+  HeaiMessageQueryInfo,
+  HeaiMessageFlag
+} from '@cacp/ui'
+import request from '@/utils/request'
+
+const contextPath = '/heai'
+
+export async function getMessageTypeList(): Promise<Result<HeaiMessageTypeInfo[]>> {
+  const res: AxiosResponse<Result<HeaiMessageTypeInfo[]>> = await request.get(
+    `${contextPath}/get-message-type-list`
+  )
+  return res.data
+}
+export async function insertMessageType(info: HeaiMessageTypeInfo): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(
+    `${contextPath}/insert-message-type`,
+    info
+  )
+  return res.data
+}
+
+export async function deleteMessageType(typeCode: string): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(
+    `${contextPath}/delete-message-type?typeCode=${typeCode}`
+  )
+  return res.data
+}
+
+export async function queryMessagePage(query: HeaiMessageQueryInfo): Promise<Result<PageInfo<HeaiMessageInfo>>> {
+  const res: AxiosResponse<Result<PageInfo<HeaiMessageInfo>>> = await request.post(
+    `${contextPath}/query-message-page`,
+    query
+  )
+  return res.data
+}
+
+export async function updateMessage(info: {appMsgId: string, content: string}): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(`${contextPath}/update-message`, info)
+  return res.data
+}
+
+export async function transferError(appMsgId: string): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(`${contextPath}/transfer-error`, null, {
+    params: { appMsgId }
+  })
+  return res.data
+}
+export async function transferCmplete(appMsgId: string, type: boolean): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(
+    `${contextPath}/transfer-complete`,
+    null,
+    {
+      params: { appMsgId, type }
+    }
+  )
+  return res.data
+}
+
+export async function transferWaiting(appMsgId: string, type: boolean, flag: HeaiMessageFlag): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(
+    `${contextPath}/transfer-waiting`,
+    null,
+    {
+      params: { appMsgId, type, flag }
+    }
+  )
+  return res.data
+}
+
+export async function getRecieve(appMsgId: string, type: boolean, flag: HeaiMessageFlag): Promise<Result<HeaiMessageInfo>> {
+  const res: AxiosResponse<Result<HeaiMessageInfo>> = await request.get(
+    `${contextPath}/get-message`,
+    {
+      params: { appMsgId, type, flag }
+    }
+  )
+  return res.data
+  
+}

+ 21 - 0
basic-demo-web/src/apis/holiday.ts

@@ -0,0 +1,21 @@
+import type { AxiosResponse } from 'axios'
+import type { Result } from '@cacp/ui'
+import request from '@/utils/request'
+import type { CacpHoliday } from '@/types/holiday'
+
+const contextPath = '/holiday'
+
+export async function getList(customsCode: string, year: number): Promise<Result<CacpHoliday[]>> {
+  const res: AxiosResponse<Result<CacpHoliday[]>> = await request.get(`${contextPath}/get-list`, {
+    params:{
+        year,
+        customsCode
+    }
+  })
+  return res.data
+}
+
+export async function saveList(list: CacpHoliday[]): Promise<Result<number>> {
+    const res: AxiosResponse<Result<number>> = await request.post(`${contextPath}/save-list`, list)
+    return res.data
+  }

+ 163 - 0
basic-demo-web/src/apis/mock.ts

@@ -0,0 +1,163 @@
+import http from '@/utils/http'
+import type { TypicalNineCode, TypicalNineCodeParams, TypicalNineUser, TypicalNineUserParams } from '@/types/nine'
+import type { TypicalFourTable, TypicalFourTags, TypicalFourUserInfo } from '@/types/four'
+import type { TypicalSevenDeleteQuery, TypicalSevenQuery, TypicalSevenTable } from '@/types/seven'
+import type { TypicalSixInfo, TypicalSixQueryParams } from '@/types/six'
+import type { TypicalTenInfo } from '@/types/ten'
+import type { PageInfo, Result, TreeInfo, TypeDescriptor } from '@cacp/ui'
+import type { AxiosResponse } from 'axios'
+import type { TypicalOne, TypicalOneQuery } from '@/types/one'
+import type { TypicalThree, TypicalThreeList } from '@/types/three'
+import type { TypicalElevenInfo, TypicalElevenThingInfo } from '@/types/eleven'
+import type { EditTableUser } from '@/types/editTable'
+
+const contextPath = '/mock'
+
+export async function getTypicalTen() {
+  const res: AxiosResponse<Result<TypicalTenInfo[]>> = await http.get(`${contextPath}/api/ten`)
+  return res.data
+}
+
+export async function getTypicalSix(params: TypicalSixQueryParams) {
+  const res: AxiosResponse<TypicalSixInfo> = await http.get(`${contextPath}/api/six`, {
+    params
+  })
+  return res.data
+}
+
+export async function getTypicalFourUserinfo() {
+  const res: AxiosResponse<Result<TypicalFourUserInfo>> = await http.get(`${contextPath}/api/four/userinfo`)
+  return res.data
+}
+
+export async function getTypicalFourTags() {
+  const res: AxiosResponse<Result<TypicalFourTags>> = await http.get(`${contextPath}/api/four/tags`)
+  return res.data
+}
+
+export async function getTypicalFourTable() {
+  const res: AxiosResponse<Result<TypicalFourTable[]>> = await http.get(`${contextPath}/api/four/query`)
+  return res.data
+}
+
+export async function typicalFourSave(params: TypicalFourUserInfo) {
+  const res: AxiosResponse = await http.post(`${contextPath}/api/four/save`, params)
+  return res.data
+}
+
+export async function typicalSevenTree() {
+  const res: AxiosResponse<Result<TreeInfo[]>> = await http.get(`${contextPath}/api/seven/tree`)
+  return res.data
+}
+
+export async function typicalSevenQuery(params: TypicalSevenQuery) {
+  const res: AxiosResponse<Result<PageInfo<TypicalSevenTable>>> = await http.get(`${contextPath}/api/seven/query`, {
+    params
+  })
+  return res.data
+}
+
+export async function typicalSevenDelete(params: TypicalSevenDeleteQuery) {
+  const res: AxiosResponse<Result<number>> = await http.get(`${contextPath}/api/seven/delete`, {
+    params
+  })
+  return res.data
+}
+
+export async function typicalEight() {
+  const res: AxiosResponse<Result<TypicalSevenTable[]>> = await http.get(`${contextPath}/api/eight`)
+  return res.data
+}
+
+export async function typicalNineUser(shipId: string) {
+  const res: AxiosResponse<Result<TypicalNineUser[]>> = await http.get(`${contextPath}/api/nine/user`, {
+    params: {
+      shipId
+    }
+  })
+  return res.data
+}
+
+export async function typicalNineCode() {
+  const res: AxiosResponse<Result<TypicalNineCode[]>> = await http.get(`${contextPath}/api/nine/code`)
+  return res.data
+}
+
+export async function typicalNineUserSave(data: TypicalNineUserParams) {
+  const res: AxiosResponse<Result<void>> = await http.post(`${contextPath}/api/nine/user/save`, data)
+  return res.data
+}
+
+export async function typicalNineCodeSave(data: TypicalNineCodeParams) {
+  const res: AxiosResponse<Result<void>> = await http.post(`${contextPath}/api/nine/code/save`, data)
+  return res.data
+}
+
+export async function typicalNineUserDel(id: string | string[]) {
+  const res: AxiosResponse<Result<void>> = await http.post(`${contextPath}/api/nine/user/del`, { id })
+  return res.data
+}
+
+export async function typicalNineCodeDel(id: string | string[]) {
+  const res: AxiosResponse<Result<void>> = await http.post(`${contextPath}/api/nine/code/del`, { id })
+  return res.data
+}
+
+export async function typicalOneQuery(params: TypicalOneQuery) {
+  const res: AxiosResponse<Result<PageInfo<TypicalOne>>> = await http.get(`${contextPath}/api/one`, {
+    params
+  })
+  return res.data
+}
+
+export async function typicalThree() {
+  const res: AxiosResponse<Result<TypicalThree>> = await http.get(`${contextPath}/api/three`)
+  return res.data
+}
+
+export async function typicalThreeDetail(id: string) {
+  const res: AxiosResponse<Result<TypicalThreeList>> = await http.get(`${contextPath}/api/three/detail`, {
+    params: {
+      id
+    }
+  })
+  return res.data
+}
+
+export async function typicalEleven(params: any) {
+  const res: AxiosResponse<Result<TypicalElevenInfo[]>> = await http.get(`${contextPath}/api/eleven`, {
+    params
+  })
+  return res.data
+}
+
+export async function typicalElevenGetChild(id: string) {
+  const res: AxiosResponse<Result<TypicalElevenThingInfo[]>> = await http.get(`${contextPath}/api/eleven/get/child`, {
+    params: {
+      id
+    }
+  })
+  return res.data
+}
+
+export async function editTableQuery(params: any) {
+  const res: AxiosResponse<Result<EditTableUser[]>> = await http.get(`${contextPath}/api/edit/table/query`, {
+    params
+  })
+  return res.data
+}
+
+export async function editTableSave(params: any) {
+  const res: AxiosResponse<Result<void>> = await http.post(`${contextPath}/api/edit/table/save`, params)
+  return res.data
+}
+
+export async function editTableDelete(id: string) {
+  const res: AxiosResponse<Result<void>> = await http.post(`${contextPath}/api/edit/table/delete`, { id })
+  return res.data
+}
+
+export async function getAreaCodes(): Promise<Result<Array<TypeDescriptor>>> {
+  const res: AxiosResponse<Result<Array<TypeDescriptor>>> = await http.get(`${contextPath}/api/areaCodes`)
+  return res.data
+}

+ 61 - 0
basic-demo-web/src/apis/order.ts

@@ -0,0 +1,61 @@
+import type { AxiosResponse } from 'axios'
+import type { PageInfo, Result, TypeDescriptor } from '@cacp/ui'
+
+import request from '@/utils/request'
+import type { OrderInfo, OrderCategoryInfo, OrderQueryInfo } from '@/types/order'
+
+const contextPath = '/order'
+
+// 获取分类列表
+export async function getCategoryList(): Promise<Result<Array<OrderCategoryInfo>>> {
+  const res: AxiosResponse<Result<Array<OrderCategoryInfo>>> = await request.get(`${contextPath}/get-category-list`)
+  return res.data
+}
+// 插入分类
+export async function insertCategory(categoryInfo: OrderCategoryInfo): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(`${contextPath}/insert-category`, categoryInfo)
+  return res.data
+}
+// 更新分类
+export async function updateCategory(categoryInfo: OrderCategoryInfo): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(`${contextPath}/update-category`, categoryInfo)
+  return res.data
+}
+// 删除分类
+export async function deleteCategory(categoryCode: string): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(
+    `${contextPath}/delete-category?categoryCode=${categoryCode}`
+  )
+  return res.data
+}
+//获取单据
+export async function getOrder(orderId: string): Promise<Result<OrderInfo>> {
+  const res: AxiosResponse<Result<OrderInfo>> = await request.get(`${contextPath}/get-order?orderId=${orderId}`)
+  return res.data
+}
+//获取单据列表
+export async function getOrderList(query: OrderQueryInfo): Promise<Result<PageInfo<OrderInfo>>> {
+  const res: AxiosResponse<Result<PageInfo<OrderInfo>>> = await request.post(`${contextPath}/get-order-list`, query)
+  return res.data
+}
+// 插入单据
+export async function insertOrder(orderInfo: OrderInfo): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(`${contextPath}/insert-order`, orderInfo)
+  return res.data
+}
+
+// 更新单据
+export async function updateOrder(orderInfo: OrderInfo): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(`${contextPath}/update-order`, orderInfo)
+  return res.data
+}
+// 删除单据
+export async function deleteOrder(orderId: string): Promise<Result<number>> {
+  const res: AxiosResponse<Result<number>> = await request.post(`${contextPath}/delete-order?orderId=${orderId}`)
+  return res.data
+}
+// 获取等级
+export async function getLevelList(): Promise<Result<Array<TypeDescriptor>>> {
+  const res: AxiosResponse<Result<Array<TypeDescriptor>>> = await request.get(`${contextPath}/get-level-list`)
+  return res.data
+}

+ 40 - 0
basic-demo-web/src/assets/base.less

@@ -0,0 +1,40 @@
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+  margin: 0;
+}
+
+ul,ol{
+  list-style: none;
+  padding: 0;
+}
+
+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
basic-demo-web/src/assets/images/404.png


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


+ 13 - 0
basic-demo-web/src/assets/main.less

@@ -0,0 +1,13 @@
+@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;
+  }
+  

+ 0 - 0
basic-demo-web/src/components/.gitkeep


+ 154 - 0
basic-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
basic-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
basic-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
basic-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
+}

+ 229 - 0
basic-demo-web/src/components/auth/OrgSelector.vue

@@ -0,0 +1,229 @@
+<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"
+  >
+    <div class="selector">
+      <div class="selector-left">
+        <el-scrollbar>
+          <el-tree
+            ref="selectorTreeRef"
+            :props="treeProps"
+            node-key="fullPathName"
+            :show-checkbox="true"
+            :check-strictly="true"
+            :default-checked-keys="selectedKeys"
+            :load="onLazyLoad"
+            lazy
+            @check-change="onCheckChange"
+            class="cacp-pd-s"
+          >
+            <template #default="{ data }">
+              <el-icon class="cacp-mr-s" :size="16">
+                <Grid />
+              </el-icon>
+              <span>{{ data.oguName }}</span>
+            </template>
+          </el-tree>
+        </el-scrollbar>
+      </div>
+      <div class="selector-right">
+        <div class="selector-toolbar">
+          <span class="count">已选 {{ count }} 个</span><el-link type="primary" @click.prevent="onClear">清空</el-link>
+        </div>
+        <div class="selector-list">
+          <selector-list :dataList="selectedList" @on-delete="onRemove"></selector-list>
+        </div>
+      </div>
+    </div>
+    <template #footer>
+      <el-button @click="onClose">取消</el-button>
+      <el-button type="primary" @click="onSave">确定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue'
+import type { ElTree } from 'element-plus'
+import type TreeNode from 'element-plus/es/components/tree/src/model/node'
+import SelectorList from './SelectorList.vue'
+import { cloneDeep } from 'lodash-es'
+import * as apis from '@/apis/auth'
+import type { CacpOrganization } from '@cacp/ui'
+import { SuccessResultCode } from '@cacp/ui'
+const selectorTreeRef = ref<InstanceType<typeof ElTree>>()
+const props = withDefaults(
+  defineProps<{
+    visible: boolean
+    multiple?: boolean
+    value?: Array<CacpOrganization>
+    rootPath?: string
+  }>(),
+  {
+    visible: false,
+    multiple: false,
+    value: () => [],
+    rootPath: ''
+  }
+)
+const emits = defineEmits<{
+  (e: 'on-close'): void
+  (e: 'on-save', value: Array<CacpOrganization>): void
+}>()
+const treeProps = {
+  label: 'oguName'
+}
+const cachedList = ref<Array<CacpOrganization>>([])
+const selectedKeys = ref<Array<string>>([])
+const selectedList = computed<Array<CacpOrganization>>(() => {
+  return cachedList.value.filter((o) => selectedKeys.value.includes(o.fullPathName))
+})
+const count = computed<number>(() => {
+  return selectedKeys.value.length
+})
+function onOpen(): void {
+  selectedKeys.value = props.value ? props.value.map((o) => o.fullPathName) : []
+  cachedList.value = props.value ? cloneDeep(props.value) : []
+}
+
+function onClose(): void {
+  emits('on-close')
+}
+
+function onSave(): void {
+  emits('on-save', selectedList.value)
+}
+async function onLazyLoad(
+  node: TreeNode,
+  resolve: (data: Array<CacpOrganization>) => void,
+  reject: () => void
+): Promise<void> {
+  if (node.level === 0) {
+    let root: CacpOrganization
+    if (props.rootPath) {
+      const result = await apis.getOrganizationByPath(props.rootPath)
+      if (result.code === SuccessResultCode && result.data) {
+        root = result.data
+        resolve([root])
+      } else {
+        if (reject) {
+          reject()
+        }
+      }
+    } else {
+      const result = await apis.getRootOrganization()
+      if (result.code === SuccessResultCode && result.data) {
+        root = result.data
+        resolve([root])
+      } else {
+        if (reject) {
+          reject()
+        }
+      }
+    }
+  } else {
+    const parent = node.data
+    const result = await apis.getOrganizationListByParent(parent.fullPathName, true)
+    if (result.code === SuccessResultCode) {
+      const children = result.data ?? []
+      resolve(children)
+    } else {
+      if (reject) {
+        reject()
+      }
+    }
+  }
+}
+
+function onCheckChange(data: CacpOrganization, checked: boolean) {
+  if (checked) {
+    handleCachedList([data])
+    if (!props.multiple) {
+      selectedKeys.value.splice(0, 1, data.fullPathName)
+    } else {
+      if (!selectedKeys.value.includes(data.fullPathName)) {
+        selectedKeys.value.push(data.fullPathName)
+      }
+    }
+  } else {
+    const idx = selectedKeys.value.indexOf(data.fullPathName)
+    if (idx > -1) {
+      selectedKeys.value.splice(idx, 1)
+    }
+  }
+  handleCheckStatus()
+}
+
+function onRemove(fullPathName: string): void {
+  const idx = selectedKeys.value.indexOf(fullPathName)
+  if (idx > -1) {
+    selectedKeys.value.splice(idx, 1)
+  }
+  handleCheckStatus()
+}
+
+function onClear(): void {
+  selectedKeys.value = []
+  handleCheckStatus()
+}
+
+function handleCachedList(list: Array<CacpOrganization>): void {
+  for (const org of list) {
+    const idx = cachedList.value.findIndex((o) => o.fullPathName === org.fullPathName)
+    if (idx > -1) {
+      cachedList.value.splice(idx, 1)
+    }
+    cachedList.value.push(org)
+  }
+}
+
+function handleCheckStatus(): void {
+  if (selectorTreeRef.value) {
+    selectorTreeRef.value.setCheckedKeys(selectedKeys.value, false)
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.selector {
+  display: flex;
+  align-items: flex-start;
+  border: 1px solid var(--el-border-color);
+  border-radius: var(--el-border-radius-base);
+  min-height: 260px;
+  max-height: 400px;
+  align-items: stretch;
+  &-left {
+    flex: 1;
+    border-right: 1px solid var(--el-border-color);
+  }
+  &-right {
+    flex: 1;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+  }
+  &-list {
+    flex: 1;
+    overflow: hidden;
+  }
+  &-toolbar {
+    line-height: 32px;
+    height: 32px;
+    padding: 0 var(--cacp-padding-space-s);
+    font-size: var(--font-size-s);
+    display: flex;
+    align-items: center;
+    border-bottom: 1px solid var(--el-border-color);
+    justify-content: space-between;
+  }
+}
+</style>

+ 58 - 0
basic-demo-web/src/components/auth/SelectorList.vue

@@ -0,0 +1,58 @@
+<template>
+  <el-scrollbar>
+    <ul>
+      <li class="selector-item" v-for="item in dataList" :key="item.fullPathName" @click="onClick(item)">
+        <span class="selector-item-name">{{ item.oguName }}</span>
+        <el-icon class="selector-item-close" @click="onDelete(item.fullPathName)" :size="16">
+          <Close />
+        </el-icon>
+      </li>
+    </ul>
+  </el-scrollbar>
+</template>
+<script lang="ts" setup>
+import type { BaseCacpOgu } from '@cacp/ui'
+
+defineProps<{
+  dataList: Array<BaseCacpOgu>
+}>()
+const emits = defineEmits<{
+  (e: 'on-click', value: BaseCacpOgu): void
+  (e: 'on-delete', value: string): void
+}>()
+function onClick(value: BaseCacpOgu) {
+  emits('on-click', value)
+}
+function onDelete(value: string) {
+  emits('on-delete', value)
+}
+</script>
+<style lang="less" scoped>
+.selector-item {
+  height: 32px;
+  line-height: 32px;
+  padding: 0 var(--cacp-padding-space-s);
+  font-size: var(--font-size-s);
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  margin-bottom: 1px;
+  overflow: hidden;
+  &-name {
+    flex: 1;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    white-space: nowrap;
+  }
+  &-close {
+    cursor: pointer;
+    opacity: 0.8;
+    &:hover {
+      opacity: 1;
+    }
+  }
+  &:hover {
+    background-color: var(--el-fill-color-light);
+  }
+}
+</style>

+ 321 - 0
basic-demo-web/src/components/auth/UserSelector.vue

@@ -0,0 +1,321 @@
+<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"
+  >
+    <div class="selector">
+      <div class="selector-left">
+        <cacp-autocomplete
+          v-model="selectedKeys"
+          :multiple="multiple"
+          value-key="fullPathName"
+          :fetch-suggestions="querySearch"
+          clearable
+          placeholder="请输入关键字"
+          @select="onSelect"
+        ></cacp-autocomplete>
+        <div class="selector-tree">
+          <el-scrollbar>
+            <el-tree
+              ref="selectorTreeRef"
+              :props="treeProps"
+              node-key="fullPathName"
+              :show-checkbox="true"
+              :check-strictly="true"
+              :default-checked-keys="selectedKeys"
+              :load="onLazyLoad"
+              lazy
+              @check-change="onCheckChange"
+              class="cacp-pd-s"
+            >
+              <template #default="{ data }">
+                <template v-if="data.oguType === 'ORGANIZATION'">
+                  <el-icon class="cacp-mr-s" :size="16">
+                    <Grid />
+                  </el-icon>
+                  <span>{{ data.oguName }}</span>
+                </template>
+                <template v-else-if="data.oguType === 'USER'">
+                  <el-icon v-if="data.sideline" class="cacp-mr-s" :size="16" title="兼职">
+                    <User />
+                  </el-icon>
+                  <el-icon v-else class="cacp-mr-s" :size="16" title="主职">
+                    <UserFilled />
+                  </el-icon>
+                  <span>{{ data.oguName }}</span>
+                </template>
+              </template>
+            </el-tree>
+          </el-scrollbar>
+        </div>
+      </div>
+      <div class="selector-right">
+        <div class="selector-toolbar">
+          <span class="count">已选 {{ count }} 个</span><el-link type="primary" @click.prevent="onClear">清空</el-link>
+        </div>
+        <div class="selector-list">
+          <selector-list :dataList="selectedList" @on-delete="onRemove"></selector-list>
+        </div>
+      </div>
+    </div>
+    <template #footer>
+      <el-button @click="onClose">取消</el-button>
+      <el-button type="primary" @click="onSave">确定</el-button>
+    </template>
+  </el-dialog>
+</template>
+<script setup lang="ts">
+import { ref, computed } from 'vue'
+import type { ElTree } from 'element-plus'
+import type TreeNode from 'element-plus/es/components/tree/src/model/node'
+import { cloneDeep } from 'lodash-es'
+import * as apis from '@/apis/auth'
+import SelectorList from './SelectorList.vue'
+import type { BaseCacpOgu, CacpUser } from '@cacp/ui'
+import { SuccessResultCode } from '@cacp/ui'
+type TreeNodeDataType = BaseCacpOgu & { disabled: boolean; isLeaf: boolean }
+const selectorTreeRef = ref<InstanceType<typeof ElTree>>()
+const props = withDefaults(
+  defineProps<{
+    visible: boolean
+    multiple?: boolean
+    value?: Array<CacpUser>
+    rootPath?: string
+  }>(),
+  {
+    visible: false,
+    multiple: false,
+    value: () => [],
+    rootPath: ''
+  }
+)
+const emits = defineEmits<{
+  (e: 'on-close'): void
+  (e: 'on-save', value: Array<CacpUser>): void
+}>()
+const treeProps = {
+  label: 'oguName',
+  isLeaf: 'isLeaf'
+}
+const rootDeptPath = ref<string>(props.rootPath ?? '')
+const cachedList = ref<Array<CacpUser>>([])
+const selectedKeys = ref<Array<string>>([])
+const selectedList = computed<Array<CacpUser>>(() => {
+  return cachedList.value.filter((o) => selectedKeys.value.includes(o.fullPathName))
+})
+const count = computed<number>(() => {
+  return selectedKeys.value.length
+})
+
+async function onOpen() {
+  selectedKeys.value = props.value ? props.value.map((o) => o.fullPathName) : []
+  cachedList.value = props.value ? cloneDeep(props.value) : []
+}
+function onClose(): void {
+  emits('on-close')
+}
+function onSave(): void {
+  emits('on-save', selectedList.value)
+}
+async function onLazyLoad(
+  node: TreeNode,
+  resolve: (data: Array<TreeNodeDataType>) => void,
+  reject: () => void
+): Promise<void> {
+  if (node.level === 0) {
+    let root: TreeNodeDataType
+    if (props.rootPath) {
+      const result = await apis.getOrganizationByPath(props.rootPath)
+      if (result.code === SuccessResultCode && result.data) {
+        root = Object.assign({}, result.data, { disabled: true, isLeaf: false })
+        rootDeptPath.value = root.fullPathName
+        resolve([root])
+      } else {
+        if (reject) {
+          reject()
+        }
+      }
+    } else {
+      const result = await apis.getRootOrganization()
+      if (result.code === SuccessResultCode && result.data) {
+        root = Object.assign({}, result.data, { disabled: true, isLeaf: false })
+        rootDeptPath.value = root.fullPathName
+        resolve([root])
+      } else {
+        if (reject) {
+          reject()
+        }
+      }
+    }
+    return
+  }
+  const parent = node.data
+  const resOrg = await apis.getOrganizationListByParent(parent.fullPathName, true)
+  if (resOrg.code !== SuccessResultCode) {
+    reject()
+    return
+  }
+  const resUser = await apis.getUserListByParent(parent.fullPathName, true)
+  if (resUser.code !== SuccessResultCode) {
+    if (reject) {
+      reject()
+    }
+    return
+  }
+  const orgData = resOrg.data?.map((o) => {
+    return Object.assign({}, o, { disabled: true, isLeaf: false })
+  })
+  const userData = resUser.data?.map((o) => {
+    return Object.assign({}, o, { disabled: false, isLeaf: true })
+  })
+  const children: Array<TreeNodeDataType> = [...(orgData ?? []), ...(userData ?? [])]
+  resolve(children)
+}
+function onCheckChange(data: TreeNodeDataType, checked: boolean) {
+  if (checked) {
+    const user: CacpUser = Object.fromEntries(
+      Object.entries(data)
+        .filter(([k, v]: [string, any]) => k !== 'isLeaf' && k !== 'disabled')
+        .map(([k, v]: [string, any]) => {
+          return [k, v]
+        })
+    ) as CacpUser
+    handleCachedList([user])
+    if (!props.multiple) {
+      selectedKeys.value.splice(0, 1, data.fullPathName)
+    } else {
+      if (!selectedKeys.value.includes(data.fullPathName)) {
+        selectedKeys.value.push(data.fullPathName)
+      }
+    }
+  } else {
+    const idx = selectedKeys.value.indexOf(data.fullPathName)
+    if (idx > -1) {
+      selectedKeys.value.splice(idx, 1)
+    }
+  }
+  handleCheckStatus()
+}
+function onRemove(fullPathName: string): void {
+  const idx = selectedKeys.value.indexOf(fullPathName)
+  if (idx > -1) {
+    selectedKeys.value.splice(idx, 1)
+  }
+  handleCheckStatus()
+}
+
+function onClear(): void {
+  selectedKeys.value = []
+  handleCheckStatus()
+}
+
+function handleCachedList(list: Array<CacpUser>): void {
+  for (const org of list) {
+    const idx = cachedList.value.findIndex((o) => o.fullPathName === org.fullPathName)
+    if (idx > -1) {
+      cachedList.value.splice(idx, 1)
+    }
+    cachedList.value.push(org)
+  }
+}
+function handleCheckStatus(): void {
+  if (selectorTreeRef.value) {
+    selectorTreeRef.value.setCheckedKeys(selectedKeys.value, false)
+  }
+}
+async function querySearch(query: string, cb: any) {
+  if (query !== '') {
+    const res = await apis.queryUserListByCondition(rootDeptPath.value, query)
+    if (res.code === SuccessResultCode) {
+      cb(res.data ?? [])
+    }
+  }
+}
+
+function onSelect(data: CacpUser, checked: boolean) {
+  if (checked) {
+    handleCachedList([data])
+    if (!props.multiple) {
+      selectedKeys.value.splice(0, 1, data.fullPathName)
+    } else {
+      if (!selectedKeys.value.includes(data.fullPathName)) {
+        selectedKeys.value.push(data.fullPathName)
+      }
+    }
+  } else {
+    const idx = selectedKeys.value.indexOf(data.fullPathName)
+    if (idx > -1) {
+      selectedKeys.value.splice(idx, 1)
+    }
+  }
+  handleCheckStatus()
+}
+</script>
+<style lang="less" scoped>
+.selector {
+  display: flex;
+  align-items: flex-start;
+  border: 1px solid var(--el-border-color);
+  border-radius: var(--el-border-radius-base);
+  min-height: 260px;
+  max-height: 400px;
+  align-items: stretch;
+  &-left {
+    width: 280px;
+    border-right: 1px solid var(--el-border-color);
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+    :deep(.el-input-group__append),
+    :deep(.el-input__wrapper) {
+      box-shadow: none;
+      border-radius: 0;
+    }
+    // :deep(.el-input__wrapper) {
+    //   padding-right: 0;
+    // }
+    :deep(.el-input) {
+      height: 32px;
+      border-bottom: 1px solid var(--el-input-border-color);
+    }
+  }
+  &-search {
+    line-height: 32px;
+  }
+  &-tree {
+    flex: 1;
+    overflow: hidden;
+  }
+  &-middle {
+    width: 320px;
+    border-right: 1px solid var(--el-border-color);
+  }
+
+  &-right {
+    flex: 1;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+  }
+  &-list {
+    flex: 1;
+  }
+  &-toolbar {
+    line-height: 32px;
+    height: 32px;
+    padding: 0 var(--cacp-padding-space-s);
+    font-size: var(--font-size-s);
+    display: flex;
+    align-items: center;
+    border-bottom: 1px solid var(--el-border-color);
+    justify-content: space-between;
+  }
+}
+</style>

+ 62 - 0
basic-demo-web/src/components/dialog.vue

@@ -0,0 +1,62 @@
+<template>
+  <el-dialog
+    :model-value="visible"
+    title="新建人员"
+    @closed="onClose"
+    @open="onOpen"
+    :destroy-on-close="true"
+    :append-to-body="true"
+  >
+    <el-form :model="formUserData" ref="formRef" :rules="rules" label-position="left">
+      <el-form-item label="人员名称" prop="userName">
+        <el-input v-model="formUserData.userName"></el-input>
+      </el-form-item>
+      <el-form-item label="人员代码" prop="userCode">
+        <CacpAutocomplete v-model="formUserData.userCode"></CacpAutocomplete>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="onClose">取消</el-button>
+        <el-button type="primary" @click="onUserSubmit">确定</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+<script lang="ts" setup>
+import { cloneDeep } from 'lodash-es'
+import { ref } from 'vue'
+
+import type { DialogInfo } from './types'
+import { CacpAutocomplete } from '@cacp/ui'
+
+const props = defineProps<{
+  visible: boolean
+  info: DialogInfo
+}>()
+const emits = defineEmits<{
+  (e: 'on-close'): void
+  (e: 'on-save', info: DialogInfo): void
+}>()
+
+const formUserData = ref({ id: '', userName: '', userCode: '' })
+const formRef = ref()
+const rules = {
+  userName: [{ required: true, message: '请输入人员名称' }],
+  userCode: [{ required: true, message: '请输入人员代码' }]
+}
+
+async function onUserSubmit() {
+  if (await formRef.value?.validate()) {
+    emits('on-save', formUserData.value)
+  }
+}
+
+function onOpen() {
+  formUserData.value = cloneDeep(props.info)
+}
+
+function onClose() {
+  emits('on-close')
+}
+</script>

+ 40 - 0
basic-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
basic-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
basic-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
basic-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
+}

+ 5 - 0
basic-demo-web/src/components/types.ts

@@ -0,0 +1,5 @@
+export interface DialogInfo {
+  id: string
+  userName: string
+  userCode: string
+}

+ 18 - 0
basic-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:18001',
+  // SERVICE_API: 'http://10.200.73.47:15090',
+  SERVICE_PAGESIZE: 20,
+  SERVICE_TIMEOUT: 150000,
+  FRAME_API: 'http://10.200.73.47:15090',
+  NEED_USER_AUTHORITY: true,
+  AUTH_MODE: 'JWT',
+  CUSTOMS_CODE: '0000'
+}
+
+const $config = (window as any).$config as CacpConfig
+const { DEV } = import.meta.env
+export default DEV ? devConfig : $config

+ 8 - 0
basic-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
basic-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

+ 19 - 0
basic-demo-web/src/hooks/previous.ts

@@ -0,0 +1,19 @@
+import { cloneDeep } from 'lodash-es'
+import { type Reactive, ref, type Ref, toRef, watch } from 'vue'
+
+export function usePrevious<T>(value: T | Ref<T> | Reactive<T>) {
+  const currentValue = toRef(value)
+  const previousValue = ref<T>(cloneDeep(currentValue.value))
+
+  watch(
+    () => currentValue.value,
+    (_v, oldValue) => {
+      previousValue.value = oldValue
+    }
+  )
+
+  return {
+    currentValue,
+    previousValue
+  }
+}

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


+ 30 - 0
basic-demo-web/src/main.ts

@@ -0,0 +1,30 @@
+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)
+// 绑定message监听
+addEventListener()
+app.use(ElementPlus, {
+  locale: zhCn
+})
+app.use(CacpUI)
+
+app.mount('#app')

+ 9 - 0
basic-demo-web/src/plugins/icon.ts

@@ -0,0 +1,9 @@
+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
basic-demo-web/src/plugins/index.ts

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

+ 219 - 0
basic-demo-web/src/router/app-routers.ts

@@ -0,0 +1,219 @@
+import type { RouteRecordRaw } from 'vue-router'
+
+const routers: Array<RouteRecordRaw> = [
+  {
+    path: '/category-manage',
+    component: () => import('@/views/CategoryManage.vue'),
+    meta: {
+      title: '分类管理',
+      keepAlive: false
+    }
+  },
+  {
+    path: '/flow-manage',
+    name: 'FlowManage',
+    component: () => import('@/views/flow/FlowManage.vue'),
+    meta: {
+      title: '流程管理',
+      keepAlive: false
+    }
+  },
+  {
+    path: '/order-detail',
+    component: () => import('@/views/HomeDetail.vue'),
+    meta: {
+      title: '单据详情',
+      keepAlive: false
+    }
+  },
+  {
+    path: '/org-manage',
+    component: () => import('@/views/OrgManage.vue'),
+    meta: {
+      title: '组织管理',
+      keepAlive: false
+    }
+  },
+  {
+    path: 'typical-one',
+    name: 'typical-one',
+    component: () => import('@/views/TypicalOne.vue'),
+    meta: {
+      title: '典型页面一',
+      anonymous: true, // 访问是否需要登录
+      keepAlive: true
+    }
+  },
+  {
+    path: 'typical-two',
+    name: 'typical-two',
+    component: () => import('@/views/TypicalThree.vue'),
+    meta: {
+      title: '典型页面二',
+      anonymous: true, // 访问是否需要登录
+      keepAlive: true
+    }
+  },
+  {
+    path: 'typical-three',
+    name: 'typical-three',
+    component: () => import('@/views/TypicalEleven.vue'),
+    meta: {
+      title: '典型页面三',
+      anonymous: true, // 访问是否需要登录
+      keepAlive: true
+    }
+  },
+  {
+    path: 'typical-four',
+    name: 'typical-four',
+    component: () => import('@/views/TypicalFour.vue'),
+    meta: {
+      title: '典型页面四',
+      anonymous: true, // 访问是否需要登录
+      keepAlive: true
+    }
+  },
+  {
+    path: 'typical-five',
+    name: 'typical-five',
+    component: () => import('@/views/TypicalFive.vue'),
+    meta: {
+      title: '典型页面五',
+      anonymous: true, // 访问是否需要登录
+      keepAlive: true
+    }
+  },
+  {
+    path: 'typical-six',
+    name: 'typical-six',
+    component: () => import('@/views/TypicalNine.vue'),
+    meta: {
+      title: '典型页面六',
+      anonymous: true, // 访问是否需要登录
+      keepAlive: true
+    }
+  },
+  {
+    path: 'typical-seven',
+    name: 'typical-seven',
+    component: () => import('@/views/TypicalTen.vue'),
+    meta: {
+      title: '典型页面七',
+      anonymous: true, // 访问是否需要登录
+      keepAlive: true
+    }
+  },
+  {
+    path: 'typical-eight',
+    name: 'typical-eight',
+    component: () => import('@/views/TypicalSeven.vue'),
+    meta: {
+      title: '典型页面八',
+      anonymous: true, // 访问是否需要登录
+      keepAlive: true
+    }
+  },
+  {
+    path: 'typical-nine',
+    name: 'typical-nine',
+    component: () => import('@/views/TypicalSix.vue'),
+    meta: {
+      title: '典型页面九',
+      anonymous: true, // 访问是否需要登录
+      keepAlive: true
+    }
+  },
+  {
+    path: 'typical-ten',
+    name: 'typical-ten',
+    component: () => import('@/views/TypicalEight.vue'),
+    meta: {
+      title: '典型页面十',
+      anonymous: true, // 访问是否需要登录
+      keepAlive: true
+    }
+  },
+  {
+    path: 'typical-eleven',
+    name: 'typical-eleven',
+    component: () => import('@/views/typical-eleven/index.vue'),
+    meta: {
+      title: '典型页面十一',
+      anonymous: true, // 访问是否需要登录
+      keepAlive: true
+    }
+  },
+
+  {
+    path: 'edit-table',
+    name: 'edit-table',
+    component: () => import('@/views/EditTable.vue'),
+    meta: {
+      title: '可编辑表格',
+      anonymous: true, // 访问是否需要登录
+      keepAlive: true
+    }
+  },
+  {
+    path: 'holiday/list',
+    name: 'holidayList',
+    component: () => import('@/views/holiday/HolidayList.vue'),
+    meta: {
+      title: '节假日列表'
+    }
+  },
+  {
+    path: 'holiday/view',
+    name: 'holidayView',
+    component: () => import('@/views/holiday/HolidayView.vue'),
+    meta: {
+      title: '节假日详情'
+    }
+  },
+  {
+    path: 'counter/list',
+    name: 'counterList',
+    component: () => import('@/views/counter/CounterList.vue'),
+    meta: {
+      title: '业务计数器列表'
+    }
+  },
+  {
+    path: 'audit-log/list',
+    name: 'auditLogList',
+    component: () => import('@/views/audit-log/AuditLogList.vue'),
+    meta: {
+      title: '用户操作日志',
+      anonymous: true, // 访问是否需要登录
+      keepAlive: true // 未登录是否展示头部
+    }
+  },
+  {
+    path: 'heai/message',
+    name: 'HeaiMessageManage',
+    component: () => import('@/views/heai/HeaiMessageManage.vue')
+  },
+  {
+    path: 'heai/message-type',
+    name: 'HeaiMessageManageType',
+    component: () => import('@/views/heai/HeaiMessageTypeManage.vue')
+  },
+  {
+    path: 'form/form-type',
+    name: 'FormFormType',
+    component: () => import('@/views/form/FormTypeManage.vue')
+  },
+  {
+    path: 'form/dept-sn',
+    name: 'FormDeptSn',
+    component: () => import('@/views/form/DeptSnManage.vue')
+  },
+  {
+    path: 'form/workflow-relation',
+    name: 'FormWorkflowRelation',
+    component: () => import('@/views/form/WorkflowRelationManage.vue')
+  }
+]
+
+export default routers

+ 89 - 0
basic-demo-web/src/router/index.ts

@@ -0,0 +1,89 @@
+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,
+          name: 'home',
+          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) => {
+  // 当path为/error的时候,不去校验是否有用户信息
+  if (DEV || to.path === '/error') {
+    next()
+    return
+  }
+  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

+ 56 - 0
basic-demo-web/src/stores/category.ts

@@ -0,0 +1,56 @@
+import { defineStore } from 'pinia'
+
+import { getCategoryList, insertCategory, updateCategory, deleteCategory } from '@/apis/order'
+import type { OrderCategoryInfo } from '@/types/order'
+import { SuccessResultCode, type Result } from '@cacp/ui'
+
+interface CoreState {
+  isInited: boolean
+  categoryInfoList: Array<OrderCategoryInfo>
+}
+
+export const useCategoryStore = defineStore('category', {
+  state: (): CoreState => ({
+    isInited: false,
+    categoryInfoList: []
+  }),
+  actions: {
+    async initCategory() {
+      if (!this.isInited) {
+        const res: Result<Array<OrderCategoryInfo>> = await getCategoryList()
+        if (res.code === SuccessResultCode) {
+          this.$patch({
+            isInited: true,
+            categoryInfoList: res.data
+          })
+        }
+      }
+    },
+    async insertCategory(category: OrderCategoryInfo) {
+      // 插入
+      const res = await insertCategory({ ...category })
+      if (res.code == SuccessResultCode) {
+        this.categoryInfoList.push({ ...category })
+      }
+    },
+    async updateCategory(category: OrderCategoryInfo) {
+      // 更新
+      const res = await updateCategory({ ...category })
+      if (res.code == SuccessResultCode) {
+        const index = this.categoryInfoList.findIndex(
+          (item: OrderCategoryInfo) => category.categoryCode === item.categoryCode
+        )
+        if (index > -1) {
+          this.categoryInfoList.splice(index, 1, { ...category })
+        }
+      }
+    },
+    deleteCategory(categoryCode: string) {
+      const index = this.categoryInfoList.findIndex((item: OrderCategoryInfo) => categoryCode === item.categoryCode)
+      if (index > -1) {
+        this.categoryInfoList.splice(index, 1)
+        deleteCategory(categoryCode)
+      }
+    }
+  }
+})

+ 53 - 0
basic-demo-web/src/stores/core.ts

@@ -0,0 +1,53 @@
+//获取用户信息与权限
+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'
+
+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()
+        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
+      })
+    }
+  }
+})

+ 5 - 0
basic-demo-web/src/stores/index.ts

@@ -0,0 +1,5 @@
+export * from './core'
+export * from './category'
+export * from './level'
+
+export { default as pinia } from './pinia'

+ 26 - 0
basic-demo-web/src/stores/level.ts

@@ -0,0 +1,26 @@
+import { getLevelList } from '@/apis/order'
+import { SuccessResultCode, type Result, type TypeDescriptor } from '@cacp/ui'
+import { defineStore } from 'pinia'
+interface CoreState {
+  isInited: boolean
+  levelList: Array<TypeDescriptor>
+}
+export const useLevelStore = defineStore('level', {
+  state: (): CoreState => ({
+    isInited: false,
+    levelList: []
+  }),
+  actions: {
+    async initLevel() {
+      if (!this.isInited) {
+        const res: Result<Array<TypeDescriptor>> = await getLevelList()
+        if (res.code === SuccessResultCode) {
+          this.$patch({
+            isInited: true,
+            levelList: res.data
+          })
+        }
+      }
+    }
+  }
+})

+ 20 - 0
basic-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

+ 32 - 0
basic-demo-web/src/types/accessory.ts

@@ -0,0 +1,32 @@
+export type AccessoryDirection = 'vertical' | 'horizontal'
+
+export interface Accessory {
+  accessoryId: string
+  appCode: string
+  category: string
+  bizId: string
+  bizTag: string
+  relId: string
+  fileName: string
+  fileMime: string
+  persisted: boolean
+  storagePath: string
+  storageFile: string
+  fileSize: number
+  fileType: string
+  sortOrder: number
+  userId: string
+  parentId: string
+  userFullPath: string
+  recCreateTime: string
+  deleted: boolean
+}
+
+export interface UploadData {
+  category: string
+  fileName: string
+  bizId: string
+  bizTag: string
+  relId: string
+  persisted: boolean
+}

+ 75 - 0
basic-demo-web/src/types/auditLog.ts

@@ -0,0 +1,75 @@
+export interface AuditLogInfo {
+  /** id */
+  logId: string
+
+  /** 部门ParentId */
+  parentId: string
+
+  /** 用户GUID */
+  userId: string
+
+  /** 用户全路径 */
+  fullPathName: string
+
+  /** 用户名 */
+  userName: string
+
+  /** 关区 */
+  customsCode: string
+
+  /** 客户端IP */
+  ipAddress: string
+
+  /** 视角 */
+  viewCode: string
+
+  /** 应用编码 */
+  appCode: string
+
+  /** 应用特征码 */
+  appFeatureCode: string
+
+  /** 业务系统日志Id */
+  orgId: string
+
+  /** 业务ID */
+  bizId: string
+
+  /** 环节ID */
+  stepId: string
+
+  /** 操作时间 */
+  operTime: number
+
+  /** 操作对象表及字段名称 */
+  operDbItems: string
+
+  /** 操作类型编码 */
+  operType: string
+
+  /** 操作内容 */
+  operContent: string
+
+  /** 操作数据量 */
+  operDataQty: string
+
+  /** 操作描述 */
+  operDesc: string
+
+  /** 操作结果 */
+  operResult: string
+
+  /** sm3加密数据 */
+  hashCode: string
+
+  /** 创建日期 */
+  recCreateTime: number
+}
+
+export interface AuditLogQuery {
+  begDate: string
+  endDate: string
+  keyword: string
+  pageSize: number
+  pageIndex: number
+}

+ 11 - 0
basic-demo-web/src/types/counter.ts

@@ -0,0 +1,11 @@
+export interface CacpCounter {
+  counterId: string
+  appCode: string
+  counterPerfix: string
+  counterValue: number
+}
+
+export interface BatchCounterResult {
+  beginValue: number
+  endValue: number
+}

+ 8 - 0
basic-demo-web/src/types/editTable.ts

@@ -0,0 +1,8 @@
+export interface EditTableUser{
+    id?: string
+    userName: string
+    userCode: string
+    address: string
+    phone: string
+    isEdit?: boolean
+}

+ 7 - 0
basic-demo-web/src/types/eight.ts

@@ -0,0 +1,7 @@
+export interface TypicalEight {
+  customsCode: string;
+  startDate: string;
+  endDate: string;
+  person: string;
+  flag: number;
+}

+ 34 - 0
basic-demo-web/src/types/eleven.ts

@@ -0,0 +1,34 @@
+export interface TypicalElevenInfo {
+  id: string
+  reason: string
+  email: string
+  taxInvoice: string
+  source: string
+  payer: string
+  total: number
+  dutiable: number
+  amount: number
+  date: string
+  printing: string
+  printDate: string
+  loading: boolean
+  isExpand: boolean
+  children: TypicalElevenThingInfo[]
+}
+
+export interface TypicalElevenThingInfo {
+  id: string
+  name: string
+  date: string
+  total: number
+  code: string
+  type: string
+  price: string
+  unit: string
+  taxRate: number
+  tax: number
+  thingName: string
+  thingTotal: string
+  thingPrice: number
+  thingCode: string
+}

+ 37 - 0
basic-demo-web/src/types/four.ts

@@ -0,0 +1,37 @@
+export interface TypicalFourUserInfo {
+  employee: string;
+  name: string;
+  sex: string;
+  area: string;
+  section: string;
+  birthday: string;
+  profession: string;
+  educational: string;
+  universities: string;
+  aptityde: string;
+  beGoodAt: string;
+  introduction: string;
+}
+
+export interface TypicalFourTable {
+  type: string;
+  product: string;
+  profession: string;
+  name: string;
+  intro: string;
+  createName: string;
+  createTime: string;
+  modification: string;
+  modifiTime: string;
+}
+
+export interface TypicalFourTag {
+  name: string;
+  type: "success" | "warning" | "info" | "primary" | "danger";
+}
+
+export interface TypicalFourTags {
+  product: TypicalFourTag[];
+  profession: TypicalFourTag[];
+  expert: TypicalFourTag[];
+}

+ 8 - 0
basic-demo-web/src/types/holiday.ts

@@ -0,0 +1,8 @@
+export type HolidayFlag = 'FESTIVAL' | 'WEEKEND'
+
+export interface CacpHoliday {
+  holidayDate: string
+  holidayName: string
+  holidayFlag: HolidayFlag
+  customsCode: string
+}

+ 5 - 0
basic-demo-web/src/types/list.ts

@@ -0,0 +1,5 @@
+export interface ListFormData {
+  keyword: string
+  beginDate: string
+  endDate: string
+}

+ 27 - 0
basic-demo-web/src/types/nine.ts

@@ -0,0 +1,27 @@
+export interface TypicalNineUser {
+  id: string;
+  shipName: string;
+  shipCode: string;
+  userName: string;
+  userCode: string;
+}
+
+export interface TypicalNineCode {
+  id: string;
+  shipName: string;
+  shipCode: string;
+  areaCode: string;
+}
+
+export interface TypicalNineUserParams {
+  userName: string;
+  userCode: string;
+  shipId: string;
+}
+
+export interface TypicalNineCodeParams {
+  id?: string;
+  shipName: string;
+  shipCode: string;
+  areaCode: string;
+}

+ 22 - 0
basic-demo-web/src/types/one.ts

@@ -0,0 +1,22 @@
+export interface TypicalOne {
+  id: string
+  status: string
+  areaCode: string
+  code: string
+  funcType: string
+  createTime: Date
+  lastModified: Date
+  operator: string
+}
+export interface TypicalOneFormData {
+  code: string
+  areaCode: string
+  [key: string]: string
+}
+
+export interface TypicalOneQuery {
+  code: string
+  areaCode: string
+  pageIndex: number
+  pageSize: number
+}

+ 30 - 0
basic-demo-web/src/types/order.ts

@@ -0,0 +1,30 @@
+import type { Accessory } from '@cacp/ui'
+export enum OrderLevel {
+  HIGH = 'HIGH',
+  NORMAL = 'NORMAL',
+  LOW = 'LOW'
+}
+
+export interface OrderInfo {
+  orderId?: string
+  parentId?: string
+  userId?: string
+  userName?: string
+  categoryCode: string
+  orderAmount: number
+  orderLevel?: string
+  createTime?: string
+  accessoryList?: Array<Accessory>
+}
+export interface OrderCategoryInfo {
+  categoryCode: string
+  categoryName: string
+}
+
+export interface OrderQueryInfo {
+  keyword: string
+  beginDate: string
+  endDate: string
+  pageIndex: number
+  pageSize: number
+}

+ 20 - 0
basic-demo-web/src/types/seven.ts

@@ -0,0 +1,20 @@
+export interface TypicalSevenTable {
+  id: string
+  jobTitle: string
+  jobCode: string
+  personName: string
+  personCode: string
+  lastModified: string
+}
+
+export interface TypicalSevenQuery {
+  jobCode: string
+  areaCode: string
+  pageIndex: number
+  pageSize: number
+}
+
+export interface TypicalSevenDeleteQuery {
+  id: string[]
+  areaCode: string
+}

+ 24 - 0
basic-demo-web/src/types/six.ts

@@ -0,0 +1,24 @@
+export interface TypicalSixInfo {
+  charts: TypicalSixChartsInfo[];
+  dataTable: TypicalSixTableInfo[];
+}
+
+export interface TypicalSixChartsInfo {
+  name: string;
+  value: number[];
+}
+
+export interface TypicalSixTableInfo {
+  id: string;
+  name: string;
+  profession: string;
+  type: string;
+}
+
+export interface TypicalSixQueryParams {
+  firm: string;
+  design: string;
+  startDate: string;
+  endDate: string;
+  tabIndex?: string;
+}

+ 11 - 0
basic-demo-web/src/types/ten.ts

@@ -0,0 +1,11 @@
+export interface TypicalTenInfo {
+  name: string;
+  check: boolean;
+  color: string;
+  children: {
+    name: string;
+    check: boolean;
+    num: number;
+    icon: string;
+  }[];
+}

+ 35 - 0
basic-demo-web/src/types/three.ts

@@ -0,0 +1,35 @@
+type Increase<T extends number, A extends number[] = []> = A['length'] extends T
+  ? A
+  : Increase<T, [...A, [0, ...A]['length']]>
+type InfoFiledsLength = Increase<55>
+
+export type TypicalThreeInfo = {
+  [key in `filed${InfoFiledsLength[number]}`]: string
+}
+
+export interface TypicalThreeList {
+  id: string
+  index: number
+  num: string
+  name: string
+  num2: string
+  num3: string
+  num4: string
+  productName: string
+  specifications: string
+  price: string
+  unit: string
+  statistics: string
+  money: string
+  total: string
+  country: string
+  mcountry: string
+  way: string
+  sprice: string
+  msprice: string
+}
+
+export interface TypicalThree {
+  info: TypicalThreeInfo
+  list: TypicalThreeList[]
+}

+ 10 - 0
basic-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
basic-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
basic-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

+ 114 - 0
basic-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

+ 13 - 0
basic-demo-web/src/utils/security.ts

@@ -0,0 +1,13 @@
+import CryptoJS from 'crypto-js'
+
+const securityKey = 'aubVseoLz3BkkeXKCXNzMg=='
+
+export function serialize(obj: object): string {
+  const json = JSON.stringify(obj)
+  return CryptoJS.AES.encrypt(json, securityKey).toString()
+}
+
+export function deserialize(input: string): object {
+  const json = CryptoJS.AES.decrypt(input, securityKey).toString(CryptoJS.enc.Utf8)
+  return JSON.parse(json)
+}

+ 110 - 0
basic-demo-web/src/views/CategoryManage.vue

@@ -0,0 +1,110 @@
+<template>
+  <cacp-search-layout>
+    <cacp-complex-table :actions="actions" :data="categoryInfoList" ref="table" :actionsWidth="200">
+      <el-table-column label="分类编码" width="200" prop="categoryCode"></el-table-column>
+      <el-table-column label="分类名称" min-width="282" prop="categoryName"></el-table-column>
+    </cacp-complex-table>
+  </cacp-search-layout>
+  <cacp-dialog v-model="dialogVisible" title="新建分类" :resizable="false" width="“360”" @closed="onDialogClosed">
+    <el-form :model="formData" ref="formRef" :rules="formRule" label-position="left">
+      <el-form-item label="分类名称" prop="categoryName"
+        ><el-input v-model="formData.categoryName"></el-input
+      ></el-form-item>
+      <el-form-item label="分类编码" prop="categoryCode"
+        ><el-input :disabled="isEdit" v-model="formData.categoryCode"></el-input
+      ></el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="onCancel">取消</el-button>
+      <el-button type="primary" @click="onSubmit">确定</el-button>
+    </template>
+  </cacp-dialog>
+</template>
+<script lang="ts" setup>
+import { reactive, ref, onBeforeMount, nextTick } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import type { TableAction } from '@cacp/ui'
+import { useCategoryStore } from '@/stores'
+import type { OrderCategoryInfo } from '@/types/order'
+import { storeToRefs } from 'pinia'
+
+const formRef = ref<FormInstance>()
+const formData = reactive<OrderCategoryInfo>({
+  categoryName: '',
+  categoryCode: ''
+})
+const formRule = reactive<FormRules<OrderCategoryInfo>>({
+  categoryName: [{ required: true, message: '请输入分类名称' }],
+  categoryCode: [{ required: true, message: '请输入分类编码' }]
+})
+const dialogVisible = ref(false)
+const isEdit = ref(false)
+const actions = <Array<TableAction>>[
+  {
+    key: '1',
+    text: '新建',
+    onclick: onCreate,
+    limit: 'none',
+    position: 'left',
+    type: 'primary'
+  },
+  {
+    key: '2',
+    text: '编辑',
+    onclick: onEdit,
+    limit: 'more',
+    type: 'primary'
+  },
+  {
+    key: '3',
+    text: '删除',
+    onclick: onDelete,
+    limit: 'none',
+    type: 'primary',
+    confirm: '您确定要删除吗?'
+  }
+]
+
+const stores = useCategoryStore()
+const { initCategory, insertCategory, updateCategory, deleteCategory } = stores
+const { categoryInfoList } = storeToRefs(stores)
+function onEdit(row: OrderCategoryInfo) {
+  isEdit.value = true
+  dialogVisible.value = true
+  nextTick(() => {
+    formData.categoryCode = row.categoryCode
+    formData.categoryName = row.categoryName
+  })
+}
+function onDelete(row: OrderCategoryInfo) {
+  deleteCategory(row.categoryCode)
+}
+function onCreate() {
+  isEdit.value = false
+  dialogVisible.value = true
+  table.value.innerTableRef.doLayout()
+}
+function onSubmit() {
+  formRef.value.validate(async (valid: boolean) => {
+    if (valid) {
+      if (isEdit.value) {
+        await updateCategory(formData)
+      } else {
+        await insertCategory(formData)
+      }
+      dialogVisible.value = false
+    }
+  })
+}
+function onCancel() {
+  dialogVisible.value = false
+}
+function onDialogClosed() {
+  formRef.value.resetFields()
+}
+const table = ref()
+onBeforeMount(() => {
+  initCategory()
+})
+</script>
+<style></style>

Деякі файли не було показано, через те що забагато файлів було змінено