ydmyzx il y a 5 mois
Parent
commit
64f5c97bd9
32 fichiers modifiés avec 3264 ajouts et 0 suppressions
  1. 11 0
      .prettierignore
  2. 6 0
      .stylelintignore
  3. 61 0
      src/App.vue
  4. BIN
      src/assets/imgs/diy/app-nav-bar-mp.png
  5. 0 0
      src/assets/svgs/403.svg
  6. 0 0
      src/assets/svgs/404.svg
  7. 0 0
      src/assets/svgs/500.svg
  8. 1 0
      src/assets/svgs/pay/icon/alipay_app.svg
  9. 1 0
      src/assets/svgs/pay/icon/alipay_bar.svg
  10. 1 0
      src/assets/svgs/pay/icon/alipay_pc.svg
  11. 2 0
      src/assets/svgs/pay/icon/alipay_qr.svg
  12. 1 0
      src/assets/svgs/pay/icon/alipay_wap.svg
  13. 207 0
      src/components/AppLinkInput/AppLinkSelectDialog.vue
  14. 237 0
      src/components/SimpleProcessDesigner/src/addNode.vue
  15. 1004 0
      src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json
  16. 83 0
      src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/activiti/activitiExtension.js
  17. 277 0
      src/store/modules/app.ts
  18. 8 0
      src/views/Error/403.vue
  19. 7 0
      src/views/Error/500.vue
  20. 132 0
      src/views/ai/model/apiKey/ApiKeyForm.vue
  21. 169 0
      src/views/bpm/fund/apply/apply-create.vue
  22. 124 0
      src/views/erp/finance/account/AccountForm.vue
  23. 79 0
      src/views/infra/apiAccessLog/ApiAccessLogDetail.vue
  24. 81 0
      src/views/infra/apiErrorLog/ApiErrorLogDetail.vue
  25. BIN
      src/views/mall/promotion/kefu/components/asserts/a.png
  26. BIN
      src/views/mall/promotion/kefu/components/asserts/aini.png
  27. BIN
      src/views/mall/promotion/kefu/components/asserts/aixin.png
  28. 70 0
      src/views/mall/trade/afterSale/form/AfterSaleDisagreeForm.vue
  29. 160 0
      src/views/mp/account/AccountForm.vue
  30. 130 0
      src/views/pay/app/components/AppForm.vue
  31. 326 0
      src/views/pay/app/components/channel/AlipayChannelForm.vue
  32. 86 0
      src/views/system/mail/account/account.data.ts

+ 11 - 0
.prettierignore

@@ -0,0 +1,11 @@
+/node_modules/**
+/dist/
+/dist*
+/public/*
+/docs/*
+/vite.config.ts
+/src/types/env.d.ts
+/src/types/auto-components.d.ts
+/src/types/auto-imports.d.ts
+/docs/**/*
+CHANGELOG

+ 6 - 0
.stylelintignore

@@ -0,0 +1,6 @@
+/dist/*
+/public/*
+public/*
+/dist*
+/src/types/env.d.ts
+/docs/**/*

+ 61 - 0
src/App.vue

@@ -0,0 +1,61 @@
+<script lang="ts" setup>
+import { isDark } from '@/utils/is'
+import { useAppStore } from '@/store/modules/app'
+import { useDesign } from '@/hooks/web/useDesign'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import routerSearch from '@/components/RouterSearch/index.vue'
+
+defineOptions({ name: 'APP' })
+
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('app')
+const appStore = useAppStore()
+const currentSize = computed(() => appStore.getCurrentSize)
+const greyMode = computed(() => appStore.getGreyMode)
+const { wsCache } = useCache()
+
+// 根据浏览器当前主题设置系统主题色
+const setDefaultTheme = () => {
+  let isDarkTheme = wsCache.get(CACHE_KEY.IS_DARK)
+  if (isDarkTheme === null) {
+    isDarkTheme = isDark()
+  }
+  appStore.setIsDark(isDarkTheme)
+}
+setDefaultTheme()
+</script>
+<template>
+  <ConfigGlobal :size="currentSize">
+    <RouterView :class="greyMode ? `${prefixCls}-grey-mode` : ''" />
+    <routerSearch />
+  </ConfigGlobal>
+</template>
+<style lang="scss">
+$prefix-cls: #{$namespace}-app;
+
+.size {
+  width: 100%;
+  height: 100%;
+}
+
+html,
+body {
+  @extend .size;
+
+  padding: 0 !important;
+  margin: 0;
+  overflow: hidden;
+
+  #app {
+    @extend .size;
+  }
+}
+
+.#{$prefix-cls}-grey-mode {
+  filter: grayscale(100%);
+}
+
+.scrollbar__view {
+  height: 99%!important;
+}
+</style>

BIN
src/assets/imgs/diy/app-nav-bar-mp.png


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
src/assets/svgs/403.svg


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
src/assets/svgs/404.svg


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
src/assets/svgs/500.svg


+ 1 - 0
src/assets/svgs/pay/icon/alipay_app.svg

@@ -0,0 +1 @@
+<svg t="1627279997305" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11904" width="40" height="40"><path d="M938.7008 669.525333L938.7008 249.412267c0-90.555733-73.5232-164.078933-164.1472-164.078933L249.378133 85.333333c-90.555733 0-164.078933 73.48906699-164.078933 164.078933l0 525.2096c0 90.555733 73.454933 164.078933 164.07893301 164.078933l525.20959999 0c80.725333 0 147.8656-58.368 161.553067-135.099733-43.52-18.8416-232.106667-100.283733-330.376533-147.182933-74.786133 90.589867-153.088 144.930133-271.121067 144.930133s-196.81279999-72.704-187.357867-161.655467c6.2464-58.402133 46.2848-153.9072 220.296533-137.5232 91.682133 8.6016 133.666133 25.736533 208.418133 50.414933 19.3536-35.4304 35.4304-74.513067 47.616-116.0192L292.0448 436.565333l0-32.8704 164.0448 0 0-58.9824L256 344.712533l1e-8-36.181333 200.12373299 0L456.123733 223.3344c0 0 1.809067-13.312 16.520533-13.31200001l82.056533 1e-8 0 98.474667 213.333333 0 0 36.181333-213.333333 1e-8 0 58.98239999 174.045867 0c-16.00853301 65.1264-40.277333 124.962133-70.690133 177.220267C708.608 599.176533 938.7008 669.525333 938.7008 669.525333L938.7008 669.525333 938.7008 669.525333 938.7008 669.525333zM321.57013299 744.994133c-124.7232 0-144.452267-78.7456-137.83039999-111.65013299 6.5536-32.733867 42.666667-75.502933 112.0256-75.50293301 79.6672 0 151.04 20.445867 236.714667 62.088533C472.302933 698.333867 398.370133 744.994133 321.57013299 744.994133L321.57013299 744.994133 321.57013299 744.994133zM321.57013299 744.994133" fill="#1296db" p-id="11905"></path></svg>

Fichier diff supprimé car celui-ci est trop grand
+ 1 - 0
src/assets/svgs/pay/icon/alipay_bar.svg


+ 1 - 0
src/assets/svgs/pay/icon/alipay_pc.svg

@@ -0,0 +1 @@
+<svg t="1627279878333" class="icon" viewBox="0 0 1285 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8535" width="40" height="40"><path d="M1141.76 855.04h-286.72c0 40.96 30.72 71.68 71.68 71.68h107.52c20.48 0 35.84 15.36 35.84 35.84s-15.36 35.84-35.84 35.84h-783.36c-20.48 0-35.84-15.36-35.84-35.84s15.36-35.84 35.84-35.84h107.52c40.96 0 71.68-30.72 71.68-71.68h-286.72c-76.8 0-143.36-61.44-143.36-143.36v-568.32c0-76.8 61.44-143.36 143.36-143.36h993.28c76.8 0 143.36 61.44 143.36 143.36v568.32c5.12 76.8-56.32 143.36-138.24 143.36z m71.68-711.68c0-40.96-30.72-71.68-71.68-71.68h-993.28c-40.96 0-71.68 30.72-71.68 71.68v568.32c0 40.96 30.72 71.68 71.68 71.68h993.28c40.96 0 71.68-30.72 71.68-71.68v-568.32z m-143.36 568.32h-855.04c-40.96 0-71.68-30.72-71.68-71.68v-424.96c0-40.96 30.72-71.68 71.68-71.68h855.04c40.96 0 71.68 30.72 71.68 71.68v424.96c0 40.96-30.72 71.68-71.68 71.68z" p-id="8536" fill="#1977FD"></path></svg>

+ 2 - 0
src/assets/svgs/pay/icon/alipay_qr.svg

@@ -0,0 +1,2 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1627279238245" class="icon" viewBox="0 0 1115 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4112" width="43.5546875" height="40" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); }
+</style></defs><path d="M751.388 68.267a34.133 34.133 0 0 1 0-68.267h227.556a91.022 91.022 0 0 1 91.022 91.022v227.556a34.133 34.133 0 1 1-68.266 0V91.022a22.756 22.756 0 0 0-22.756-22.755H751.388M1001.7 705.422a34.133 34.133 0 0 1 68.266 0v227.556A91.022 91.022 0 0 1 978.944 1024H748.885a34.133 34.133 0 0 1 0-68.267H978.49a22.756 22.756 0 0 0 22.755-22.755V705.422M364.09 955.733a34.133 34.133 0 1 1 0 68.267H136.533a91.022 91.022 0 0 1-91.022-91.022V705.422a34.133 34.133 0 0 1 68.267 0v227.556a22.756 22.756 0 0 0 22.755 22.755H364.09M113.778 318.578a34.133 34.133 0 1 1-68.267 0V91.022A91.022 91.022 0 0 1 136.533 0H364.09a34.133 34.133 0 0 1 0 68.267H136.533a22.756 22.756 0 0 0-22.755 22.755v227.556M34.133 477.867a34.133 34.133 0 0 0 0 68.266h168.619v-68.266z m1046.756 0H912.27v68.266h168.619a34.133 34.133 0 0 0 0-68.266zM202.752 157.24h709.746v320.627H202.752z m0 388.893h709.746V866.76H202.752z" fill="#1977FD" p-id="4113"></path></svg>

+ 1 - 0
src/assets/svgs/pay/icon/alipay_wap.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1645964864184" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8460" xmlns:xlink="http://www.w3.org/1999/xlink" width="40" height="40"><defs><style type="text/css"></style></defs><path d="M768.3 0 255.7 0c-70.8 0-128.1 57.4-128.1 128.1l0 767.8c0 70.8 57.4 128.1 128.1 128.1L512 1024l256.3 0c70.8 0 128.1-57.4 128.1-128.1L896.4 128.1C896.4 57.3 839 0 768.3 0zM383.9 96.1c0-17.7 14.3-32 32-32l192.2 0c17.7 0 32 14.3 32 32l0 0c0 17.7-14.3 32-32 32L415.9 128.1C398.2 128.1 383.9 113.8 383.9 96.1L383.9 96.1zM512 959.9 512 959.9 512 959.9c-35.4 0-64.1-28.8-64.1-64.1 0-35.4 28.7-64.1 64.1-64.1l0 0 0 0c35.4 0 64.1 28.7 64.1 64.1C576.1 931.1 547.4 959.9 512 959.9zM832.3 755.6c0 6.7-5.4 12.2-12.2 12.2L203.9 767.8c-6.7 0-12.2-5.4-12.2-12.2L191.7 204.3c0-6.7 5.4-12.2 12.2-12.2l616.3 0c6.7 0 12.2 5.4 12.2 12.2L832.4 755.6z" p-id="8461" fill="#1977FD"></path></svg>

+ 207 - 0
src/components/AppLinkInput/AppLinkSelectDialog.vue

@@ -0,0 +1,207 @@
+<template>
+  <Dialog v-model="dialogVisible" title="选择链接" width="65%">
+    <div class="h-500px flex gap-8px">
+      <!-- 左侧分组列表 -->
+      <el-scrollbar wrap-class="h-full" ref="groupScrollbar" view-class="flex flex-col">
+        <el-button
+          v-for="(group, groupIndex) in APP_LINK_GROUP_LIST"
+          :key="groupIndex"
+          :class="[
+            'm-r-16px m-l-0px! justify-start! w-90px',
+            { active: activeGroup === group.name }
+          ]"
+          ref="groupBtnRefs"
+          :text="activeGroup !== group.name"
+          :type="activeGroup === group.name ? 'primary' : 'default'"
+          @click="handleGroupSelected(group.name)"
+        >
+          {{ group.name }}
+        </el-button>
+      </el-scrollbar>
+      <!-- 右侧链接列表 -->
+      <el-scrollbar class="h-full flex-1" @scroll="handleScroll" ref="linkScrollbar">
+        <div v-for="(group, groupIndex) in APP_LINK_GROUP_LIST" :key="groupIndex">
+          <!-- 分组标题 -->
+          <div class="font-bold" ref="groupTitleRefs">{{ group.name }}</div>
+          <!-- 链接列表 -->
+          <el-tooltip
+            v-for="(appLink, appLinkIndex) in group.links"
+            :key="appLinkIndex"
+            :content="appLink.path"
+            placement="bottom"
+            :show-after="300"
+          >
+            <el-button
+              class="m-b-8px m-r-8px m-l-0px!"
+              :type="isSameLink(appLink.path, activeAppLink.path) ? 'primary' : 'default'"
+              @click="handleAppLinkSelected(appLink)"
+            >
+              {{ appLink.name }}
+            </el-button>
+          </el-tooltip>
+        </div>
+      </el-scrollbar>
+    </div>
+    <!-- 底部对话框操作按钮 -->
+    <template #footer>
+      <el-button type="primary" @click="handleSubmit">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+  <Dialog v-model="detailSelectDialog.visible" title="" width="50%">
+    <el-form class="min-h-200px">
+      <el-form-item
+        label="选择分类"
+        v-if="detailSelectDialog.type === APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST"
+      >
+        <ProductCategorySelect
+          v-model="detailSelectDialog.id"
+          :parent-id="0"
+          @update:model-value="handleProductCategorySelected"
+        />
+      </el-form-item>
+    </el-form>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import { APP_LINK_GROUP_LIST, APP_LINK_TYPE_ENUM, AppLink } from './data'
+import { ButtonInstance, ScrollbarInstance } from 'element-plus'
+import { split } from 'lodash-es'
+import ProductCategorySelect from '@/views/mall/product/category/components/ProductCategorySelect.vue'
+import { getUrlNumberValue } from '@/utils'
+
+// APP 链接选择弹框
+defineOptions({ name: 'AppLinkSelectDialog' })
+// 选中的分组,默认选中第一个
+const activeGroup = ref(APP_LINK_GROUP_LIST[0].name)
+// 选中的 APP 链接
+const activeAppLink = ref({} as AppLink)
+
+/** 打开弹窗 */
+const dialogVisible = ref(false)
+const open = (link: string) => {
+  activeAppLink.value.path = link
+  dialogVisible.value = true
+
+  // 滚动到当前的链接
+  const group = APP_LINK_GROUP_LIST.find((group) =>
+    group.links.some((linkItem) => {
+      const sameLink = isSameLink(linkItem.path, link)
+      if (sameLink) {
+        activeAppLink.value = { ...linkItem, path: link }
+      }
+      return sameLink
+    })
+  )
+  if (group) {
+    // 使用 nextTick 的原因:可能 Dom 还没生成,导致滚动失败
+    nextTick(() => handleGroupSelected(group.name))
+  }
+}
+defineExpose({ open })
+
+// 处理 APP 链接选中
+const handleAppLinkSelected = (appLink: AppLink) => {
+  if (!isSameLink(appLink.path, activeAppLink.value.path)) {
+    activeAppLink.value = appLink
+  }
+  switch (appLink.type) {
+    case APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST:
+      detailSelectDialog.value.visible = true
+      detailSelectDialog.value.type = appLink.type
+      // 返显
+      detailSelectDialog.value.id =
+        getUrlNumberValue('id', 'http://127.0.0.1' + activeAppLink.value.path) || undefined
+      break
+    default:
+      break
+  }
+}
+
+// 处理绑定值更新
+const emit = defineEmits<{
+  change: [link: string]
+  appLinkChange: [appLink: AppLink]
+}>()
+const handleSubmit = () => {
+  dialogVisible.value = false
+  emit('change', activeAppLink.value.path)
+  emit('appLinkChange', activeAppLink.value)
+}
+
+// 分组标题引用列表
+const groupTitleRefs = ref<HTMLInputElement[]>([])
+/**
+ * 处理右侧链接列表滚动
+ * @param scrollTop 滚动条的位置
+ */
+const handleScroll = ({ scrollTop }: { scrollTop: number }) => {
+  const titleEl = groupTitleRefs.value.find((titleEl: HTMLInputElement) => {
+    // 获取标题的位置信息
+    const { offsetHeight, offsetTop } = titleEl
+    // 判断标题是否在可视范围内
+    return scrollTop >= offsetTop && scrollTop < offsetTop + offsetHeight
+  })
+  // 只需处理一次
+  if (titleEl && activeGroup.value !== titleEl.textContent) {
+    activeGroup.value = titleEl.textContent || ''
+    // 同步左侧的滚动条位置
+    scrollToGroupBtn(activeGroup.value)
+  }
+}
+
+// 右侧滚动条
+const linkScrollbar = ref<ScrollbarInstance>()
+// 处理分组选中
+const handleGroupSelected = (group: string) => {
+  activeGroup.value = group
+  const titleRef = groupTitleRefs.value.find((item: HTMLInputElement) => item.textContent === group)
+  if (titleRef) {
+    // 滚动分组标题
+    linkScrollbar.value?.setScrollTop(titleRef.offsetTop)
+  }
+}
+
+// 分组滚动条
+const groupScrollbar = ref<ScrollbarInstance>()
+// 分组引用列表
+const groupBtnRefs = ref<ButtonInstance[]>([])
+// 自动滚动分组按钮,确保分组按钮保持在可视区域内
+const scrollToGroupBtn = (group: string) => {
+  const groupBtn = groupBtnRefs.value
+    .map((btn: ButtonInstance) => btn['ref'])
+    .find((ref: Node) => ref.textContent === group)
+  if (groupBtn) {
+    groupScrollbar.value?.setScrollTop(groupBtn.offsetTop)
+  }
+}
+
+// 是否为相同的链接(不比较参数,只比较链接)
+const isSameLink = (link1: string, link2: string) => {
+  return split(link1, '?', 1)[0] === split(link2, '?', 1)[0]
+}
+
+// 详情选择对话框
+const detailSelectDialog = ref<{
+  visible: boolean
+  id?: number
+  type?: APP_LINK_TYPE_ENUM
+}>({
+  visible: false,
+  id: undefined,
+  type: undefined
+})
+// 处理详情选择
+const handleProductCategorySelected = (id: number) => {
+  const url = new URL(activeAppLink.value.path, 'http://127.0.0.1')
+  // 修改 id 参数
+  url.searchParams.set('id', `${id}`)
+  // 排除域名
+  activeAppLink.value.path = `${url.pathname}${url.search}`
+  // 关闭对话框
+  detailSelectDialog.value.visible = false
+  // 重置 id
+  detailSelectDialog.value.id = undefined
+}
+</script>
+<style lang="scss" scoped></style>

+ 237 - 0
src/components/SimpleProcessDesigner/src/addNode.vue

@@ -0,0 +1,237 @@
+/* stylelint-disable order/properties-order */
+<template>
+  <div class="add-node-btn-box">
+    <div class="add-node-btn">
+      <el-popover placement="right-start" v-model="visible" width="auto">
+        <div class="add-node-popover-body">
+          <a class="add-node-popover-item approver" @click="addType(1)">
+            <div class="item-wrapper">
+              <span class="iconfont"></span>
+            </div>
+            <p>审批人</p>
+          </a>
+          <a class="add-node-popover-item notifier" @click="addType(2)">
+            <div class="item-wrapper">
+              <span class="iconfont"></span>
+            </div>
+            <p>抄送人</p>
+          </a>
+          <a class="add-node-popover-item condition" @click="addType(4)">
+            <div class="item-wrapper">
+              <span class="iconfont"></span>
+            </div>
+            <p>条件分支</p>
+          </a>
+        </div>
+        <template #reference>
+          <button class="btn" type="button">
+            <span class="iconfont"></span>
+          </button>
+        </template>
+      </el-popover>
+    </div>
+  </div>
+</template>
+<script setup>
+import { ref } from 'vue'
+let props = defineProps({
+  childNodeP: {
+    type: Object,
+    default: () => ({})
+  }
+})
+let emits = defineEmits(['update:childNodeP'])
+let visible = ref(false)
+const addType = (type) => {
+  visible.value = false
+  if (type != 4) {
+    var data
+    if (type == 1) {
+      data = {
+        nodeName: '审核人',
+        error: true,
+        type: 1,
+        settype: 1,
+        selectMode: 0,
+        selectRange: 0,
+        directorLevel: 1,
+        examineMode: 1,
+        noHanderAction: 1,
+        examineEndDirectorLevel: 0,
+        childNode: props.childNodeP,
+        nodeUserList: []
+      }
+    } else if (type == 2) {
+      data = {
+        nodeName: '抄送人',
+        type: 2,
+        ccSelfSelectFlag: 1,
+        childNode: props.childNodeP,
+        nodeUserList: []
+      }
+    }
+    emits('update:childNodeP', data)
+  } else {
+    emits('update:childNodeP', {
+      nodeName: '路由',
+      type: 4,
+      childNode: null,
+      conditionNodes: [
+        {
+          nodeName: '条件1',
+          error: true,
+          type: 3,
+          priorityLevel: 1,
+          conditionList: [],
+          nodeUserList: [],
+          childNode: props.childNodeP
+        },
+        {
+          nodeName: '条件2',
+          type: 3,
+          priorityLevel: 2,
+          conditionList: [],
+          nodeUserList: [],
+          childNode: null
+        }
+      ]
+    })
+  }
+}
+</script>
+<style scoped lang="scss">
+.add-node-btn-box {
+  width: 240px;
+  display: inline-flex;
+  -ms-flex-negative: 0;
+  flex-shrink: 0;
+  -webkit-box-flex: 1;
+  -ms-flex-positive: 1;
+  position: relative;
+
+  &:before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: -1;
+    margin: auto;
+    width: 2px;
+    height: 100%;
+    background-color: #cacaca;
+  }
+
+  .add-node-btn {
+    user-select: none;
+    width: 240px;
+    padding: 20px 0 32px;
+    display: flex;
+    -webkit-box-pack: center;
+    justify-content: center;
+    flex-shrink: 0;
+    -webkit-box-flex: 1;
+    flex-grow: 1;
+
+    .btn {
+      outline: none;
+      box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
+      width: 30px;
+      height: 30px;
+      background: #3296fa;
+      border-radius: 50%;
+      position: relative;
+      border: none;
+      line-height: 30px;
+      -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+
+      .iconfont {
+        color: #fff;
+        font-size: 16px;
+      }
+
+      &:hover {
+        transform: scale(1.3);
+        box-shadow: 0 13px 27px 0 rgba(0, 0, 0, 0.1);
+      }
+
+      &:active {
+        transform: none;
+        background: #1e83e9;
+        box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
+      }
+    }
+  }
+}
+
+.add-node-popover-body {
+  display: flex;
+
+  .add-node-popover-item {
+    margin-right: 10px;
+    cursor: pointer;
+    text-align: center;
+    flex: 1;
+    color: #191f25 !important;
+
+    .item-wrapper {
+      user-select: none;
+      display: inline-block;
+      width: 80px;
+      height: 80px;
+      margin-bottom: 5px;
+      background: #fff;
+      border: 1px solid #e2e2e2;
+      border-radius: 50%;
+      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+
+      .iconfont {
+        font-size: 35px;
+        line-height: 80px;
+      }
+    }
+
+    &.approver {
+      .item-wrapper {
+        color: #ff943e;
+      }
+    }
+
+    &.notifier {
+      .item-wrapper {
+        color: #3296fa;
+      }
+    }
+
+    &.condition {
+      .item-wrapper {
+        color: #15bc83;
+      }
+    }
+
+    &:hover {
+      .item-wrapper {
+        background: #3296fa;
+        box-shadow: 0 10px 20px 0 rgba(50, 150, 250, 0.4);
+      }
+
+      .iconfont {
+        color: #fff;
+      }
+    }
+
+    &:active {
+      .item-wrapper {
+        box-shadow: none;
+        background: #eaeaea;
+      }
+
+      .iconfont {
+        color: inherit;
+      }
+    }
+  }
+}
+</style>

+ 1004 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json

@@ -0,0 +1,1004 @@
+{
+  "name": "Activiti",
+  "uri": "http://activiti.org/bpmn",
+  "prefix": "activiti",
+  "xml": {
+    "tagAlias": "lowerCase"
+  },
+  "associations": [],
+  "types": [
+    {
+      "name": "Definitions",
+      "isAbstract": true,
+      "extends": ["bpmn:Definitions"],
+      "properties": [
+        {
+          "name": "diagramRelationId",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "InOutBinding",
+      "superClass": ["Element"],
+      "isAbstract": true,
+      "properties": [
+        {
+          "name": "source",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "sourceExpression",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "target",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "businessKey",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "local",
+          "isAttr": true,
+          "type": "Boolean",
+          "default": false
+        },
+        {
+          "name": "variables",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "In",
+      "superClass": ["InOutBinding"],
+      "meta": {
+        "allowedIn": ["bpmn:CallActivity"]
+      }
+    },
+    {
+      "name": "Out",
+      "superClass": ["InOutBinding"],
+      "meta": {
+        "allowedIn": ["bpmn:CallActivity"]
+      }
+    },
+    {
+      "name": "AsyncCapable",
+      "isAbstract": true,
+      "extends": ["bpmn:Activity", "bpmn:Gateway", "bpmn:Event"],
+      "properties": [
+        {
+          "name": "async",
+          "isAttr": true,
+          "type": "Boolean",
+          "default": false
+        },
+        {
+          "name": "asyncBefore",
+          "isAttr": true,
+          "type": "Boolean",
+          "default": false
+        },
+        {
+          "name": "asyncAfter",
+          "isAttr": true,
+          "type": "Boolean",
+          "default": false
+        },
+        {
+          "name": "exclusive",
+          "isAttr": true,
+          "type": "Boolean",
+          "default": true
+        }
+      ]
+    },
+    {
+      "name": "JobPriorized",
+      "isAbstract": true,
+      "extends": ["bpmn:Process", "activiti:AsyncCapable"],
+      "properties": [
+        {
+          "name": "jobPriority",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "SignalEventDefinition",
+      "isAbstract": true,
+      "extends": ["bpmn:SignalEventDefinition"],
+      "properties": [
+        {
+          "name": "async",
+          "isAttr": true,
+          "type": "Boolean",
+          "default": false
+        }
+      ]
+    },
+    {
+      "name": "ErrorEventDefinition",
+      "isAbstract": true,
+      "extends": ["bpmn:ErrorEventDefinition"],
+      "properties": [
+        {
+          "name": "errorCodeVariable",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "errorMessageVariable",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "Error",
+      "isAbstract": true,
+      "extends": ["bpmn:Error"],
+      "properties": [
+        {
+          "name": "activiti:errorMessage",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "PotentialStarter",
+      "superClass": ["Element"],
+      "properties": [
+        {
+          "name": "resourceAssignmentExpression",
+          "type": "bpmn:ResourceAssignmentExpression"
+        }
+      ]
+    },
+    {
+      "name": "FormSupported",
+      "isAbstract": true,
+      "extends": ["bpmn:StartEvent", "bpmn:UserTask"],
+      "properties": [
+        {
+          "name": "formHandlerClass",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "formKey",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "TemplateSupported",
+      "isAbstract": true,
+      "extends": ["bpmn:Process", "bpmn:FlowElement"],
+      "properties": [
+        {
+          "name": "modelerTemplate",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "Initiator",
+      "isAbstract": true,
+      "extends": ["bpmn:StartEvent"],
+      "properties": [
+        {
+          "name": "initiator",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "ScriptTask",
+      "isAbstract": true,
+      "extends": ["bpmn:ScriptTask"],
+      "properties": [
+        {
+          "name": "resultVariable",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "resource",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "Process",
+      "isAbstract": true,
+      "extends": ["bpmn:Process"],
+      "properties": [
+        {
+          "name": "candidateStarterGroups",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "candidateStarterUsers",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "versionTag",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "historyTimeToLive",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "isStartableInTasklist",
+          "isAttr": true,
+          "type": "Boolean",
+          "default": true
+        },
+        {
+          "name": "executionListener",
+          "isAbstract": true,
+          "type": "Expression"
+        }
+      ]
+    },
+    {
+      "name": "EscalationEventDefinition",
+      "isAbstract": true,
+      "extends": ["bpmn:EscalationEventDefinition"],
+      "properties": [
+        {
+          "name": "escalationCodeVariable",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "FormalExpression",
+      "isAbstract": true,
+      "extends": ["bpmn:FormalExpression"],
+      "properties": [
+        {
+          "name": "resource",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "multiinstance_type",
+      "superClass": ["Element"]
+    },
+    {
+      "name": "multiinstance_condition",
+      "superClass": ["Element"]
+    },
+    {
+      "name": "Assignable",
+      "extends": ["bpmn:UserTask"],
+      "properties": [
+        {
+          "name": "assignee",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "candidateUsers",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "candidateGroups",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "dueDate",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "followUpDate",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "priority",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "multiinstance_condition",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "candidateStrategy",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "candidateParam",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "CallActivity",
+      "extends": ["bpmn:CallActivity"],
+      "properties": [
+        {
+          "name": "calledElementBinding",
+          "isAttr": true,
+          "type": "String",
+          "default": "latest"
+        },
+        {
+          "name": "calledElementVersion",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "calledElementVersionTag",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "calledElementTenantId",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "caseRef",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "caseBinding",
+          "isAttr": true,
+          "type": "String",
+          "default": "latest"
+        },
+        {
+          "name": "caseVersion",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "caseTenantId",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "variableMappingClass",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "variableMappingDelegateExpression",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "ServiceTaskLike",
+      "extends": [
+        "bpmn:ServiceTask",
+        "bpmn:BusinessRuleTask",
+        "bpmn:SendTask",
+        "bpmn:MessageEventDefinition"
+      ],
+      "properties": [
+        {
+          "name": "expression",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "class",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "delegateExpression",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "resultVariable",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "DmnCapable",
+      "extends": ["bpmn:BusinessRuleTask"],
+      "properties": [
+        {
+          "name": "decisionRef",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "decisionRefBinding",
+          "isAttr": true,
+          "type": "String",
+          "default": "latest"
+        },
+        {
+          "name": "decisionRefVersion",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "mapDecisionResult",
+          "isAttr": true,
+          "type": "String",
+          "default": "resultList"
+        },
+        {
+          "name": "decisionRefTenantId",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "ExternalCapable",
+      "extends": ["activiti:ServiceTaskLike"],
+      "properties": [
+        {
+          "name": "type",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "topic",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "TaskPriorized",
+      "extends": ["bpmn:Process", "activiti:ExternalCapable"],
+      "properties": [
+        {
+          "name": "taskPriority",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "Properties",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["*"]
+      },
+      "properties": [
+        {
+          "name": "values",
+          "type": "Property",
+          "isMany": true
+        }
+      ]
+    },
+    {
+      "name": "Property",
+      "superClass": ["Element"],
+      "properties": [
+        {
+          "name": "id",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "name",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "value",
+          "type": "String",
+          "isAttr": true
+        }
+      ]
+    },
+    {
+      "name": "Connector",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["activiti:ServiceTaskLike"]
+      },
+      "properties": [
+        {
+          "name": "inputOutput",
+          "type": "InputOutput"
+        },
+        {
+          "name": "connectorId",
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "InputOutput",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:FlowNode", "activiti:Connector"]
+      },
+      "properties": [
+        {
+          "name": "inputOutput",
+          "type": "InputOutput"
+        },
+        {
+          "name": "connectorId",
+          "type": "String"
+        },
+        {
+          "name": "inputParameters",
+          "isMany": true,
+          "type": "InputParameter"
+        },
+        {
+          "name": "outputParameters",
+          "isMany": true,
+          "type": "OutputParameter"
+        }
+      ]
+    },
+    {
+      "name": "InputOutputParameter",
+      "properties": [
+        {
+          "name": "name",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "value",
+          "isBody": true,
+          "type": "String"
+        },
+        {
+          "name": "definition",
+          "type": "InputOutputParameterDefinition"
+        }
+      ]
+    },
+    {
+      "name": "InputOutputParameterDefinition",
+      "isAbstract": true
+    },
+    {
+      "name": "List",
+      "superClass": ["InputOutputParameterDefinition"],
+      "properties": [
+        {
+          "name": "items",
+          "isMany": true,
+          "type": "InputOutputParameterDefinition"
+        }
+      ]
+    },
+    {
+      "name": "Map",
+      "superClass": ["InputOutputParameterDefinition"],
+      "properties": [
+        {
+          "name": "entries",
+          "isMany": true,
+          "type": "Entry"
+        }
+      ]
+    },
+    {
+      "name": "Entry",
+      "properties": [
+        {
+          "name": "key",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "value",
+          "isBody": true,
+          "type": "String"
+        },
+        {
+          "name": "definition",
+          "type": "InputOutputParameterDefinition"
+        }
+      ]
+    },
+    {
+      "name": "Value",
+      "superClass": ["InputOutputParameterDefinition"],
+      "properties": [
+        {
+          "name": "id",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "name",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "value",
+          "isBody": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "Script",
+      "superClass": ["InputOutputParameterDefinition"],
+      "properties": [
+        {
+          "name": "scriptFormat",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "resource",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "value",
+          "isBody": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "Field",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": [
+          "activiti:ServiceTaskLike",
+          "activiti:ExecutionListener",
+          "activiti:TaskListener"
+        ]
+      },
+      "properties": [
+        {
+          "name": "name",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "expression",
+          "type": "String"
+        },
+        {
+          "name": "stringValue",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "string",
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "InputParameter",
+      "superClass": ["InputOutputParameter"]
+    },
+    {
+      "name": "OutputParameter",
+      "superClass": ["InputOutputParameter"]
+    },
+    {
+      "name": "Collectable",
+      "isAbstract": true,
+      "extends": ["bpmn:MultiInstanceLoopCharacteristics"],
+      "superClass": ["activiti:AsyncCapable"],
+      "properties": [
+        {
+          "name": "collection",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "elementVariable",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "FailedJobRetryTimeCycle",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["activiti:AsyncCapable", "bpmn:MultiInstanceLoopCharacteristics"]
+      },
+      "properties": [
+        {
+          "name": "body",
+          "isBody": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "ExecutionListener",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": [
+          "bpmn:Task",
+          "bpmn:ServiceTask",
+          "bpmn:UserTask",
+          "bpmn:BusinessRuleTask",
+          "bpmn:ScriptTask",
+          "bpmn:ReceiveTask",
+          "bpmn:ManualTask",
+          "bpmn:ExclusiveGateway",
+          "bpmn:SequenceFlow",
+          "bpmn:ParallelGateway",
+          "bpmn:InclusiveGateway",
+          "bpmn:EventBasedGateway",
+          "bpmn:StartEvent",
+          "bpmn:IntermediateCatchEvent",
+          "bpmn:IntermediateThrowEvent",
+          "bpmn:EndEvent",
+          "bpmn:BoundaryEvent",
+          "bpmn:CallActivity",
+          "bpmn:SubProcess",
+          "bpmn:Process"
+        ]
+      },
+      "properties": [
+        {
+          "name": "expression",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "class",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "delegateExpression",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "event",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "script",
+          "type": "Script"
+        },
+        {
+          "name": "fields",
+          "type": "Field",
+          "isMany": true
+        }
+      ]
+    },
+    {
+      "name": "TaskListener",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "expression",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "class",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "delegateExpression",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "event",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "script",
+          "type": "Script"
+        },
+        {
+          "name": "fields",
+          "type": "Field",
+          "isMany": true
+        }
+      ]
+    },
+    {
+      "name": "FormProperty",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "id",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "name",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "type",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "required",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "readable",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "writable",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "variable",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "expression",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "datePattern",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "default",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "values",
+          "type": "Value",
+          "isMany": true
+        }
+      ]
+    },
+    {
+      "name": "FormProperty",
+      "superClass": ["Element"],
+      "properties": [
+        {
+          "name": "id",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "label",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "type",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "datePattern",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "defaultValue",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "properties",
+          "type": "Properties"
+        },
+        {
+          "name": "validation",
+          "type": "Validation"
+        },
+        {
+          "name": "values",
+          "type": "Value",
+          "isMany": true
+        }
+      ]
+    },
+    {
+      "name": "Validation",
+      "superClass": ["Element"],
+      "properties": [
+        {
+          "name": "constraints",
+          "type": "Constraint",
+          "isMany": true
+        }
+      ]
+    },
+    {
+      "name": "Constraint",
+      "superClass": ["Element"],
+      "properties": [
+        {
+          "name": "name",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "config",
+          "type": "String",
+          "isAttr": true
+        }
+      ]
+    },
+    {
+      "name": "ConditionalEventDefinition",
+      "isAbstract": true,
+      "extends": ["bpmn:ConditionalEventDefinition"],
+      "properties": [
+        {
+          "name": "variableName",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "variableEvent",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    }
+  ],
+  "emumerations": []
+}

+ 83 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/activiti/activitiExtension.js

@@ -0,0 +1,83 @@
+'use strict'
+
+import { some } from 'min-dash'
+
+// const some = require('min-dash').some
+// const some = some
+
+const ALLOWED_TYPES = {
+  FailedJobRetryTimeCycle: [
+    'bpmn:StartEvent',
+    'bpmn:BoundaryEvent',
+    'bpmn:IntermediateCatchEvent',
+    'bpmn:Activity'
+  ],
+  Connector: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'],
+  Field: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent']
+}
+
+function is(element, type) {
+  return element && typeof element.$instanceOf === 'function' && element.$instanceOf(type)
+}
+
+function exists(element) {
+  return element && element.length
+}
+
+function includesType(collection, type) {
+  return (
+    exists(collection) &&
+    some(collection, function (element) {
+      return is(element, type)
+    })
+  )
+}
+
+function anyType(element, types) {
+  return some(types, function (type) {
+    return is(element, type)
+  })
+}
+
+function isAllowed(propName, propDescriptor, newElement) {
+  const name = propDescriptor.name,
+    types = ALLOWED_TYPES[name.replace(/activiti:/, '')]
+
+  return name === propName && anyType(newElement, types)
+}
+
+function ActivitiModdleExtension(eventBus) {
+  eventBus.on(
+    'property.clone',
+    function (context) {
+      const newElement = context.newElement,
+        propDescriptor = context.propertyDescriptor
+
+      this.canCloneProperty(newElement, propDescriptor)
+    },
+    this
+  )
+}
+
+ActivitiModdleExtension.$inject = ['eventBus']
+
+ActivitiModdleExtension.prototype.canCloneProperty = function (newElement, propDescriptor) {
+  if (isAllowed('activiti:FailedJobRetryTimeCycle', propDescriptor, newElement)) {
+    return (
+      includesType(newElement.eventDefinitions, 'bpmn:TimerEventDefinition') ||
+      includesType(newElement.eventDefinitions, 'bpmn:SignalEventDefinition') ||
+      is(newElement.loopCharacteristics, 'bpmn:MultiInstanceLoopCharacteristics')
+    )
+  }
+
+  if (isAllowed('activiti:Connector', propDescriptor, newElement)) {
+    return includesType(newElement.eventDefinitions, 'bpmn:MessageEventDefinition')
+  }
+
+  if (isAllowed('activiti:Field', propDescriptor, newElement)) {
+    return includesType(newElement.eventDefinitions, 'bpmn:MessageEventDefinition')
+  }
+}
+
+// module.exports = ActivitiModdleExtension;
+export default ActivitiModdleExtension

+ 277 - 0
src/store/modules/app.ts

@@ -0,0 +1,277 @@
+import { defineStore } from 'pinia'
+import { store } from '../index'
+import { setCssVar, humpToUnderline } from '@/utils'
+import { ElMessage } from 'element-plus'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { ElementPlusSize } from '@/types/elementPlus'
+import { LayoutType } from '@/types/layout'
+import { ThemeTypes } from '@/types/theme'
+
+const { wsCache } = useCache()
+
+interface AppState {
+  breadcrumb: boolean
+  breadcrumbIcon: boolean
+  collapse: boolean
+  uniqueOpened: boolean
+  hamburger: boolean
+  screenfull: boolean
+  search: boolean
+  size: boolean
+  locale: boolean
+  message: boolean
+  tagsView: boolean
+  tagsViewIcon: boolean
+  logo: boolean
+  fixedHeader: boolean
+  greyMode: boolean
+  pageLoading: boolean
+  layout: LayoutType
+  title: string
+  userInfo: string
+  isDark: boolean
+  currentSize: ElementPlusSize
+  sizeMap: ElementPlusSize[]
+  mobile: boolean
+  footer: boolean
+  theme: ThemeTypes
+  fixedMenu: boolean
+}
+
+export const useAppStore = defineStore('app', {
+  state: (): AppState => {
+    return {
+      userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其他项目冲突
+      sizeMap: ['default', 'large', 'small'],
+      mobile: false, // 是否是移动端
+      title: import.meta.env.VITE_APP_TITLE, // 标题
+      pageLoading: false, // 路由跳转loading
+
+      breadcrumb: true, // 面包屑
+      breadcrumbIcon: true, // 面包屑图标
+      collapse: false, // 折叠菜单
+      uniqueOpened: true, // 是否只保持一个子菜单的展开
+      hamburger: true, // 折叠图标
+      screenfull: true, // 全屏图标
+      search: true, // 搜索图标
+      size: true, // 尺寸图标
+      locale: true, // 多语言图标
+      message: true, // 消息图标
+      tagsView: true, // 标签页
+      tagsViewIcon: true, // 是否显示标签图标
+      logo: true, // logo
+      fixedHeader: true, // 固定toolheader
+      footer: true, // 显示页脚
+      greyMode: false, // 是否开始灰色模式,用于特殊悼念日
+      fixedMenu: wsCache.get('fixedMenu') || false, // 是否固定菜单
+
+      layout: wsCache.get(CACHE_KEY.LAYOUT) || 'classic', // layout布局
+      isDark: wsCache.get(CACHE_KEY.IS_DARK) || false, // 是否是暗黑模式
+      currentSize: wsCache.get('default') || 'default', // 组件尺寸
+      theme: wsCache.get(CACHE_KEY.THEME) || {
+        // 主题色
+        elColorPrimary: '#409eff',
+        // 左侧菜单边框颜色
+        leftMenuBorderColor: 'inherit',
+        // 左侧菜单背景颜色
+        leftMenuBgColor: '#001529',
+        // 左侧菜单浅色背景颜色
+        leftMenuBgLightColor: '#0f2438',
+        // 左侧菜单选中背景颜色
+        leftMenuBgActiveColor: 'var(--el-color-primary)',
+        // 左侧菜单收起选中背景颜色
+        leftMenuCollapseBgActiveColor: 'var(--el-color-primary)',
+        // 左侧菜单字体颜色
+        leftMenuTextColor: '#bfcbd9',
+        // 左侧菜单选中字体颜色
+        leftMenuTextActiveColor: '#fff',
+        // logo字体颜色
+        logoTitleTextColor: '#fff',
+        // logo边框颜色
+        logoBorderColor: 'inherit',
+        // 头部背景颜色
+        topHeaderBgColor: '#fff',
+        // 头部字体颜色
+        topHeaderTextColor: 'inherit',
+        // 头部悬停颜色
+        topHeaderHoverColor: '#f6f6f6',
+        // 头部边框颜色
+        topToolBorderColor: '#eee'
+      }
+    }
+  },
+  getters: {
+    getBreadcrumb(): boolean {
+      return this.breadcrumb
+    },
+    getBreadcrumbIcon(): boolean {
+      return this.breadcrumbIcon
+    },
+    getCollapse(): boolean {
+      return this.collapse
+    },
+    getUniqueOpened(): boolean {
+      return this.uniqueOpened
+    },
+    getHamburger(): boolean {
+      return this.hamburger
+    },
+    getScreenfull(): boolean {
+      return this.screenfull
+    },
+    getSize(): boolean {
+      return this.size
+    },
+    getLocale(): boolean {
+      return this.locale
+    },
+    getMessage(): boolean {
+      return this.message
+    },
+    getTagsView(): boolean {
+      return this.tagsView
+    },
+    getTagsViewIcon(): boolean {
+      return this.tagsViewIcon
+    },
+    getLogo(): boolean {
+      return this.logo
+    },
+    getFixedHeader(): boolean {
+      return this.fixedHeader
+    },
+    getGreyMode(): boolean {
+      return this.greyMode
+    },
+    getFixedMenu(): boolean {
+      return this.fixedMenu
+    },
+    getPageLoading(): boolean {
+      return this.pageLoading
+    },
+    getLayout(): LayoutType {
+      return this.layout
+    },
+    getTitle(): string {
+      return this.title
+    },
+    getUserInfo(): string {
+      return this.userInfo
+    },
+    getIsDark(): boolean {
+      return this.isDark
+    },
+    getCurrentSize(): ElementPlusSize {
+      return this.currentSize
+    },
+    getSizeMap(): ElementPlusSize[] {
+      return this.sizeMap
+    },
+    getMobile(): boolean {
+      return this.mobile
+    },
+    getTheme(): ThemeTypes {
+      return this.theme
+    },
+    getFooter(): boolean {
+      return this.footer
+    }
+  },
+  actions: {
+    setBreadcrumb(breadcrumb: boolean) {
+      this.breadcrumb = breadcrumb
+    },
+    setBreadcrumbIcon(breadcrumbIcon: boolean) {
+      this.breadcrumbIcon = breadcrumbIcon
+    },
+    setCollapse(collapse: boolean) {
+      this.collapse = collapse
+    },
+    setUniqueOpened(uniqueOpened: boolean) {
+      this.uniqueOpened = uniqueOpened
+    },
+    setHamburger(hamburger: boolean) {
+      this.hamburger = hamburger
+    },
+    setScreenfull(screenfull: boolean) {
+      this.screenfull = screenfull
+    },
+    setSize(size: boolean) {
+      this.size = size
+    },
+    setLocale(locale: boolean) {
+      this.locale = locale
+    },
+    setMessage(message: boolean) {
+      this.message = message
+    },
+    setTagsView(tagsView: boolean) {
+      this.tagsView = tagsView
+    },
+    setTagsViewIcon(tagsViewIcon: boolean) {
+      this.tagsViewIcon = tagsViewIcon
+    },
+    setLogo(logo: boolean) {
+      this.logo = logo
+    },
+    setFixedHeader(fixedHeader: boolean) {
+      this.fixedHeader = fixedHeader
+    },
+    setGreyMode(greyMode: boolean) {
+      this.greyMode = greyMode
+    },
+    setFixedMenu(fixedMenu: boolean) {
+      wsCache.set('fixedMenu', fixedMenu)
+      this.fixedMenu = fixedMenu
+    },
+    setPageLoading(pageLoading: boolean) {
+      this.pageLoading = pageLoading
+    },
+    setLayout(layout: LayoutType) {
+      if (this.mobile && layout !== 'classic') {
+        ElMessage.warning('移动端模式下不支持切换其他布局')
+        return
+      }
+      this.layout = layout
+      wsCache.set(CACHE_KEY.LAYOUT, this.layout)
+    },
+    setTitle(title: string) {
+      this.title = title
+    },
+    setIsDark(isDark: boolean) {
+      this.isDark = isDark
+      if (this.isDark) {
+        document.documentElement.classList.add('dark')
+        document.documentElement.classList.remove('light')
+      } else {
+        document.documentElement.classList.add('light')
+        document.documentElement.classList.remove('dark')
+      }
+      wsCache.set(CACHE_KEY.IS_DARK, this.isDark)
+    },
+    setCurrentSize(currentSize: ElementPlusSize) {
+      this.currentSize = currentSize
+      wsCache.set('currentSize', this.currentSize)
+    },
+    setMobile(mobile: boolean) {
+      this.mobile = mobile
+    },
+    setTheme(theme: ThemeTypes) {
+      this.theme = Object.assign(this.theme, theme)
+      wsCache.set(CACHE_KEY.THEME, this.theme)
+    },
+    setCssVarTheme() {
+      for (const key in this.theme) {
+        setCssVar(`--${humpToUnderline(key)}`, this.theme[key])
+      }
+    },
+    setFooter(footer: boolean) {
+      this.footer = footer
+    }
+  },
+  persist: false
+})
+
+export const useAppStoreWithOut = () => {
+  return useAppStore(store)
+}

+ 8 - 0
src/views/Error/403.vue

@@ -0,0 +1,8 @@
+<template>
+  <Error type="403" @error-click="push('/')" />
+</template>
+<script lang="ts" setup>
+defineOptions({ name: 'Error403' })
+
+const { push } = useRouter()
+</script>

+ 7 - 0
src/views/Error/500.vue

@@ -0,0 +1,7 @@
+<template>
+  <Error type="500" @error-click="push('/')" />
+</template>
+<script lang="ts" setup>
+defineOptions({ name: 'Error500' })
+const { push } = useRouter()
+</script>

+ 132 - 0
src/views/ai/model/apiKey/ApiKeyForm.vue

@@ -0,0 +1,132 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="所属平台" prop="platform">
+        <el-select v-model="formData.platform" placeholder="请输入平台" clearable>
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入名称" />
+      </el-form-item>
+      <el-form-item label="密钥" prop="apiKey">
+        <el-input v-model="formData.apiKey" placeholder="请输入密钥" />
+      </el-form-item>
+      <el-form-item label="自定义 API URL" prop="url">
+        <el-input v-model="formData.url" placeholder="请输入自定义 API URL" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** AI API 密钥 表单 */
+defineOptions({ name: 'ApiKeyForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  apiKey: undefined,
+  platform: undefined,
+  url: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+  name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
+  apiKey: [{ required: true, message: '密钥不能为空', trigger: 'blur' }],
+  platform: [{ required: true, message: '平台不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await ApiKeyApi.getApiKey(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as ApiKeyVO
+    if (formType.value === 'create') {
+      await ApiKeyApi.createApiKey(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ApiKeyApi.updateApiKey(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    apiKey: undefined,
+    platform: undefined,
+    url: undefined,
+    status: CommonStatusEnum.ENABLE
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 169 - 0
src/views/bpm/fund/apply/apply-create.vue

@@ -0,0 +1,169 @@
+<template>
+  <el-form
+    ref="formRef"
+    v-loading="formLoading"
+    :model="formData"
+    :rules="formRules"
+    label-width="80px"
+  >
+    <el-form-item label="资金类型" prop="type">
+      <el-select v-model="formData.type" clearable placeholder="请选择资金申请类型">
+        <el-option
+          v-for="dict in getIntDictOptions(DICT_TYPE.BPM_FUND_APPLY_TYPE)"
+          :key="dict.value"
+          :label="dict.label"
+          :value="dict.value"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item label="申请时间" prop="startTime">
+      <el-date-picker
+        v-model="formData.startTime"
+        clearable
+        placeholder="请选择开始申请时间"
+        type="datetime"
+        value-format="x"
+      />
+<!--    </el-form-item>-->
+<!--    <el-form-item label="结束时间" prop="endTime">-->
+<!--      <el-date-picker-->
+<!--        v-model="formData.endTime"-->
+<!--        clearable-->
+<!--        placeholder="请选择结束时间"-->
+<!--        type="datetime"-->
+<!--        value-format="x"-->
+<!--      />-->
+    </el-form-item>
+    <el-form-item label="申请金额" prop="applyAmount">
+      <el-input v-model="formData.applyAmount"  placeholder="请输申请金额" type="number" />
+    </el-form-item>
+    <el-form-item label="申请原因" prop="reason">
+      <el-input v-model="formData.reason" placeholder="请输申请原因" type="textarea" />
+    </el-form-item>
+    <el-col v-if="startUserSelectTasks.length > 0">
+      <el-card class="mb-10px">
+        <template #header>指定审批人</template>
+        <el-form
+          :model="startUserSelectAssignees"
+          :rules="startUserSelectAssigneesFormRules"
+          ref="startUserSelectAssigneesFormRef"
+        >
+          <el-form-item
+            v-for="userTask in startUserSelectTasks"
+            :key="userTask.id"
+            :label="`任务【${userTask.name}】`"
+            :prop="userTask.id"
+          >
+            <el-select
+              v-model="startUserSelectAssignees[userTask.id]"
+              multiple
+              placeholder="请选择审批人"
+            >
+              <el-option
+                v-for="user in userList"
+                :key="user.id"
+                :label="user.nickname"
+                :value="user.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-form>
+      </el-card>
+    </el-col>
+    <el-form-item>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+    </el-form-item>
+  </el-form>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as FundApi from '@/api/bpm/fund'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as DefinitionApi from '@/api/bpm/definition'
+import * as UserApi from '@/api/system/user'
+
+defineOptions({ name: 'BpmFundApplyCreate' })
+
+const message = useMessage() // 消息弹窗
+const { delView } = useTagsViewStore() // 视图操作
+const { push, currentRoute } = useRouter() // 路由
+
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  type: undefined,
+  reason: undefined,
+  startTime: undefined,
+  // endTime: undefined,
+  applyAmount: undefined
+})
+const formRules = reactive({
+  type: [{ required: true, message: '资金类型不能为空', trigger: 'blur' }],
+  applyAmount: [{ required: true, message: '申请金额不能为空', trigger: 'change' }],
+  reason: [{ required: true, message: '申请原因不能为空', trigger: 'change' }],
+  startTime: [{ required: true, message: '申请时间不能为空', trigger: 'change' }],
+  // endTime: [{ required: true, message: '申请结束时间不能为空', trigger: 'change' }]
+})
+const formRef = ref() // 表单 Ref
+
+// 指定审批人
+const processDefineKey = 'fund_apply' // 流程定义 Key
+const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
+const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
+const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
+const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
+const userList = ref<any[]>([]) // 用户列表
+
+/** 提交表单 */
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 校验指定审批人
+  if (startUserSelectTasks.value?.length > 0) {
+    await startUserSelectAssigneesFormRef.value.validate()
+  }
+
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = { ...formData.value } as unknown as FundApi.FundVO
+    // 设置指定审批人
+    if (startUserSelectTasks.value?.length > 0) {
+      data.startUserSelectAssignees = startUserSelectAssignees.value
+    }
+    await FundApi.createFundApply(data)
+    message.success('发起成功')
+    // 关闭当前 Tab
+    delView(unref(currentRoute))
+    await push({ name: 'BpmFundApply' })
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 初始化 */
+onMounted(async () => {
+  const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
+    undefined,
+    processDefineKey
+  )
+  if (!processDefinitionDetail) {
+    message.error('资金申请的流程模型未配置,请检查!')
+    return
+  }
+  startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
+  // 设置指定审批人
+  if (startUserSelectTasks.value?.length > 0) {
+    // 设置校验规则
+    for (const userTask of startUserSelectTasks.value) {
+      startUserSelectAssignees.value[userTask.id] = []
+      startUserSelectAssigneesFormRules.value[userTask.id] = [
+        { required: true, message: '请选择审批人', trigger: 'blur' }
+      ]
+    }
+    // 加载用户列表
+    userList.value = await UserApi.getSimpleUserList()
+  }
+})
+</script>

+ 124 - 0
src/views/erp/finance/account/AccountForm.vue

@@ -0,0 +1,124 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入名称" />
+      </el-form-item>
+      <el-form-item label="编码" prop="no">
+        <el-input v-model="formData.no" placeholder="请输入编码" />
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input v-model="formData.remark" placeholder="请输入备注" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="排序" prop="sort">
+        <el-input v-model="formData.sort" placeholder="请输入排序" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { AccountApi, AccountVO } from '@/api/erp/finance/account'
+
+/** ERP 结算 表单 */
+defineOptions({ name: 'AccountForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  no: undefined,
+  remark: undefined,
+  status: undefined,
+  sort: undefined,
+  defaultStatus: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }],
+  sort: [{ required: true, message: '排序不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await AccountApi.getAccount(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as AccountVO
+    if (formType.value === 'create') {
+      await AccountApi.createAccount(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await AccountApi.updateAccount(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    no: undefined,
+    remark: undefined,
+    status: undefined,
+    sort: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 79 - 0
src/views/infra/apiAccessLog/ApiAccessLogDetail.vue

@@ -0,0 +1,79 @@
+<template>
+  <Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="详情" width="800">
+    <el-descriptions :column="1" border>
+      <el-descriptions-item label="日志主键" min-width="120">
+        {{ detailData.id }}
+      </el-descriptions-item>
+      <el-descriptions-item label="链路追踪">
+        {{ detailData.traceId }}
+      </el-descriptions-item>
+      <el-descriptions-item label="应用名">
+        {{ detailData.applicationName }}
+      </el-descriptions-item>
+      <el-descriptions-item label="用户信息">
+        {{ detailData.userId }}
+        <dict-tag :type="DICT_TYPE.USER_TYPE" :value="detailData.userType" />
+      </el-descriptions-item>
+      <el-descriptions-item label="用户 IP">
+        {{ detailData.userIp }}
+      </el-descriptions-item>
+      <el-descriptions-item label="用户 UA">
+        {{ detailData.userAgent }}
+      </el-descriptions-item>
+      <el-descriptions-item label="请求信息">
+        {{ detailData.requestMethod }} {{ detailData.requestUrl }}
+      </el-descriptions-item>
+      <el-descriptions-item label="请求参数">
+        {{ detailData.requestParams }}
+      </el-descriptions-item>
+      <el-descriptions-item label="请求结果">
+        {{ detailData.responseBody }}
+      </el-descriptions-item>
+      <el-descriptions-item label="请求时间">
+        {{ formatDate(detailData.beginTime) }} ~ {{ formatDate(detailData.endTime) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="请求耗时">{{ detailData.duration }} ms</el-descriptions-item>
+      <el-descriptions-item label="操作结果">
+        <div v-if="detailData.resultCode === 0">正常</div>
+        <div v-else-if="detailData.resultCode > 0">
+          失败 | {{ detailData.resultCode }} | {{ detailData.resultMsg }}
+        </div>
+      </el-descriptions-item>
+      <el-descriptions-item label="操作模块">
+        {{ detailData.operateModule }}
+      </el-descriptions-item>
+      <el-descriptions-item label="操作名">
+        {{ detailData.operateName }}
+      </el-descriptions-item>
+      <el-descriptions-item label="操作名">
+        <dict-tag :type="DICT_TYPE.INFRA_OPERATE_TYPE" :value="detailData.operateType" />
+      </el-descriptions-item>
+    </el-descriptions>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as ApiAccessLog from '@/api/infra/apiAccessLog'
+
+defineOptions({ name: 'ApiAccessLogDetail' })
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const detailLoading = ref(false) // 表单地加载中
+const detailData = ref({} as ApiAccessLog.ApiAccessLogVO) // 详情数据
+
+/** 打开弹窗 */
+const open = async (data: ApiAccessLog.ApiAccessLogVO) => {
+  dialogVisible.value = true
+  // 设置数据
+  detailLoading.value = true
+  try {
+    detailData.value = data
+  } finally {
+    detailLoading.value = false
+  }
+}
+
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+</script>

+ 81 - 0
src/views/infra/apiErrorLog/ApiErrorLogDetail.vue

@@ -0,0 +1,81 @@
+<template>
+  <Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="详情" width="800">
+    <el-descriptions :column="1" border>
+      <el-descriptions-item label="日志主键" min-width="120">
+        {{ detailData.id }}
+      </el-descriptions-item>
+      <el-descriptions-item label="链路追踪">
+        {{ detailData.traceId }}
+      </el-descriptions-item>
+      <el-descriptions-item label="应用名">
+        {{ detailData.applicationName }}
+      </el-descriptions-item>
+      <el-descriptions-item label="用户编号">
+        {{ detailData.userId }}
+        <dict-tag :type="DICT_TYPE.USER_TYPE" :value="detailData.userType" />
+      </el-descriptions-item>
+      <el-descriptions-item label="用户 IP">
+        {{ detailData.userIp }}
+      </el-descriptions-item>
+      <el-descriptions-item label="用户 UA">
+        {{ detailData.userAgent }}
+      </el-descriptions-item>
+      <el-descriptions-item label="请求信息">
+        {{ detailData.requestMethod }} {{ detailData.requestUrl }}
+      </el-descriptions-item>
+      <el-descriptions-item label="请求参数">
+        {{ detailData.requestParams }}
+      </el-descriptions-item>
+      <el-descriptions-item label="异常时间">
+        {{ formatDate(detailData.exceptionTime) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="异常名">
+        {{ detailData.exceptionName }}
+      </el-descriptions-item>
+      <el-descriptions-item v-if="detailData.exceptionStackTrace" label="异常堆栈">
+        <el-input
+          v-model="detailData.exceptionStackTrace"
+          :autosize="{ maxRows: 20 }"
+          :readonly="true"
+          type="textarea"
+        />
+      </el-descriptions-item>
+      <el-descriptions-item label="处理状态">
+        <dict-tag
+          :type="DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS"
+          :value="detailData.processStatus"
+        />
+      </el-descriptions-item>
+      <el-descriptions-item v-if="detailData.processUserId" label="处理人">
+        {{ detailData.processUserId }}
+      </el-descriptions-item>
+      <el-descriptions-item v-if="detailData.processTime" label="处理时间">
+        {{ formatDate(detailData.processTime) }}
+      </el-descriptions-item>
+    </el-descriptions>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as ApiErrorLog from '@/api/infra/apiErrorLog'
+
+defineOptions({ name: 'ApiErrorLogDetail' })
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const detailLoading = ref(false) // 表单的加载中
+const detailData = ref({} as ApiErrorLog.ApiErrorLogVO) // 详情数据
+
+/** 打开弹窗 */
+const open = async (data: ApiErrorLog.ApiErrorLogVO) => {
+  dialogVisible.value = true
+  // 设置数据
+  detailLoading.value = true
+  try {
+    detailData.value = data
+  } finally {
+    detailLoading.value = false
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+</script>

BIN
src/views/mall/promotion/kefu/components/asserts/a.png


BIN
src/views/mall/promotion/kefu/components/asserts/aini.png


BIN
src/views/mall/promotion/kefu/components/asserts/aixin.png


+ 70 - 0
src/views/mall/trade/afterSale/form/AfterSaleDisagreeForm.vue

@@ -0,0 +1,70 @@
+<template>
+  <Dialog v-model="dialogVisible" title="拒绝售后" width="45%">
+    <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
+      <el-form-item label="审批备注">
+        <el-input
+          v-model="formData.auditReason"
+          :rows="3"
+          placeholder="请输入审批备注"
+          type="textarea"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import * as AfterSaleApi from '@/api/mall/trade/afterSale/index'
+
+defineOptions({ name: 'AfterSaleDisagreeForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  id: undefined, // 售后订单编号
+  auditReason: '' // 审批备注
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (row: AfterSaleApi.TradeAfterSaleVO) => {
+  resetForm()
+  // 设置数据
+  formData.value.id = row.id
+  formData.value.auditReason = row.auditReason
+  dialogVisible.value = true
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = unref(formData)
+    await AfterSaleApi.disagree(data)
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success', true)
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined, // 售后订单编号
+    auditReason: '' // 审批备注
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 160 - 0
src/views/mp/account/AccountForm.vue

@@ -0,0 +1,160 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="rules"
+      label-width="120px"
+    >
+      <el-form-item label="名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入名称" />
+      </el-form-item>
+      <el-form-item label="微信号" prop="account">
+        <template #label>
+          <span>
+            <el-tooltip
+              content="在微信公众平台(mp.weixin.qq.com)的菜单 [设置与开发 - 公众号设置 - 账号详情] 中能找到「微信号」"
+              placement="top"
+            >
+              <Icon icon="ep:question-filled" style="vertical-align: middle" />
+            </el-tooltip>
+            微信号
+          </span>
+        </template>
+        <el-input v-model="formData.account" placeholder="请输入微信号" />
+      </el-form-item>
+      <el-form-item label="appId" prop="appId">
+        <template #label>
+          <span>
+            <el-tooltip
+              content="在微信公众平台(mp.weixin.qq.com)的菜单 [设置与开发 - 公众号设置 - 基本设置] 中能找到「开发者ID(AppID)」"
+              placement="top"
+            >
+              <Icon icon="ep:question-filled" style="vertical-align: middle" />
+            </el-tooltip>
+            appId
+          </span>
+        </template>
+        <el-input v-model="formData.appId" placeholder="请输入公众号 appId" />
+      </el-form-item>
+      <el-form-item label="appSecret" prop="appSecret">
+        <template #label>
+          <span>
+            <el-tooltip
+              content="在微信公众平台(mp.weixin.qq.com)的菜单 [设置与开发 - 公众号设置 - 基本设置] 中能找到「开发者密码(AppSecret)」"
+              placement="top"
+            >
+              <Icon icon="ep:question-filled" style="vertical-align: middle" />
+            </el-tooltip>
+            appSecret
+          </span>
+        </template>
+        <el-input v-model="formData.appSecret" placeholder="请输入公众号 appSecret" />
+      </el-form-item>
+      <el-form-item label="token" prop="token">
+        <el-input v-model="formData.token" placeholder="请输入公众号token" />
+      </el-form-item>
+      <el-form-item label="消息加解密密钥" prop="aesKey">
+        <el-input v-model="formData.aesKey" placeholder="请输入消息加解密密钥" />
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import * as AccountApi from '@/api/mp/account'
+
+defineOptions({ name: 'MpAccountForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: '',
+  account: '',
+  appId: '',
+  appSecret: '',
+  token: '',
+  aesKey: '',
+  remark: ''
+})
+const rules = reactive({
+  name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
+  account: [{ required: true, message: '公众号账号不能为空', trigger: 'blur' }],
+  appId: [{ required: true, message: '公众号 appId 不能为空', trigger: 'blur' }],
+  appSecret: [{ required: true, message: '公众号密钥不能为空', trigger: 'blur' }],
+  token: [{ required: true, message: '公众号 token 不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await AccountApi.getAccount(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await AccountApi.createAccount(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await AccountApi.updateAccount(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 表单重置 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: '',
+    account: '',
+    appId: '',
+    appSecret: '',
+    token: '',
+    aesKey: '',
+    remark: ''
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 130 - 0
src/views/pay/app/components/AppForm.vue

@@ -0,0 +1,130 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="160px"
+    >
+      <el-form-item label="应用名" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入应用名" />
+      </el-form-item>
+      <el-form-item label="开启状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="支付结果的回调地址" prop="orderNotifyUrl">
+        <el-input v-model="formData.orderNotifyUrl" placeholder="请输入支付结果的回调地址" />
+      </el-form-item>
+      <el-form-item label="退款结果的回调地址" prop="refundNotifyUrl">
+        <el-input v-model="formData.refundNotifyUrl" placeholder="请输入退款结果的回调地址" />
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input v-model="formData.remark" placeholder="请输入备注" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as AppApi from '@/api/pay/app'
+import { CommonStatusEnum } from '@/utils/constants'
+
+defineOptions({ name: 'PayAppForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  packageId: undefined,
+  contactName: undefined,
+  contactMobile: undefined,
+  accountCount: undefined,
+  expireTime: undefined,
+  domain: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+  name: [{ required: true, message: '应用名不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }],
+  orderNotifyUrl: [{ required: true, message: '支付结果的回调地址不能为空', trigger: 'blur' }],
+  refundNotifyUrl: [{ required: true, message: '退款结果的回调地址不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await AppApi.getApp(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as AppApi.AppVO
+    if (formType.value === 'create') {
+      await AppApi.createApp(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await AppApi.updateApp(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    status: CommonStatusEnum.ENABLE,
+    remark: undefined,
+    orderNotifyUrl: undefined,
+    refundNotifyUrl: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 326 - 0
src/views/pay/app/components/channel/AlipayChannelForm.vue

@@ -0,0 +1,326 @@
+<template>
+  <div>
+    <Dialog v-model="dialogVisible" :title="dialogTitle" @closed="close" width="830px">
+      <el-form
+        ref="formRef"
+        :model="formData"
+        :formRules="formRules"
+        label-width="100px"
+        v-loading="formLoading"
+      >
+        <el-form-item label-width="180px" label="渠道费率" prop="feeRate">
+          <el-input v-model="formData.feeRate" placeholder="请输入渠道费率" clearable>
+            <template #append>%</template>
+          </el-input>
+        </el-form-item>
+        <el-form-item label-width="180px" label="开放平台 APPID" prop="config.appId">
+          <el-input v-model="formData.config.appId" placeholder="请输入开放平台 APPID" clearable />
+        </el-form-item>
+        <el-form-item label-width="180px" label="渠道状态" prop="status">
+          <el-radio-group v-model="formData.status">
+            <el-radio
+              v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)"
+              :key="parseInt(dict.value)"
+              :label="parseInt(dict.value)"
+            >
+              {{ dict.label }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label-width="180px" label="网关地址" prop="config.serverUrl">
+          <el-radio-group v-model="formData.config.serverUrl">
+            <el-radio label="https://openapi.alipay.com/gateway.do">线上环境</el-radio>
+            <el-radio label="https://openapi-sandbox.dl.alipaydev.com/gateway.do">
+              沙箱环境
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label-width="180px" label="算法类型" prop="config.signType">
+          <el-radio-group v-model="formData.config.signType">
+            <el-radio key="RSA2" label="RSA2">RSA2</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label-width="180px" label="公钥类型" prop="config.mode">
+          <el-radio-group v-model="formData.config.mode">
+            <el-radio key="公钥模式" :label="1">公钥模式</el-radio>
+            <el-radio key="证书模式" :label="2">证书模式</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <div v-if="formData.config.mode === 1">
+          <el-form-item label-width="180px" label="应用私钥" prop="config.privateKey">
+            <el-input
+              type="textarea"
+              :autosize="{ minRows: 8, maxRows: 8 }"
+              v-model="formData.config.privateKey"
+              placeholder="请输入应用私钥"
+              clearable
+              :style="{ width: '100%' }"
+            />
+          </el-form-item>
+          <el-form-item label-width="180px" label="支付宝公钥" prop="config.alipayPublicKey">
+            <el-input
+              type="textarea"
+              :autosize="{ minRows: 8, maxRows: 8 }"
+              v-model="formData.config.alipayPublicKey"
+              placeholder="请输入支付宝公钥"
+              clearable
+              :style="{ width: '100%' }"
+            />
+          </el-form-item>
+        </div>
+        <div v-if="formData.config.mode === 2">
+          <el-form-item label-width="180px" label="应用私钥" prop="config.privateKey">
+            <el-input
+              type="textarea"
+              :autosize="{ minRows: 8, maxRows: 8 }"
+              v-model="formData.config.privateKey"
+              placeholder="请输入应用私钥"
+              clearable
+              :style="{ width: '100%' }"
+            />
+          </el-form-item>
+          <el-form-item label-width="180px" label="商户公钥应用证书" prop="config.appCertContent">
+            <el-input
+              v-model="formData.config.appCertContent"
+              type="textarea"
+              placeholder="请上传商户公钥应用证书"
+              readonly
+              :autosize="{ minRows: 8, maxRows: 8 }"
+              :style="{ width: '100%' }"
+            />
+          </el-form-item>
+          <el-form-item label-width="180px" label="">
+            <el-upload
+              action=""
+              ref="privateKeyContentFile"
+              :limit="1"
+              :accept="fileAccept"
+              :http-request="appCertUpload"
+              :before-upload="fileBeforeUpload"
+            >
+              <el-button type="primary">
+                <Icon icon="ep:upload" class="mr-5px" /> 点击上传
+              </el-button>
+            </el-upload>
+          </el-form-item>
+          <el-form-item
+            label-width="180px"
+            label="支付宝公钥证书"
+            prop="config.alipayPublicCertContent"
+          >
+            <el-input
+              v-model="formData.config.alipayPublicCertContent"
+              type="textarea"
+              placeholder="请上传支付宝公钥证书"
+              readonly
+              :autosize="{ minRows: 8, maxRows: 8 }"
+              :style="{ width: '100%' }"
+            />
+          </el-form-item>
+          <el-form-item label-width="180px" label="">
+            <el-upload
+              ref="privateCertContentFile"
+              action=""
+              :limit="1"
+              :accept="fileAccept"
+              :before-upload="fileBeforeUpload"
+              :http-request="alipayPublicCertUpload"
+            >
+              <el-button type="primary">
+                <Icon icon="ep:upload" class="mr-5px" /> 点击上传
+              </el-button>
+            </el-upload>
+          </el-form-item>
+          <el-form-item label-width="180px" label="根证书" prop="config.rootCertContent">
+            <el-input
+              v-model="formData.config.rootCertContent"
+              type="textarea"
+              placeholder="请上传根证书"
+              readonly
+              :autosize="{ minRows: 8, maxRows: 8 }"
+              :style="{ width: '100%' }"
+            />
+          </el-form-item>
+          <el-form-item label-width="180px" label="">
+            <el-upload
+              ref="privateCertContentFile"
+              :limit="1"
+              :accept="fileAccept"
+              action=""
+              :before-upload="fileBeforeUpload"
+              :http-request="rootCertUpload"
+            >
+              <el-button type="primary">
+                <Icon icon="ep:upload" class="mr-5px" /> 点击上传
+              </el-button>
+            </el-upload>
+          </el-form-item>
+        </div>
+        <el-form-item label-width="180px" label="备注" prop="remark">
+          <el-input v-model="formData.remark" :style="{ width: '100%' }" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </template>
+    </Dialog>
+  </div>
+</template>
+<script lang="ts" setup>
+import { CommonStatusEnum } from '@/utils/constants'
+import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import * as ChannelApi from '@/api/pay/channel'
+
+defineOptions({ name: 'AlipayChannelForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref<any>({
+  appId: '',
+  code: '',
+  status: undefined,
+  feeRate: undefined,
+  remark: '',
+  config: {
+    appId: '',
+    serverUrl: null,
+    signType: '',
+    mode: null,
+    privateKey: '',
+    alipayPublicKey: '',
+    appCertContent: '',
+    alipayPublicCertContent: '',
+    rootCertContent: ''
+  }
+})
+const formRules = {
+  feeRate: [{ required: true, message: '请输入渠道费率', trigger: 'blur' }],
+  status: [{ required: true, message: '渠道状态不能为空', trigger: 'blur' }],
+  'config.appId': [{ required: true, message: '请输入开放平台上创建的应用的 ID', trigger: 'blur' }],
+  'config.serverUrl': [{ required: true, message: '请传入网关地址', trigger: 'blur' }],
+  'config.signType': [{ required: true, message: '请传入签名算法类型', trigger: 'blur' }],
+  'config.mode': [{ required: true, message: '公钥类型不能为空', trigger: 'blur' }],
+  'config.privateKey': [{ required: true, message: '请输入商户私钥', trigger: 'blur' }],
+  'config.alipayPublicKey': [
+    { required: true, message: '请输入支付宝公钥字符串', trigger: 'blur' }
+  ],
+  'config.appCertContent': [{ required: true, message: '请上传商户公钥应用证书', trigger: 'blur' }],
+  'config.alipayPublicCertContent': [
+    { required: true, message: '请上传支付宝公钥证书', trigger: 'blur' }
+  ],
+  'config.rootCertContent': [{ required: true, message: '请上传指定根证书', trigger: 'blur' }]
+}
+const fileAccept = '.crt'
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (appId, code) => {
+  dialogVisible.value = true
+  formLoading.value = true
+  resetForm(appId, code)
+  // 加载数据
+  try {
+    const data = await ChannelApi.getChannel(appId, code)
+    if (data && data.id) {
+      formData.value = data
+      formData.value.config = JSON.parse(data.config)
+    }
+    dialogTitle.value = !formData.value.id ? '创建支付渠道' : '编辑支付渠道'
+  } finally {
+    formLoading.value = false
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = { ...formData.value } as unknown as ChannelApi.ChannelVO
+    data.config = JSON.stringify(formData.value.config)
+    if (!data.id) {
+      await ChannelApi.createChannel(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ChannelApi.updateChannel(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = (appId, code) => {
+  formData.value = {
+    appId: appId,
+    code: code,
+    status: CommonStatusEnum.ENABLE,
+    remark: '',
+    feeRate: null,
+    config: {
+      appId: '',
+      serverUrl: null,
+      signType: 'RSA2',
+      mode: null,
+      privateKey: '',
+      alipayPublicKey: '',
+      appCertContent: '',
+      alipayPublicCertContent: '',
+      rootCertContent: ''
+    }
+  }
+  formRef.value?.resetFields()
+}
+
+const fileBeforeUpload = (file) => {
+  let format = '.' + file.name.split('.')[1]
+  if (format !== fileAccept) {
+    message.error(`请上传指定格式"${fileAccept}"文件`)
+    return false
+  }
+  let isRightSize = file.size / 1024 / 1024 < 2
+  if (!isRightSize) {
+    message.error('文件大小超过 2MB')
+  }
+  return isRightSize
+}
+
+const appCertUpload = (event) => {
+  const readFile = new FileReader()
+  readFile.onload = (e: any) => {
+    formData.value.config.appCertContent = e.target.result
+  }
+  readFile.readAsText(event.file)
+}
+
+const alipayPublicCertUpload = (event) => {
+  const readFile = new FileReader()
+  readFile.onload = (e: any) => {
+    formData.value.config.alipayPublicCertContent = e.target.result
+  }
+  readFile.readAsText(event.file)
+}
+
+const rootCertUpload = (event) => {
+  const readFile = new FileReader()
+  readFile.onload = (e: any) => {
+    formData.value.config.rootCertContent = e.target.result
+  }
+  readFile.readAsText(event.file)
+}
+</script>

+ 86 - 0
src/views/system/mail/account/account.data.ts

@@ -0,0 +1,86 @@
+import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
+import { dateFormatter } from '@/utils/formatTime'
+const { t } = useI18n() // 国际化
+
+// 表单校验
+export const rules = reactive({
+  mail: [
+    { required: true, message: t('profile.rules.mail'), trigger: 'blur' },
+    {
+      type: 'email',
+      message: t('profile.rules.truemail'),
+      trigger: ['blur', 'change']
+    }
+  ],
+  username: [required],
+  password: [required],
+  host: [required],
+  port: [required],
+  sslEnable: [required],
+  starttlsEnable: [required]
+})
+
+// CrudSchema:https://doc.iocoder.cn/vue3/crud-schema/
+const crudSchemas = reactive<CrudSchema[]>([
+  {
+    label: '邮箱',
+    field: 'mail',
+    isSearch: true
+  },
+  {
+    label: '用户名',
+    field: 'username',
+    isSearch: true
+  },
+  {
+    label: '密码',
+    field: 'password',
+    isTable: false
+  },
+  {
+    label: 'SMTP 服务器域名',
+    field: 'host'
+  },
+  {
+    label: 'SMTP 服务器端口',
+    field: 'port',
+    form: {
+      component: 'InputNumber',
+      value: 465
+    }
+  },
+  {
+    label: '是否开启 SSL',
+    field: 'sslEnable',
+    dictType: DICT_TYPE.INFRA_BOOLEAN_STRING,
+    dictClass: 'boolean',
+    form: {
+      component: 'Radio'
+    }
+  },
+  {
+    label: '是否开启 STARTTLS',
+    field: 'starttlsEnable',
+    dictType: DICT_TYPE.INFRA_BOOLEAN_STRING,
+    dictClass: 'boolean',
+    form: {
+      component: 'Radio'
+    }
+  },
+  {
+    label: '创建时间',
+    field: 'createTime',
+    isForm: false,
+    formatter: dateFormatter,
+    detail: {
+      dateFormat: 'YYYY-MM-DD HH:mm:ss'
+    }
+  },
+  {
+    label: '操作',
+    field: 'action',
+    isForm: false,
+    isDetail: false
+  }
+])
+export const { allSchemas } = useCrudSchemas(crudSchemas)

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff