ydmyzx 5 månader sedan
förälder
incheckning
a372e34126
26 ändrade filer med 4042 tillägg och 0 borttagningar
  1. 58 0
      src/api/crm/statistics/funnel.ts
  2. 307 0
      src/components/Form/src/Form.vue
  3. 1217 0
      src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
  4. 83 0
      src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/flowable/flowableExtension.js
  5. 191 0
      src/components/bpmnProcessDesigner/package/penal/flow-condition/FlowCondition.vue
  6. 24 0
      src/layout/components/Footer/src/Footer.vue
  7. BIN
      src/styles/FormCreate/fonts/fontello.woff
  8. 44 0
      src/types/form.d.ts
  9. 57 0
      src/utils/formCreate.ts
  10. 7 0
      src/utils/formRules.ts
  11. 332 0
      src/utils/formatTime.ts
  12. 7 0
      src/utils/formatter.ts
  13. 188 0
      src/views/crm/followup/FollowUpRecordForm.vue
  14. 42 0
      src/views/crm/followup/components/FollowUpRecordBusinessForm.vue
  15. 47 0
      src/views/crm/followup/components/FollowUpRecordContactForm.vue
  16. 149 0
      src/views/crm/statistics/funnel/components/FunnelBusiness.vue
  17. 98 0
      src/views/crm/statistics/rank/components/FollowCountRank.vue
  18. 98 0
      src/views/crm/statistics/rank/components/FollowCustomerCountRank.vue
  19. 201 0
      src/views/gislayer/gisform/GisFormForm.vue
  20. 385 0
      src/views/infra/codegen/components/GenerateInfoForm.vue
  21. 121 0
      src/views/layer/formvector/FormVectorForm.vue
  22. 130 0
      src/views/layer/formvectorattributes/FormVectorAttributesForm.vue
  23. 212 0
      src/views/layer/gisname/GisNameForm.vue
  24. 44 0
      src/views/mall/promotion/coupon/formatter.ts
  25. BIN
      src/views/mall/promotion/kefu/components/asserts/ganga.png
  26. BIN
      src/views/mall/promotion/kefu/components/asserts/ganmao.png

+ 58 - 0
src/api/crm/statistics/funnel.ts

@@ -0,0 +1,58 @@
+import request from '@/config/axios'
+
+export interface CrmStatisticFunnelRespVO {
+  customerCount: number // 客户数
+  businessCount: number // 商机数
+  businessWinCount: number // 赢单数
+}
+
+export interface CrmStatisticsBusinessSummaryByDateRespVO {
+  time: string // 时间
+  businessCreateCount: number // 商机数
+  totalPrice: number | string // 商机金额
+}
+
+export interface CrmStatisticsBusinessInversionRateSummaryByDateRespVO {
+  time: string // 时间
+  businessCount: number // 商机数量
+  businessWinCount: number // 赢单商机数
+}
+
+// 客户分析 API
+export const StatisticFunnelApi = {
+  // 1. 获取销售漏斗统计数据
+  getFunnelSummary: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-funnel/get-funnel-summary',
+      params
+    })
+  },
+  // 2. 获取商机结束状态统计
+  getBusinessSummaryByEndStatus: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-funnel/get-business-summary-by-end-status',
+      params
+    })
+  },
+  // 3. 获取新增商机分析(按日期)
+  getBusinessSummaryByDate: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-funnel/get-business-summary-by-date',
+      params
+    })
+  },
+  // 4. 获取商机转化率分析(按日期)
+  getBusinessInversionRateSummaryByDate: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-funnel/get-business-inversion-rate-summary-by-date',
+      params
+    })
+  },
+  // 5. 获取商机列表(按日期)
+  getBusinessPageByDate: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-funnel/get-business-page-by-date',
+      params
+    })
+  }
+}

+ 307 - 0
src/components/Form/src/Form.vue

@@ -0,0 +1,307 @@
+<script lang="tsx">
+import { computed, defineComponent, onMounted, PropType, ref, unref, watch } from 'vue'
+import { ElCol, ElForm, ElFormItem, ElRow, ElTooltip } from 'element-plus'
+import { componentMap } from './componentMap'
+import { propTypes } from '@/utils/propTypes'
+import { getSlot } from '@/utils/tsxHelper'
+import {
+  initModel,
+  setComponentProps,
+  setFormItemSlots,
+  setGridProp,
+  setItemComponentSlots,
+  setTextPlaceholder
+} from './helper'
+import { useRenderSelect } from './components/useRenderSelect'
+import { useRenderRadio } from './components/useRenderRadio'
+import { useRenderCheckbox } from './components/useRenderCheckbox'
+import { useDesign } from '@/hooks/web/useDesign'
+import { findIndex } from '@/utils'
+import { set } from 'lodash-es'
+import { FormProps } from './types'
+import { Icon } from '@/components/Icon'
+import { FormSchema, FormSetPropsType } from '@/types/form'
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('form')
+
+export default defineComponent({
+  // eslint-disable-next-line vue/no-reserved-component-names
+  name: 'Form',
+  props: {
+    // 生成Form的布局结构数组
+    schema: {
+      type: Array as PropType<FormSchema[]>,
+      default: () => []
+    },
+    // 是否需要栅格布局
+    // update by 芋艿:将 true 改成 false,因为项目更常用这种方式
+    isCol: propTypes.bool.def(false),
+    // 表单数据对象
+    model: {
+      type: Object as PropType<Recordable>,
+      default: () => ({})
+    },
+    // 是否自动设置placeholder
+    autoSetPlaceholder: propTypes.bool.def(true),
+    // 是否自定义内容
+    isCustom: propTypes.bool.def(false),
+    // 表单label宽度
+    labelWidth: propTypes.oneOfType([String, Number]).def('auto'),
+    // 是否 loading 数据中 add by 芋艿
+    vLoading: propTypes.bool.def(false)
+  },
+  emits: ['register'],
+  setup(props, { slots, expose, emit }) {
+    // element form 实例
+    const elFormRef = ref<ComponentRef<typeof ElForm>>()
+
+    // useForm传入的props
+    const outsideProps = ref<FormProps>({})
+
+    const mergeProps = ref<FormProps>({})
+
+    const getProps = computed(() => {
+      const propsObj = { ...props }
+      Object.assign(propsObj, unref(mergeProps))
+      return propsObj
+    })
+
+    // 表单数据
+    const formModel = ref<Recordable>({})
+
+    onMounted(() => {
+      emit('register', unref(elFormRef)?.$parent, unref(elFormRef))
+    })
+
+    // 对表单赋值
+    const setValues = (data: Recordable = {}) => {
+      formModel.value = Object.assign(unref(formModel), data)
+    }
+
+    const setProps = (props: FormProps = {}) => {
+      mergeProps.value = Object.assign(unref(mergeProps), props)
+      outsideProps.value = props
+    }
+
+    const delSchema = (field: string) => {
+      const { schema } = unref(getProps)
+
+      const index = findIndex(schema, (v: FormSchema) => v.field === field)
+      if (index > -1) {
+        schema.splice(index, 1)
+      }
+    }
+
+    const addSchema = (formSchema: FormSchema, index?: number) => {
+      const { schema } = unref(getProps)
+      if (index !== void 0) {
+        schema.splice(index, 0, formSchema)
+        return
+      }
+      schema.push(formSchema)
+    }
+
+    const setSchema = (schemaProps: FormSetPropsType[]) => {
+      const { schema } = unref(getProps)
+      for (const v of schema) {
+        for (const item of schemaProps) {
+          if (v.field === item.field) {
+            set(v, item.path, item.value)
+          }
+        }
+      }
+    }
+
+    const getElFormRef = (): ComponentRef<typeof ElForm> => {
+      return unref(elFormRef) as ComponentRef<typeof ElForm>
+    }
+
+    expose({
+      setValues,
+      formModel,
+      setProps,
+      delSchema,
+      addSchema,
+      setSchema,
+      getElFormRef
+    })
+
+    // 监听表单结构化数组,重新生成formModel
+    watch(
+      () => unref(getProps).schema,
+      (schema = []) => {
+        formModel.value = initModel(schema, unref(formModel))
+      },
+      {
+        immediate: true,
+        deep: true
+      }
+    )
+
+    // 渲染包裹标签,是否使用栅格布局
+    const renderWrap = () => {
+      const { isCol } = unref(getProps)
+      const content = isCol ? (
+        <ElRow gutter={20}>{renderFormItemWrap()}</ElRow>
+      ) : (
+        renderFormItemWrap()
+      )
+      return content
+    }
+
+    // 是否要渲染el-col
+    const renderFormItemWrap = () => {
+      // hidden属性表示隐藏,不做渲染
+      const { schema = [], isCol } = unref(getProps)
+
+      return schema
+        .filter((v) => !v.hidden)
+        .map((item) => {
+          // 如果是 Divider 组件,需要自己占用一行
+          const isDivider = item.component === 'Divider'
+          const Com = componentMap['Divider'] as ReturnType<typeof defineComponent>
+          return isDivider ? (
+            <Com {...{ contentPosition: 'left', ...item.componentProps }}>{item?.label}</Com>
+          ) : isCol ? (
+            // 如果需要栅格,需要包裹 ElCol
+            <ElCol {...setGridProp(item.colProps)}>{renderFormItem(item)}</ElCol>
+          ) : (
+            renderFormItem(item)
+          )
+        })
+    }
+
+    // 渲染formItem
+    const renderFormItem = (item: FormSchema) => {
+      // 单独给只有options属性的组件做判断
+      const notRenderOptions = ['SelectV2', 'Cascader', 'Transfer']
+      const slotsMap: Recordable = {
+        ...setItemComponentSlots(slots, item?.componentProps?.slots, item.field)
+      }
+      if (
+        item?.component !== 'SelectV2' &&
+        item?.component !== 'Cascader' &&
+        item?.componentProps?.options
+      ) {
+        slotsMap.default = () => renderOptions(item)
+      }
+
+      const formItemSlots: Recordable = setFormItemSlots(slots, item.field)
+      // 如果有 labelMessage,自动使用插槽渲染
+      if (item?.labelMessage) {
+        formItemSlots.label = () => {
+          return (
+            <>
+              <span>{item.label}</span>
+              <ElTooltip placement="right" raw-content>
+                {{
+                  content: () => <span v-dompurify-html={item.labelMessage}></span>,
+                  default: () => (
+                    <Icon
+                      icon="ep:warning"
+                      size={16}
+                      color="var(--el-color-primary)"
+                      class="relative top-1px ml-2px"
+                    ></Icon>
+                  )
+                }}
+              </ElTooltip>
+            </>
+          )
+        }
+      }
+      return (
+        <ElFormItem {...(item.formItemProps || {})} prop={item.field} label={item.label || ''}>
+          {{
+            ...formItemSlots,
+            default: () => {
+              const Com = componentMap[item.component as string] as ReturnType<
+                typeof defineComponent
+              >
+
+              const { autoSetPlaceholder } = unref(getProps)
+
+              return slots[item.field] ? (
+                getSlot(slots, item.field, formModel.value)
+              ) : (
+                <Com
+                  vModel={formModel.value[item.field]}
+                  {...(autoSetPlaceholder && setTextPlaceholder(item))}
+                  {...setComponentProps(item)}
+                  style={item.componentProps?.style}
+                  {...(notRenderOptions.includes(item?.component as string) &&
+                  item?.componentProps?.options
+                    ? { options: item?.componentProps?.options || [] }
+                    : {})}
+                >
+                  {{ ...slotsMap }}
+                </Com>
+              )
+            }
+          }}
+        </ElFormItem>
+      )
+    }
+
+    // 渲染options
+    const renderOptions = (item: FormSchema) => {
+      switch (item.component) {
+        case 'Select':
+        case 'SelectV2':
+          const { renderSelectOptions } = useRenderSelect(slots)
+          return renderSelectOptions(item)
+        case 'Radio':
+        case 'RadioButton':
+          const { renderRadioOptions } = useRenderRadio()
+          return renderRadioOptions(item)
+        case 'Checkbox':
+        case 'CheckboxButton':
+          const { renderCheckboxOptions } = useRenderCheckbox()
+          return renderCheckboxOptions(item)
+        default:
+          break
+      }
+    }
+
+    // 过滤传入Form组件的属性
+    const getFormBindValue = () => {
+      // 避免在标签上出现多余的属性
+      const delKeys = ['schema', 'isCol', 'autoSetPlaceholder', 'isCustom', 'model']
+      const props = { ...unref(getProps) }
+      for (const key in props) {
+        if (delKeys.indexOf(key) !== -1) {
+          delete props[key]
+        }
+      }
+      return props
+    }
+
+    return () => (
+      <ElForm
+        ref={elFormRef}
+        {...getFormBindValue()}
+        model={props.isCustom ? props.model : formModel}
+        class={prefixCls}
+        v-loading={props.vLoading}
+      >
+        {{
+          // 如果需要自定义,就什么都不渲染,而是提供默认插槽
+          default: () => {
+            const { isCustom } = unref(getProps)
+            return isCustom ? getSlot(slots, 'default') : renderWrap()
+          }
+        }}
+      </ElForm>
+    )
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.#{$elNamespace}-form.#{$namespace}-form .#{$elNamespace}-row {
+  margin-right: 0 !important;
+  margin-left: 0 !important;
+}
+</style>

+ 1217 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json

@@ -0,0 +1,1217 @@
+{
+  "name": "Flowable",
+  "uri": "http://flowable.org/bpmn",
+  "prefix": "flowable",
+  "xml": {
+    "tagAlias": "lowerCase"
+  },
+  "associations": [],
+  "types": [
+    {
+      "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", "flowable: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": "flowable: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": "formType",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "formReadOnly",
+          "isAttr": true,
+          "type": "Boolean",
+          "default": false
+        },
+        {
+          "name": "formInit",
+          "isAttr": true,
+          "type": "Boolean",
+          "default": true
+        }
+      ]
+    },
+    {
+      "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": "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": "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": "candidateStrategy",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "candidateParam",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "Assignee",
+      "supperClass": "Element",
+      "meta": {
+        "allowedIn": ["*"]
+      },
+      "properties": [
+        {
+          "name": "label",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "viewId",
+          "type": "Number",
+          "isAttr": true
+        }
+      ]
+    },
+    {
+      "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": ["flowable:ServiceTaskLike"],
+      "properties": [
+        {
+          "name": "type",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "topic",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "TaskPriorized",
+      "extends": ["bpmn:Process", "flowable: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": "Button",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "id",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "name",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "code",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "isHide",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "next",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "sort",
+          "type": "Integer",
+          "isAttr": true
+        }
+      ]
+    },
+    {
+      "name": "Assignee",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "id",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "type",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "value",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "condition",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "operationType",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "sort",
+          "type": "Integer",
+          "isAttr": true
+        }
+      ]
+    },
+    {
+      "name": "Connector",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["flowable:ServiceTaskLike"]
+      },
+      "properties": [
+        {
+          "name": "inputOutput",
+          "type": "InputOutput"
+        },
+        {
+          "name": "connectorId",
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "InputOutput",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:FlowNode", "flowable: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": [
+          "flowable:ServiceTaskLike",
+          "flowable:ExecutionListener",
+          "flowable:TaskListener"
+        ]
+      },
+      "properties": [
+        {
+          "name": "name",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "expression",
+          "type": "String"
+        },
+        {
+          "name": "stringValue",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "string",
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "ChildField",
+      "superClass": ["Element"],
+      "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": "InputParameter",
+      "superClass": ["InputOutputParameter"]
+    },
+    {
+      "name": "OutputParameter",
+      "superClass": ["InputOutputParameter"]
+    },
+    {
+      "name": "Collectable",
+      "isAbstract": true,
+      "extends": ["bpmn:MultiInstanceLoopCharacteristics"],
+      "superClass": ["flowable:AsyncCapable"],
+      "properties": [
+        {
+          "name": "collection",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "elementVariable",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "FailedJobRetryTimeCycle",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["flowable: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": "children",
+          "type": "ChildField",
+          "isMany": true
+        },
+        {
+          "name": "extensionElements",
+          "type": "bpmn:ExtensionElements",
+          "isMany": true
+        }
+      ]
+    },
+    {
+      "name": "FormData",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "fields",
+          "type": "FormField",
+          "isMany": true
+        },
+        {
+          "name": "businessKey",
+          "type": "String",
+          "isAttr": true
+        }
+      ]
+    },
+    {
+      "name": "FormField",
+      "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"
+        }
+      ]
+    },
+    {
+      "name": "Condition",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:SequenceFlow"]
+      },
+      "properties": [
+        {
+          "name": "id",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "field",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "compare",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "value",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "logic",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "sort",
+          "type": "Integer",
+          "isAttr": true
+        }
+      ]
+    }
+  ],
+  "emumerations": []
+}

+ 83 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/flowable/flowableExtension.js

@@ -0,0 +1,83 @@
+'use strict'
+
+import { some } from 'min-dash'
+
+// const some = some
+// const some = require('min-dash').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(/flowable:/, '')]
+
+  return name === propName && anyType(newElement, types)
+}
+
+function FlowableModdleExtension(eventBus) {
+  eventBus.on(
+    'property.clone',
+    function (context) {
+      const newElement = context.newElement,
+        propDescriptor = context.propertyDescriptor
+
+      this.canCloneProperty(newElement, propDescriptor)
+    },
+    this
+  )
+}
+
+FlowableModdleExtension.$inject = ['eventBus']
+
+FlowableModdleExtension.prototype.canCloneProperty = function (newElement, propDescriptor) {
+  if (isAllowed('flowable:FailedJobRetryTimeCycle', propDescriptor, newElement)) {
+    return (
+      includesType(newElement.eventDefinitions, 'bpmn:TimerEventDefinition') ||
+      includesType(newElement.eventDefinitions, 'bpmn:SignalEventDefinition') ||
+      is(newElement.loopCharacteristics, 'bpmn:MultiInstanceLoopCharacteristics')
+    )
+  }
+
+  if (isAllowed('flowable:Connector', propDescriptor, newElement)) {
+    return includesType(newElement.eventDefinitions, 'bpmn:MessageEventDefinition')
+  }
+
+  if (isAllowed('flowable:Field', propDescriptor, newElement)) {
+    return includesType(newElement.eventDefinitions, 'bpmn:MessageEventDefinition')
+  }
+}
+
+// module.exports = FlowableModdleExtension;
+export default FlowableModdleExtension

+ 191 - 0
src/components/bpmnProcessDesigner/package/penal/flow-condition/FlowCondition.vue

@@ -0,0 +1,191 @@
+<template>
+  <div class="panel-tab__content">
+    <el-form :model="flowConditionForm" label-width="90px" size="small">
+      <el-form-item label="流转类型">
+        <el-select v-model="flowConditionForm.type" @change="updateFlowType">
+          <el-option label="普通流转路径" value="normal" />
+          <el-option label="默认流转路径" value="default" />
+          <el-option label="条件流转路径" value="condition" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="条件格式" v-if="flowConditionForm.type === 'condition'" key="condition">
+        <el-select v-model="flowConditionForm.conditionType">
+          <el-option label="表达式" value="expression" />
+          <el-option label="脚本" value="script" />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        label="表达式"
+        v-if="flowConditionForm.conditionType && flowConditionForm.conditionType === 'expression'"
+        key="express"
+      >
+        <el-input
+          v-model="flowConditionForm.body"
+          style="width: 192px"
+          clearable
+          @change="updateFlowCondition"
+        />
+      </el-form-item>
+      <template
+        v-if="flowConditionForm.conditionType && flowConditionForm.conditionType === 'script'"
+      >
+        <el-form-item label="脚本语言" key="language">
+          <el-input v-model="flowConditionForm.language" clearable @change="updateFlowCondition" />
+        </el-form-item>
+        <el-form-item label="脚本类型" key="scriptType">
+          <el-select v-model="flowConditionForm.scriptType">
+            <el-option label="内联脚本" value="inlineScript" />
+            <el-option label="外部脚本" value="externalScript" />
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          label="脚本"
+          v-if="flowConditionForm.scriptType === 'inlineScript'"
+          key="body"
+        >
+          <el-input
+            v-model="flowConditionForm.body"
+            type="textarea"
+            clearable
+            @change="updateFlowCondition"
+          />
+        </el-form-item>
+        <el-form-item
+          label="资源地址"
+          v-if="flowConditionForm.scriptType === 'externalScript'"
+          key="resource"
+        >
+          <el-input v-model="flowConditionForm.resource" clearable @change="updateFlowCondition" />
+        </el-form-item>
+      </template>
+    </el-form>
+  </div>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'FlowCondition' })
+
+const props = defineProps({
+  businessObject: Object,
+  type: String
+})
+const flowConditionForm = ref<any>({})
+const bpmnElement = ref()
+const bpmnElementSource = ref()
+const bpmnElementSourceRef = ref()
+const flowConditionRef = ref()
+const bpmnInstances = () => (window as any)?.bpmnInstances
+const resetFlowCondition = () => {
+  bpmnElement.value = bpmnInstances().bpmnElement
+  bpmnElementSource.value = bpmnElement.value.source
+  bpmnElementSourceRef.value = bpmnElement.value.businessObject.sourceRef
+  // 初始化默认type为default
+  flowConditionForm.value = { type: 'default' }
+  if (
+    bpmnElementSourceRef.value &&
+    bpmnElementSourceRef.value.default &&
+    bpmnElementSourceRef.value.default.id === bpmnElement.value.id
+  ) {
+    flowConditionForm.value = { type: 'default' }
+  } else if (!bpmnElement.value.businessObject.conditionExpression) {
+    // 普通
+    flowConditionForm.value = { type: 'normal' }
+  } else {
+    // 带条件
+    const conditionExpression = bpmnElement.value.businessObject.conditionExpression
+    flowConditionForm.value = { ...conditionExpression, type: 'condition' }
+    // resource 可直接标识 是否是外部资源脚本
+    if (flowConditionForm.value.resource) {
+      // this.$set(this.flowConditionForm, "conditionType", "script");
+      // this.$set(this.flowConditionForm, "scriptType", "externalScript");
+      flowConditionForm.value['conditionType'] = 'script'
+      flowConditionForm.value['scriptType'] = 'externalScript'
+      return
+    }
+    if (conditionExpression.language) {
+      // this.$set(this.flowConditionForm, "conditionType", "script");
+      // this.$set(this.flowConditionForm, "scriptType", "inlineScript");
+      flowConditionForm.value['conditionType'] = 'script'
+      flowConditionForm.value['scriptType'] = 'inlineScript'
+
+      return
+    }
+    // this.$set(this.flowConditionForm, "conditionType", "expression");
+    flowConditionForm.value['conditionType'] = 'expression'
+  }
+}
+const updateFlowType = (flowType) => {
+  // 正常条件类
+  if (flowType === 'condition') {
+    flowConditionRef.value = bpmnInstances().moddle.create('bpmn:FormalExpression')
+    bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+      conditionExpression: flowConditionRef.value
+    })
+    return
+  }
+  // 默认路径
+  if (flowType === 'default') {
+    bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+      conditionExpression: null
+    })
+    bpmnInstances().modeling.updateProperties(toRaw(bpmnElementSource.value), {
+      default: toRaw(bpmnElement.value)
+    })
+    return
+  }
+  // 正常路径,如果来源节点的默认路径是当前连线时,清除父元素的默认路径配置
+  if (
+    bpmnElementSourceRef.value.default &&
+    bpmnElementSourceRef.value.default.id === bpmnElement.value.id
+  ) {
+    bpmnInstances().modeling.updateProperties(toRaw(bpmnElementSource.value), {
+      default: null
+    })
+  }
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+    conditionExpression: null
+  })
+}
+const updateFlowCondition = () => {
+  let { conditionType, scriptType, body, resource, language } = flowConditionForm.value
+  let condition
+  if (conditionType === 'expression') {
+    condition = bpmnInstances().moddle.create('bpmn:FormalExpression', { body })
+  } else {
+    if (scriptType === 'inlineScript') {
+      condition = bpmnInstances().moddle.create('bpmn:FormalExpression', { body, language })
+      // this.$set(this.flowConditionForm, "resource", "");
+      flowConditionForm.value['resource'] = ''
+    } else {
+      // this.$set(this.flowConditionForm, "body", "");
+      flowConditionForm.value['body'] = ''
+      condition = bpmnInstances().moddle.create('bpmn:FormalExpression', {
+        resource,
+        language
+      })
+    }
+  }
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+    conditionExpression: condition
+  })
+}
+
+onBeforeUnmount(() => {
+  bpmnElement.value = null
+  bpmnElementSource.value = null
+  bpmnElementSourceRef.value = null
+})
+
+watch(
+  () => props.businessObject,
+  (val) => {
+    console.log(val, 'val')
+    nextTick(() => {
+      resetFlowCondition()
+    })
+  },
+  {
+    immediate: true
+  }
+)
+</script>

+ 24 - 0
src/layout/components/Footer/src/Footer.vue

@@ -0,0 +1,24 @@
+<script lang="ts" setup>
+import { useAppStore } from '@/store/modules/app'
+import { useDesign } from '@/hooks/web/useDesign'
+
+// eslint-disable-next-line vue/no-reserved-component-names
+defineOptions({ name: 'Footer' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('footer')
+
+const appStore = useAppStore()
+
+const title = computed(() => appStore.getTitle)
+</script>
+
+<template>
+  <div
+    :class="prefixCls"
+    class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)]"
+  >
+    <span class="text-14px">Copyright ©2024-{{ title }}</span>
+  </div>
+</template>

BIN
src/styles/FormCreate/fonts/fontello.woff


+ 44 - 0
src/types/form.d.ts

@@ -0,0 +1,44 @@
+import type { CSSProperties } from 'vue'
+import { ColProps, ComponentProps, ComponentName } from '@/types/components'
+import type { AxiosPromise } from 'axios'
+
+export type FormSetPropsType = {
+  field: string
+  path: string
+  value: any
+}
+
+export type FormValueType = string | number | string[] | number[] | boolean | undefined | null
+
+export type FormItemProps = {
+  labelWidth?: string | number
+  required?: boolean
+  rules?: Recordable
+  error?: string
+  showMessage?: boolean
+  inlineMessage?: boolean
+  style?: CSSProperties
+}
+
+export type FormSchema = {
+  // 唯一值
+  field: string
+  // 标题
+  label?: string
+  // 提示
+  labelMessage?: string
+  // col组件属性
+  colProps?: ColProps
+  // 表单组件属性,slots对应的是表单组件的插槽,规则:${field}-xxx,具体可以查看element-plus文档
+  componentProps?: { slots?: Recordable } & ComponentProps
+  // formItem组件属性
+  formItemProps?: FormItemProps
+  // 渲染的组件
+  component?: ComponentName
+  // 初始值
+  value?: FormValueType
+  // 是否隐藏
+  hidden?: boolean
+  // 远程加载下拉项
+  api?: <T = any>() => AxiosPromise<T>
+}

+ 57 - 0
src/utils/formCreate.ts

@@ -0,0 +1,57 @@
+/**
+ * 针对 https://github.com/xaboy/form-create-designer 封装的工具类
+ */
+
+// 编码表单 Conf
+export const encodeConf = (designerRef: object) => {
+  // @ts-ignore
+  return JSON.stringify(designerRef.value.getOption())
+}
+
+// 编码表单 Fields
+export const encodeFields = (designerRef: object) => {
+  // @ts-ignore
+  const rule = designerRef.value.getRule()
+  const fields: string[] = []
+  rule.forEach((item) => {
+    fields.push(JSON.stringify(item))
+  })
+  return fields
+}
+
+// 解码表单 Fields
+export const decodeFields = (fields: string[]) => {
+  const rule: object[] = []
+  fields.forEach((item) => {
+    rule.push(JSON.parse(item))
+  })
+  return rule
+}
+
+// 设置表单的 Conf 和 Fields,适用 FcDesigner 场景
+export const setConfAndFields = (designerRef: object, conf: string, fields: string) => {
+  // @ts-ignore
+  designerRef.value.setOption(JSON.parse(conf))
+  // @ts-ignore
+  designerRef.value.setRule(decodeFields(fields))
+}
+
+// 设置表单的 Conf 和 Fields,适用 form-create 场景
+export const setConfAndFields2 = (
+  detailPreview: object,
+  conf: string,
+  fields: string[],
+  value?: object
+) => {
+  if (isRef(detailPreview)) {
+    detailPreview = detailPreview.value
+  }
+  // @ts-ignore
+  detailPreview.option = JSON.parse(conf)
+  // @ts-ignore
+  detailPreview.rule = decodeFields(fields)
+  if (value) {
+    // @ts-ignore
+    detailPreview.value = value
+  }
+}

+ 7 - 0
src/utils/formRules.ts

@@ -0,0 +1,7 @@
+const { t } = useI18n()
+
+// 必填项
+export const required = {
+  required: true,
+  message: t('common.required')
+}

+ 332 - 0
src/utils/formatTime.ts

@@ -0,0 +1,332 @@
+import dayjs from 'dayjs'
+import type { TableColumnCtx } from 'element-plus'
+
+/**
+ * 日期快捷选项适用于 el-date-picker
+ */
+export const defaultShortcuts = [
+  {
+    text: '今天',
+    value: () => {
+      return new Date()
+    }
+  },
+  {
+    text: '昨天',
+    value: () => {
+      const date = new Date()
+      date.setTime(date.getTime() - 3600 * 1000 * 24)
+      return [date, date]
+    }
+  },
+  {
+    text: '最近七天',
+    value: () => {
+      const date = new Date()
+      date.setTime(date.getTime() - 3600 * 1000 * 24 * 7)
+      return [date, new Date()]
+    }
+  },
+  {
+    text: '最近 30 天',
+    value: () => {
+      const date = new Date()
+      date.setTime(date.getTime() - 3600 * 1000 * 24 * 30)
+      return [date, new Date()]
+    }
+  },
+  {
+    text: '本月',
+    value: () => {
+      const date = new Date()
+      date.setDate(1) // 设置为当前月的第一天
+      return [date, new Date()]
+    }
+  },
+  {
+    text: '今年',
+    value: () => {
+      const date = new Date()
+      return [new Date(`${date.getFullYear()}-01-01`), date]
+    }
+  }
+]
+
+/**
+ * 时间日期转换
+ * @param date 当前时间,new Date() 格式
+ * @param format 需要转换的时间格式字符串
+ * @description format 字符串随意,如 `YYYY-mm、YYYY-mm-dd`
+ * @description format 季度:"YYYY-mm-dd HH:MM:SS QQQQ"
+ * @description format 星期:"YYYY-mm-dd HH:MM:SS WWW"
+ * @description format 几周:"YYYY-mm-dd HH:MM:SS ZZZ"
+ * @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ"
+ * @returns 返回拼接后的时间字符串
+ */
+export function formatDate(date: Date, format?: string): string {
+  // 日期不存在,则返回空
+  if (!date) {
+    return ''
+  }
+  // 日期存在,则进行格式化
+  return date ? dayjs(date).format(format ?? 'YYYY-MM-DD HH:mm:ss') : ''
+}
+
+/**
+ * 获取当前的日期+时间
+ */
+export function getNowDateTime() {
+  return dayjs()
+}
+
+/**
+ * 获取当前日期是第几周
+ * @param dateTime 当前传入的日期值
+ * @returns 返回第几周数字值
+ */
+export function getWeek(dateTime: Date): number {
+  const temptTime = new Date(dateTime.getTime())
+  // 周几
+  const weekday = temptTime.getDay() || 7
+  // 周1+5天=周六
+  temptTime.setDate(temptTime.getDate() - weekday + 1 + 5)
+  let firstDay = new Date(temptTime.getFullYear(), 0, 1)
+  const dayOfWeek = firstDay.getDay()
+  let spendDay = 1
+  if (dayOfWeek != 0) spendDay = 7 - dayOfWeek + 1
+  firstDay = new Date(temptTime.getFullYear(), 0, 1 + spendDay)
+  const d = Math.ceil((temptTime.valueOf() - firstDay.valueOf()) / 86400000)
+  return Math.ceil(d / 7)
+}
+
+/**
+ * 将时间转换为 `几秒前`、`几分钟前`、`几小时前`、`几天前`
+ * @param param 当前时间,new Date() 格式或者字符串时间格式
+ * @param format 需要转换的时间格式字符串
+ * @description param 10秒:  10 * 1000
+ * @description param 1分:   60 * 1000
+ * @description param 1小时: 60 * 60 * 1000
+ * @description param 24小时:60 * 60 * 24 * 1000
+ * @description param 3天:   60 * 60* 24 * 1000 * 3
+ * @returns 返回拼接后的时间字符串
+ */
+export function formatPast(param: string | Date, format = 'YYYY-mm-dd HH:MM:SS'): string {
+  // 传入格式处理、存储转换值
+  let t: any, s: number
+  // 获取js 时间戳
+  let time: number = new Date().getTime()
+  // 是否是对象
+  typeof param === 'string' || 'object' ? (t = new Date(param).getTime()) : (t = param)
+  // 当前时间戳 - 传入时间戳
+  time = Number.parseInt(`${time - t}`)
+  if (time < 10000) {
+    // 10秒内
+    return '刚刚'
+  } else if (time < 60000 && time >= 10000) {
+    // 超过10秒少于1分钟内
+    s = Math.floor(time / 1000)
+    return `${s}秒前`
+  } else if (time < 3600000 && time >= 60000) {
+    // 超过1分钟少于1小时
+    s = Math.floor(time / 60000)
+    return `${s}分钟前`
+  } else if (time < 86400000 && time >= 3600000) {
+    // 超过1小时少于24小时
+    s = Math.floor(time / 3600000)
+    return `${s}小时前`
+  } else if (time < 259200000 && time >= 86400000) {
+    // 超过1天少于3天内
+    s = Math.floor(time / 86400000)
+    return `${s}天前`
+  } else {
+    // 超过3天
+    const date = typeof param === 'string' || 'object' ? new Date(param) : param
+    return formatDate(date, format)
+  }
+}
+
+/**
+ * 时间问候语
+ * @param param 当前时间,new Date() 格式
+ * @description param 调用 `formatAxis(new Date())` 输出 `上午好`
+ * @returns 返回拼接后的时间字符串
+ */
+export function formatAxis(param: Date): string {
+  const hour: number = new Date(param).getHours()
+  if (hour < 6) return '凌晨好'
+  else if (hour < 9) return '早上好'
+  else if (hour < 12) return '上午好'
+  else if (hour < 14) return '中午好'
+  else if (hour < 17) return '下午好'
+  else if (hour < 19) return '傍晚好'
+  else if (hour < 22) return '晚上好'
+  else return '夜里好'
+}
+
+/**
+ * 将毫秒,转换成时间字符串。例如说,xx 分钟
+ *
+ * @param ms 毫秒
+ * @returns {string} 字符串
+ */
+export function formatPast2(ms: number): string {
+  const day = Math.floor(ms / (24 * 60 * 60 * 1000))
+  const hour = Math.floor(ms / (60 * 60 * 1000) - day * 24)
+  const minute = Math.floor(ms / (60 * 1000) - day * 24 * 60 - hour * 60)
+  const second = Math.floor(ms / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60)
+  if (day > 0) {
+    return day + ' 天' + hour + ' 小时 ' + minute + ' 分钟'
+  }
+  if (hour > 0) {
+    return hour + ' 小时 ' + minute + ' 分钟'
+  }
+  if (minute > 0) {
+    return minute + ' 分钟'
+  }
+  if (second > 0) {
+    return second + ' 秒'
+  } else {
+    return 0 + ' 秒'
+  }
+}
+
+/**
+ * element plus 的时间 Formatter 实现,使用 YYYY-MM-DD HH:mm:ss 格式
+ *
+ * @param row 行数据
+ * @param column 字段
+ * @param cellValue 字段值
+ */
+export function dateFormatter(_row: any, _column: TableColumnCtx<any>, cellValue: any): string {
+  return cellValue ? formatDate(cellValue) : ''
+}
+
+/**
+ * element plus 的时间 Formatter 实现,使用 YYYY-MM-DD 格式
+ *
+ * @param row 行数据
+ * @param column 字段
+ * @param cellValue 字段值
+ */
+export function dateFormatter2(_row: any, _column: TableColumnCtx<any>, cellValue: any): string {
+  return cellValue ? formatDate(cellValue, 'YYYY-MM-DD') : ''
+}
+
+/**
+ * 设置起始日期,时间为00:00:00
+ * @param param 传入日期
+ * @returns 带时间00:00:00的日期
+ */
+export function beginOfDay(param: Date): Date {
+  return new Date(param.getFullYear(), param.getMonth(), param.getDate(), 0, 0, 0)
+}
+
+/**
+ * 设置结束日期,时间为23:59:59
+ * @param param 传入日期
+ * @returns 带时间23:59:59的日期
+ */
+export function endOfDay(param: Date): Date {
+  return new Date(param.getFullYear(), param.getMonth(), param.getDate(), 23, 59, 59)
+}
+
+/**
+ * 计算两个日期间隔天数
+ * @param param1 日期1
+ * @param param2 日期2
+ */
+export function betweenDay(param1: Date, param2: Date): number {
+  param1 = convertDate(param1)
+  param2 = convertDate(param2)
+  // 计算差值
+  return Math.floor((param2.getTime() - param1.getTime()) / (24 * 3600 * 1000))
+}
+
+/**
+ * 日期计算
+ * @param param1 日期
+ * @param param2 添加的时间
+ */
+export function addTime(param1: Date, param2: number): Date {
+  param1 = convertDate(param1)
+  return new Date(param1.getTime() + param2)
+}
+
+/**
+ * 日期转换
+ * @param param 日期
+ */
+export function convertDate(param: Date | string): Date {
+  if (typeof param === 'string') {
+    return new Date(param)
+  }
+  return param
+}
+
+/**
+ * 指定的两个日期, 是否为同一天
+ * @param a 日期 A
+ * @param b 日期 B
+ */
+export function isSameDay(a: dayjs.ConfigType, b: dayjs.ConfigType): boolean {
+  if (!a || !b) return false
+
+  const aa = dayjs(a)
+  const bb = dayjs(b)
+  return aa.year() == bb.year() && aa.month() == bb.month() && aa.day() == bb.day()
+}
+
+/**
+ * 获取一天的开始时间、截止时间
+ * @param date 日期
+ * @param days 天数
+ */
+export function getDayRange(
+  date: dayjs.ConfigType,
+  days: number
+): [dayjs.ConfigType, dayjs.ConfigType] {
+  const day = dayjs(date).add(days, 'd')
+  return getDateRange(day, day)
+}
+
+/**
+ * 获取最近7天的开始时间、截止时间
+ */
+export function getLast7Days(): [dayjs.ConfigType, dayjs.ConfigType] {
+  const lastWeekDay = dayjs().subtract(7, 'd')
+  const yesterday = dayjs().subtract(1, 'd')
+  return getDateRange(lastWeekDay, yesterday)
+}
+
+/**
+ * 获取最近30天的开始时间、截止时间
+ */
+export function getLast30Days(): [dayjs.ConfigType, dayjs.ConfigType] {
+  const lastMonthDay = dayjs().subtract(30, 'd')
+  const yesterday = dayjs().subtract(1, 'd')
+  return getDateRange(lastMonthDay, yesterday)
+}
+
+/**
+ * 获取最近1年的开始时间、截止时间
+ */
+export function getLast1Year(): [dayjs.ConfigType, dayjs.ConfigType] {
+  const lastYearDay = dayjs().subtract(1, 'y')
+  const yesterday = dayjs().subtract(1, 'd')
+  return getDateRange(lastYearDay, yesterday)
+}
+
+/**
+ * 获取指定日期的开始时间、截止时间
+ * @param beginDate 开始日期
+ * @param endDate 截止日期
+ */
+export function getDateRange(
+  beginDate: dayjs.ConfigType,
+  endDate: dayjs.ConfigType
+): [string, string] {
+  return [
+    dayjs(beginDate).startOf('d').format('YYYY-MM-DD HH:mm:ss'),
+    dayjs(endDate).endOf('d').format('YYYY-MM-DD HH:mm:ss')
+  ]
+}

+ 7 - 0
src/utils/formatter.ts

@@ -0,0 +1,7 @@
+import { floatToFixed2 } from '@/utils'
+
+// 格式化金额【分转元】
+// @ts-ignore
+export const fenToYuanFormat = (_, __, cellValue: any, ___) => {
+  return `¥${floatToFixed2(cellValue)}`
+}

+ 188 - 0
src/views/crm/followup/FollowUpRecordForm.vue

@@ -0,0 +1,188 @@
+<!-- 跟进记录的添加表单弹窗 -->
+<template>
+  <Dialog v-model="dialogVisible" title="添加跟进记录" width="50%">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+    >
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="跟进类型" prop="type">
+            <el-select v-model="formData.type" placeholder="请选择跟进类型">
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.CRM_FOLLOW_UP_TYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="下次联系时间" prop="nextTime">
+            <el-date-picker
+              v-model="formData.nextTime"
+              placeholder="选择下次联系时间"
+              type="date"
+              value-format="x"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="跟进内容" prop="content">
+            <el-input v-model="formData.content" :rows="3" type="textarea" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="图片" prop="picUrls">
+            <UploadImgs v-model="formData.picUrls" class="min-w-80px" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="附件" prop="fileUrls">
+            <UploadFile v-model="formData.fileUrls" class="min-w-80px" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24" v-if="formData.bizType == BizTypeEnum.CRM_CUSTOMER">
+          <el-form-item label="关联联系人" prop="contactIds">
+            <el-button @click="handleOpenContact">
+              <Icon class="mr-5px" icon="ep:plus" />
+              添加联系人
+            </el-button>
+            <FollowUpRecordContactForm :contacts="formData.contacts" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24" v-if="formData.bizType == BizTypeEnum.CRM_CUSTOMER">
+          <el-form-item label="关联商机" prop="businessIds">
+            <el-button @click="handleOpenBusiness">
+              <Icon class="mr-5px" icon="ep:plus" />
+              添加商机
+            </el-button>
+            <FollowUpRecordBusinessForm :businesses="formData.businesses" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+
+  <!-- 弹窗 -->
+  <ContactListModal
+    ref="contactTableSelectRef"
+    :customer-id="formData.bizId"
+    @success="handleAddContact"
+  />
+  <BusinessListModal
+    ref="businessTableSelectRef"
+    :customer-id="formData.bizId"
+    @success="handleAddBusiness"
+  />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { FollowUpRecordApi, FollowUpRecordVO } from '@/api/crm/followup'
+import { BizTypeEnum } from '@/api/crm/permission'
+import FollowUpRecordBusinessForm from './components/FollowUpRecordBusinessForm.vue'
+import FollowUpRecordContactForm from './components/FollowUpRecordContactForm.vue'
+import BusinessListModal from '@/views/crm/business/components/BusinessListModal.vue'
+import * as BusinessApi from '@/api/crm/business'
+import ContactListModal from '@/views/crm/contact/components/ContactListModal.vue'
+import * as ContactApi from '@/api/crm/contact'
+
+defineOptions({ name: 'FollowUpRecordForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  bizType: undefined,
+  bizId: undefined,
+  businesses: [],
+  contacts: []
+})
+const formRules = reactive({
+  type: [{ required: true, message: '跟进类型不能为空', trigger: 'change' }],
+  content: [{ required: true, message: '跟进内容不能为空', trigger: 'blur' }],
+  nextTime: [{ required: true, message: '下次联系时间不能为空', trigger: 'blur' }]
+})
+
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (bizType: number, bizId: number) => {
+  dialogVisible.value = true
+  resetForm()
+  formData.value.bizType = bizType
+  formData.value.bizId = bizId
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = {
+      ...formData.value,
+      contactIds: formData.value.contacts.map((item) => item.id),
+      businessIds: formData.value.businesses.map((item) => item.id)
+    } as unknown as FollowUpRecordVO
+    await FollowUpRecordApi.createFollowUpRecord(data)
+    message.success(t('common.createSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 关联联系人 */
+const contactTableSelectRef = ref<InstanceType<typeof ContactListModal>>()
+const handleOpenContact = () => {
+  contactTableSelectRef.value?.open()
+}
+const handleAddContact = (contactId: [], newContacts: ContactApi.ContactVO[]) => {
+  newContacts.forEach((contact) => {
+    if (!formData.value.contacts.some((item) => item.id === contact.id)) {
+      formData.value.contacts.push(contact)
+    }
+  })
+}
+
+/** 关联商机 */
+const businessTableSelectRef = ref<InstanceType<typeof BusinessListModal>>()
+const handleOpenBusiness = () => {
+  businessTableSelectRef.value?.open()
+}
+const handleAddBusiness = (businessId: [], newBusinesses: BusinessApi.BusinessVO[]) => {
+  newBusinesses.forEach((business) => {
+    if (!formData.value.businesses.some((item) => item.id === business.id)) {
+      formData.value.businesses.push(business)
+    }
+  })
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formRef.value?.resetFields()
+  formData.value = {
+    bizId: undefined,
+    bizType: undefined,
+    businesses: [],
+    contacts: []
+  }
+}
+</script>

+ 42 - 0
src/views/crm/followup/components/FollowUpRecordBusinessForm.vue

@@ -0,0 +1,42 @@
+<template>
+  <el-table :data="formData" :show-overflow-tooltip="true" :stripe="true" height="120">
+    <el-table-column label="商机名称" fixed="left" align="center" prop="name" />
+    <el-table-column
+      label="商机金额"
+      align="center"
+      prop="totalPrice"
+      :formatter="erpPriceTableColumnFormatter"
+    />
+    <el-table-column label="客户名称" align="center" prop="customerName" />
+    <el-table-column label="商机组" align="center" prop="statusTypeName" />
+    <el-table-column label="商机阶段" align="center" prop="statusName" />
+    <el-table-column align="center" fixed="right" label="操作" width="80">
+      <template #default="{ $index }">
+        <el-button link type="danger" @click="handleDelete($index)"> 移除</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script lang="ts" setup>
+import { erpPriceTableColumnFormatter } from '@/utils'
+
+const props = defineProps<{
+  businesses: undefined
+}>()
+const formData = ref([])
+
+/** 初始化商机列表 */
+watch(
+  () => props.businesses,
+  async (val) => {
+    formData.value = val
+  },
+  { immediate: true }
+)
+
+/** 删除按钮操作 */
+const handleDelete = (index: number) => {
+  formData.value.splice(index, 1)
+}
+</script>

+ 47 - 0
src/views/crm/followup/components/FollowUpRecordContactForm.vue

@@ -0,0 +1,47 @@
+<template>
+  <el-table :data="contacts" :show-overflow-tooltip="true" :stripe="true" height="150">
+    <el-table-column label="姓名" fixed="left" align="center" prop="name">
+      <template #default="scope">
+        <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
+          {{ scope.row.name }}
+        </el-link>
+      </template>
+    </el-table-column>
+    <el-table-column label="手机号" align="center" prop="mobile" />
+    <el-table-column label="职位" align="center" prop="post" />
+    <el-table-column label="直属上级" align="center" prop="parentName" />
+    <el-table-column label="是否关键决策人" align="center" prop="master" min-width="100">
+      <template #default="scope">
+        <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
+      </template>
+    </el-table-column>
+    <el-table-column align="center" fixed="right" label="操作" width="130">
+      <template #default="scope">
+        <el-button link type="danger" @click="handleDelete(scope.row.id)"> 移除</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+
+const props = defineProps<{
+  contacts: undefined
+}>()
+const formData = ref([])
+
+/** 初始化联系人列表 */
+watch(
+  () => props.contacts,
+  async (val) => {
+    formData.value = val
+  },
+  { immediate: true }
+)
+
+/** 删除按钮操作 */
+const handleDelete = (index: number) => {
+  formData.value.splice(index, 1)
+}
+</script>

+ 149 - 0
src/views/crm/statistics/funnel/components/FunnelBusiness.vue

@@ -0,0 +1,149 @@
+<!-- 销售漏斗分析 -->
+<template>
+  <!-- Echarts图 -->
+  <el-card shadow="never">
+    <el-row>
+      <el-col :span="24">
+        <el-button-group class="mb-10px">
+          <el-button type="primary" @click="handleActive(true)">客户视角</el-button>
+          <el-button type="primary" @click="handleActive(false)">动态视角</el-button>
+        </el-button-group>
+        <el-skeleton :loading="loading" animated>
+          <Echart :height="500" :options="echartsOption" />
+        </el-skeleton>
+      </el-col>
+    </el-row>
+  </el-card>
+
+  <!-- 统计列表 -->
+  <el-card class="mt-16px" shadow="never">
+    <el-table v-loading="loading" :data="list">
+      <el-table-column align="center" label="序号" type="index" width="80" />
+      <el-table-column align="center" label="阶段" prop="endStatus" width="200">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.CRM_BUSINESS_END_STATUS_TYPE" :value="scope.row.endStatus" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="商机数" min-width="200" prop="businessCount" />
+      <el-table-column align="center" label="商机总金额(元)" min-width="200" prop="totalPrice" />
+    </el-table>
+  </el-card>
+</template>
+<script lang="ts" setup>
+import { CrmStatisticFunnelRespVO, StatisticFunnelApi } from '@/api/crm/statistics/funnel'
+import { EChartsOption } from 'echarts'
+import { DICT_TYPE } from '@/utils/dict'
+
+defineOptions({ name: 'FunnelBusiness' })
+const props = defineProps<{ queryParams: any }>() // 搜索参数
+
+const active = ref(true)
+const loading = ref(false) // 加载中
+const list = ref<CrmStatisticFunnelRespVO[]>([]) // 列表的数据
+
+/** 销售漏斗 */
+const echartsOption = reactive<EChartsOption>({
+  title: {
+    text: '销售漏斗'
+  },
+  tooltip: {
+    trigger: 'item',
+    formatter: '{a} <br/>{b}'
+  },
+  toolbox: {
+    feature: {
+      dataView: { readOnly: false },
+      restore: {},
+      saveAsImage: {}
+    }
+  },
+  legend: {
+    data: ['客户', '商机', '赢单']
+  },
+  series: [
+    {
+      name: '销售漏斗',
+      type: 'funnel',
+      left: '10%',
+      top: 60,
+      bottom: 60,
+      width: '80%',
+      min: 0,
+      max: 100,
+      minSize: '0%',
+      maxSize: '100%',
+      sort: 'descending',
+      gap: 2,
+      label: {
+        show: true,
+        position: 'inside'
+      },
+      labelLine: {
+        length: 10,
+        lineStyle: {
+          width: 1,
+          type: 'solid'
+        }
+      },
+      itemStyle: {
+        borderColor: '#fff',
+        borderWidth: 1
+      },
+      emphasis: {
+        label: {
+          fontSize: 20
+        }
+      },
+      data: [
+        { value: 60, name: '客户-0个' },
+        { value: 40, name: '商机-0个' },
+        { value: 20, name: '赢单-0个' }
+      ]
+    }
+  ]
+}) as EChartsOption
+
+const handleActive = async (val: boolean) => {
+  active.value = val
+  await loadData()
+}
+
+/** 获取统计数据 */
+const loadData = async () => {
+  loading.value = true
+  // 1. 加载漏斗数据
+  const data = (await StatisticFunnelApi.getFunnelSummary(
+    props.queryParams
+  )) as CrmStatisticFunnelRespVO
+  // 2.1 更新 Echarts 数据
+  if (
+    !!data &&
+    echartsOption.series &&
+    echartsOption.series[0] &&
+    echartsOption.series[0]['data']
+  ) {
+    // tips:写死 value 值是为了保持漏斗顺序不变
+    const list: { value: number; name: string }[] = []
+    if (active.value) {
+      list.push({ value: 60, name: `客户-${data.customerCount || 0}个` })
+      list.push({ value: 40, name: `商机-${data.businessCount || 0}个` })
+      list.push({ value: 20, name: `赢单-${data.businessWinCount || 0}个` })
+    } else {
+      list.push({ value: data.customerCount || 0, name: `客户-${data.customerCount || 0}个` })
+      list.push({ value: data.businessCount || 0, name: `商机-${data.businessCount || 0}个` })
+      list.push({ value: data.businessWinCount || 0, name: `赢单-${data.businessWinCount || 0}个` })
+    }
+
+    echartsOption.series[0]['data'] = list
+  }
+  // 2.2 获取商机结束状态统计
+  list.value = await StatisticFunnelApi.getBusinessSummaryByEndStatus(props.queryParams)
+  loading.value = false
+}
+defineExpose({ loadData })
+
+/** 初始化 */
+onMounted(() => {
+  loadData()
+})
+</script>

+ 98 - 0
src/views/crm/statistics/rank/components/FollowCountRank.vue

@@ -0,0 +1,98 @@
+<!-- 跟进次数排行 -->
+<template>
+  <!-- 柱状图 -->
+  <el-card shadow="never">
+    <el-skeleton :loading="loading" animated>
+      <Echart :height="500" :options="echartsOption" />
+    </el-skeleton>
+  </el-card>
+
+  <!-- 排行列表 -->
+  <el-card shadow="never" class="mt-16px">
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="公司排名" align="center" type="index" width="80" />
+      <el-table-column label="员工" align="center" prop="nickname" min-width="200" />
+      <el-table-column label="部门" align="center" prop="deptName" min-width="200" />
+      <el-table-column label="跟进次数(次)" align="center" prop="count" min-width="200" />
+    </el-table>
+  </el-card>
+</template>
+<script setup lang="ts">
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
+import { EChartsOption } from 'echarts'
+import { clone } from 'lodash-es'
+
+defineOptions({ name: 'FollowCountRank' })
+const props = defineProps<{ queryParams: any }>() // 搜索参数
+
+const loading = ref(false) // 加载中
+const list = ref<StatisticsRankRespVO[]>([]) // 列表的数据
+
+/** 柱状图配置:横向 */
+const echartsOption = reactive<EChartsOption>({
+  dataset: {
+    dimensions: ['nickname', 'count'],
+    source: []
+  },
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    containLabel: true
+  },
+  legend: {
+    top: 50
+  },
+  series: [
+    {
+      name: '跟进次数排行',
+      type: 'bar'
+    }
+  ],
+  toolbox: {
+    feature: {
+      dataZoom: {
+        yAxisIndex: false // 数据区域缩放:Y 轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: '跟进次数排行' } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  xAxis: {
+    type: 'value',
+    name: '跟进次数(次)'
+  },
+  yAxis: {
+    type: 'category',
+    name: '员工'
+  }
+}) as EChartsOption
+
+/** 获取跟进次数排行 */
+const loadData = async () => {
+  // 1. 加载排行数据
+  loading.value = true
+  const rankingList = await StatisticsRankApi.getFollowCountRank(props.queryParams)
+  // 2.1 更新 Echarts 数据
+  if (echartsOption.dataset && echartsOption.dataset['source']) {
+    echartsOption.dataset['source'] = clone(rankingList).reverse()
+  }
+  // 2.2 更新列表数据
+  list.value = rankingList
+  loading.value = false
+}
+defineExpose({ loadData })
+
+/** 初始化 */
+onMounted(() => {
+  loadData()
+})
+</script>

+ 98 - 0
src/views/crm/statistics/rank/components/FollowCustomerCountRank.vue

@@ -0,0 +1,98 @@
+<!-- 跟进客户数排行 -->
+<template>
+  <!-- 柱状图 -->
+  <el-card shadow="never">
+    <el-skeleton :loading="loading" animated>
+      <Echart :height="500" :options="echartsOption" />
+    </el-skeleton>
+  </el-card>
+
+  <!-- 排行列表 -->
+  <el-card shadow="never" class="mt-16px">
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="公司排名" align="center" type="index" width="80" />
+      <el-table-column label="员工" align="center" prop="nickname" min-width="200" />
+      <el-table-column label="部门" align="center" prop="deptName" min-width="200" />
+      <el-table-column label="跟进客户数(个)" align="center" prop="count" min-width="200" />
+    </el-table>
+  </el-card>
+</template>
+<script setup lang="ts">
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
+import { EChartsOption } from 'echarts'
+import { clone } from 'lodash-es'
+
+defineOptions({ name: 'FollowCustomerCountRank' })
+const props = defineProps<{ queryParams: any }>() // 搜索参数
+
+const loading = ref(false) // 加载中
+const list = ref<StatisticsRankRespVO[]>([]) // 列表的数据
+
+/** 柱状图配置:横向 */
+const echartsOption = reactive<EChartsOption>({
+  dataset: {
+    dimensions: ['nickname', 'count'],
+    source: []
+  },
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    containLabel: true
+  },
+  legend: {
+    top: 50
+  },
+  series: [
+    {
+      name: '跟进客户数排行',
+      type: 'bar'
+    }
+  ],
+  toolbox: {
+    feature: {
+      dataZoom: {
+        yAxisIndex: false // 数据区域缩放:Y 轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: '跟进客户数排行' } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  xAxis: {
+    type: 'value',
+    name: '跟进客户数(个)'
+  },
+  yAxis: {
+    type: 'category',
+    name: '员工'
+  }
+}) as EChartsOption
+
+/** 获取跟进客户数排行 */
+const loadData = async () => {
+  // 1. 加载排行数据
+  loading.value = true
+  const rankingList = await StatisticsRankApi.getFollowCustomerCountRank(props.queryParams)
+  // 2.1 更新 Echarts 数据
+  if (echartsOption.dataset && echartsOption.dataset['source']) {
+    echartsOption.dataset['source'] = clone(rankingList).reverse()
+  }
+  // 2.2 更新列表数据
+  list.value = rankingList
+  loading.value = false
+}
+defineExpose({ loadData })
+
+/** 初始化 */
+onMounted(() => {
+  loadData()
+})
+</script>

+ 201 - 0
src/views/gislayer/gisform/GisFormForm.vue

@@ -0,0 +1,201 @@
+<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="type">
+        <el-select v-model="formData.type" placeholder="请选择图层数据类型">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.LAYER_GISFROM_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+             />
+        </el-select>
+      </el-form-item>
+      <el-form-item v-if="[1,3,4,5,6].indexOf(formData.type) !=-1" label="导入的文件名" prop="shpName">
+        <el-input v-model="formData.shpName" placeholder="请输入矢量图形文件的系统名(导入的英文名)" />
+
+      </el-form-item>
+      <el-form-item v-if="formData.type == 2" label="瓦片服务地址" prop="urlAddress">
+        <el-input v-model="formData.urlAddress"  placeholder="若为3dtile,请输入瓦片接口地址(后续会更新为上传/文件选择)" />
+      </el-form-item>
+      <el-form-item label="图层名" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入孪生场景内,展示的图层中文名" />
+      </el-form-item>
+      <el-form-item label="数据结构" prop="parentId">
+        <el-tree-select
+          v-model="formData.parentId"
+          :data="gisFormTree"
+          :props="defaultProps"
+          check-strictly
+          default-expand-all
+          placeholder="请选择上级图层数据(孪生场景——树形折叠配置)"
+        />
+      </el-form-item>
+      <el-form-item label="图层排序" prop="seq">
+        <el-input v-model="formData.seq" placeholder="图层在列表中的顺序" />
+      </el-form-item>
+      <!-- <el-form-item label="文件格式" prop="fileFormat">
+        <el-select v-model="formData.fileFormat" placeholder="请选择文件格式">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.LAYER_CESIUM_FORMAT)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="文件地址" prop="fileAddress">
+        <UploadFile v-model="formData.fileAddress" />
+      </el-form-item>
+      <el-form-item label="API密钥" prop="key">
+        <el-input v-model="formData.key" placeholder="请输入应用编程接口密钥" />
+      </el-form-item>
+      <el-form-item label="图层颜色" prop="rgb">
+        <el-input v-model="formData.rgb" placeholder="请输入图层颜色" />
+      </el-form-item>
+      <el-form-item label="数据获取日期" prop="acquisitionDate">
+        <el-date-picker
+          v-model="formData.acquisitionDate"
+          type="date"
+          value-format="x"
+          placeholder="选择数据获取日期"
+        />
+      </el-form-item>
+      <el-form-item label="数据描述" prop="description">
+        <Editor v-model="formData.description" height="150px" />
+      </el-form-item>
+      <el-form-item label="图层关联" prop="shpId">
+        <el-input v-model="formData.shpId" placeholder="请输入图层关联" />
+      </el-form-item>
+      <el-form-item label="文件上传" prop="file">
+        <UploadFile v-model="formData.file" />
+      </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 { GisFormApi, GisFormVO } from '@/api/gislayer/gisform'
+import { defaultProps, handleTree } from '@/utils/tree'
+import {DICT_TYPE, getIntDictOptions} from "@/utils/dict";
+
+/** 图层表单 表单 */
+defineOptions({ name: 'GisFormForm' })
+
+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,
+  type: undefined,
+  name: undefined,
+  parentId: undefined,
+  seq: undefined,
+  fileFormat: undefined,
+  urlAddress: undefined,
+  fileAddress: undefined,
+  key: undefined,
+  rgb: undefined,
+  acquisitionDate: undefined,
+  description:"",
+  shpId: undefined,
+  file: undefined,
+  shpName: undefined,
+})
+const formRules = reactive({
+  type: [{ required: true, message: '图层数据类型不能为空', trigger: 'change' }],
+  name: [{ required: true, message: '地理实体名不能为空', trigger: 'blur' }],
+  parentId: [{ required: true, message: '父级编号不能为空', trigger: 'blur' }],
+  seq: [{ required: true, message: '排序不能为空', trigger: 'blur' }],
+})
+const formRef = ref() // 表单 Ref
+const gisFormTree = 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 GisFormApi.getGisForm(id)
+      console.log(formData.value);
+    } finally {
+      formLoading.value = false
+    }
+  }
+  await getGisFormTree()
+}
+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 GisFormVO
+    if (formType.value === 'create') {
+      await GisFormApi.createGisForm(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await GisFormApi.updateGisForm(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    type: undefined,
+    name: undefined,
+    parentId: undefined,
+    seq: undefined,
+    fileFormat: undefined,
+    urlAddress: undefined,
+    fileAddress: undefined,
+    key: undefined,
+    rgb: undefined,
+    acquisitionDate: undefined,
+    description:"",
+    shpId: undefined,
+    file: undefined,
+    shpName: undefined,
+  }
+  formRef.value?.resetFields()
+}
+
+/** 获得图层表单树 */
+const getGisFormTree = async () => {
+  gisFormTree.value = []
+  const data = await GisFormApi.getGisFormList()
+  const root: Tree = { id: 0, name: '顶级图层表单', children: [] }
+  root.children = handleTree(data, 'id', 'parentId')
+  gisFormTree.value.push(root)
+}
+</script>

+ 385 - 0
src/views/infra/codegen/components/GenerateInfoForm.vue

@@ -0,0 +1,385 @@
+<template>
+  <el-form ref="formRef" :model="formData" :rules="rules" label-width="150px">
+    <el-row>
+      <el-col :span="12">
+        <el-form-item label="生成模板" prop="templateType">
+          <el-select v-model="formData.templateType">
+            <el-option
+              v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="前端类型" prop="frontType">
+          <el-select v-model="formData.frontType">
+            <el-option
+              v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_FRONT_TYPE)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+      </el-col>
+
+      <el-col :span="12">
+        <el-form-item label="生成场景" prop="scene">
+          <el-select v-model="formData.scene">
+            <el-option
+              v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item>
+          <template #label>
+            <span>
+              上级菜单
+              <el-tooltip content="分配到指定菜单下,例如 系统管理" placement="top">
+                <Icon icon="ep:question-filled" />
+              </el-tooltip>
+            </span>
+          </template>
+          <el-tree-select
+            v-model="formData.parentMenuId"
+            :data="menus"
+            :props="menuTreeProps"
+            check-strictly
+            node-key="id"
+            placeholder="请选择系统菜单"
+          />
+        </el-form-item>
+      </el-col>
+
+      <!--      <el-col :span="12">-->
+      <!--        <el-form-item prop="packageName">-->
+      <!--          <span slot="label">-->
+      <!--            生成包路径-->
+      <!--            <el-tooltip content="生成在哪个java包下,例如 com.ruoyi.system" placement="top">-->
+      <!--              <i class="el-icon-question"></i>-->
+      <!--            </el-tooltip>-->
+      <!--          </span>-->
+      <!--          <el-input v-model="formData.packageName" />-->
+      <!--        </el-form-item>-->
+      <!--      </el-col>-->
+
+      <el-col :span="12">
+        <el-form-item prop="moduleName">
+          <template #label>
+            <span>
+              模块名
+              <el-tooltip
+                content="模块名,即一级目录,例如 system、infra、tool 等等"
+                placement="top"
+              >
+                <Icon icon="ep:question-filled" />
+              </el-tooltip>
+            </span>
+          </template>
+          <el-input v-model="formData.moduleName" />
+        </el-form-item>
+      </el-col>
+
+      <el-col :span="12">
+        <el-form-item prop="businessName">
+          <template #label>
+            <span>
+              业务名
+              <el-tooltip
+                content="业务名,即二级目录,例如 user、permission、dict 等等"
+                placement="top"
+              >
+                <Icon icon="ep:question-filled" />
+              </el-tooltip>
+            </span>
+          </template>
+          <el-input v-model="formData.businessName" />
+        </el-form-item>
+      </el-col>
+
+      <!--      <el-col :span="12">-->
+      <!--        <el-form-item prop="businessPackage">-->
+      <!--          <span slot="label">-->
+      <!--            业务包-->
+      <!--            <el-tooltip content="业务包,自定义二级目录。例如说,我们希望将 dictType 和 dictData 归类成 dict 业务" placement="top">-->
+      <!--              <i class="el-icon-question"></i>-->
+      <!--            </el-tooltip>-->
+      <!--          </span>-->
+      <!--          <el-input v-model="formData.businessPackage" />-->
+      <!--        </el-form-item>-->
+      <!--      </el-col>-->
+
+      <el-col :span="12">
+        <el-form-item prop="className">
+          <template #label>
+            <span>
+              类名称
+              <el-tooltip
+                content="类名称(首字母大写),例如SysUser、SysMenu、SysDictData 等等"
+                placement="top"
+              >
+                <Icon icon="ep:question-filled" />
+              </el-tooltip>
+            </span>
+          </template>
+          <el-input v-model="formData.className" />
+        </el-form-item>
+      </el-col>
+
+      <el-col :span="12">
+        <el-form-item prop="classComment">
+          <template #label>
+            <span>
+              类描述
+              <el-tooltip content="用作类描述,例如 用户" placement="top">
+                <Icon icon="ep:question-filled" />
+              </el-tooltip>
+            </span>
+          </template>
+          <el-input v-model="formData.classComment" />
+        </el-form-item>
+      </el-col>
+
+      <el-col v-if="formData.genType === '1'" :span="24">
+        <el-form-item prop="genPath">
+          <template #label>
+            <span>
+              自定义路径
+              <el-tooltip
+                content="填写磁盘绝对路径,若不填写,则生成到当前Web项目下"
+                placement="top"
+              >
+                <Icon icon="ep:question-filled" />
+              </el-tooltip>
+            </span>
+          </template>
+          <el-input v-model="formData.genPath">
+            <template #append>
+              <el-dropdown>
+                <el-button type="primary">
+                  最近路径快速选择
+                  <i class="el-icon-arrow-down el-icon--right"></i>
+                </el-button>
+                <template #dropdown>
+                  <el-dropdown-menu>
+                    <el-dropdown-item @click="formData.genPath = '/'">
+                      恢复默认的生成基础路径
+                    </el-dropdown-item>
+                  </el-dropdown-menu>
+                </template>
+              </el-dropdown>
+            </template>
+          </el-input>
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <!-- 树表信息 -->
+    <el-row v-if="formData.templateType == 2">
+      <el-col :span="24">
+        <h4 class="form-header">树表信息</h4>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item prop="treeParentColumnId">
+          <template #label>
+            <span>
+              父编号字段
+              <el-tooltip content="树显示的父编码字段名, 如:parent_Id" placement="top">
+                <Icon icon="ep:question-filled" />
+              </el-tooltip>
+            </span>
+          </template>
+          <el-select v-model="formData.treeParentColumnId" placeholder="请选择">
+            <el-option
+              v-for="(column, index) in props.columns"
+              :key="index"
+              :label="column.columnName + ':' + column.columnComment"
+              :value="column.id"
+            />
+          </el-select>
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item prop="treeNameColumnId">
+          <template #label>
+            <span>
+              树名称字段
+              <el-tooltip content="树节点的显示名称字段名, 如:dept_name" placement="top">
+                <Icon icon="ep:question-filled" />
+              </el-tooltip>
+            </span>
+          </template>
+          <el-select v-model="formData.treeNameColumnId" placeholder="请选择">
+            <el-option
+              v-for="(column, index) in props.columns"
+              :key="index"
+              :label="column.columnName + ':' + column.columnComment"
+              :value="column.id"
+            />
+          </el-select>
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <!-- 主表信息 -->
+    <el-row v-if="formData.templateType == 15">
+      <el-col :span="24">
+        <h4 class="form-header">主表信息</h4>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item prop="masterTableId">
+          <template #label>
+            <span>
+              关联的主表
+              <el-tooltip content="关联主表(父表)的表名, 如:system_user" placement="top">
+                <Icon icon="ep:question-filled" />
+              </el-tooltip>
+            </span>
+          </template>
+          <el-select v-model="formData.masterTableId" placeholder="请选择">
+            <el-option
+              v-for="(table0, index) in tables"
+              :key="index"
+              :label="table0.tableName + ':' + table0.tableComment"
+              :value="table0.id"
+            />
+          </el-select>
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item prop="subJoinColumnId">
+          <template #label>
+            <span>
+              子表关联的字段
+              <el-tooltip content="子表关联的字段, 如:user_id" placement="top">
+                <Icon icon="ep:question-filled" />
+              </el-tooltip>
+            </span>
+          </template>
+          <el-select v-model="formData.subJoinColumnId" placeholder="请选择">
+            <el-option
+              v-for="(column, index) in props.columns"
+              :key="index"
+              :label="column.columnName + ':' + column.columnComment"
+              :value="column.id"
+            />
+          </el-select>
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item prop="subJoinMany">
+          <template #label>
+            <span>
+              关联关系
+              <el-tooltip content="主表与子表的关联关系" placement="top">
+                <Icon icon="ep:question-filled" />
+              </el-tooltip>
+            </span>
+          </template>
+          <el-radio-group v-model="formData.subJoinMany" placeholder="请选择">
+            <el-radio :label="true">一对多</el-radio>
+            <el-radio :label="false">一对一</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-col>
+    </el-row>
+  </el-form>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { handleTree } from '@/utils/tree'
+import * as CodegenApi from '@/api/infra/codegen'
+import * as MenuApi from '@/api/system/menu'
+import { PropType } from 'vue'
+
+defineOptions({ name: 'InfraCodegenGenerateInfoForm' })
+
+const message = useMessage() // 消息弹窗
+const props = defineProps({
+  table: {
+    type: Object as PropType<Nullable<CodegenApi.CodegenTableVO>>,
+    default: () => null
+  },
+  columns: {
+    type: Array as unknown as PropType<CodegenApi.CodegenColumnVO[]>,
+    default: () => null
+  }
+})
+
+const formRef = ref()
+const formData = ref({
+  templateType: null,
+  frontType: null,
+  scene: null,
+  moduleName: '',
+  businessName: '',
+  className: '',
+  classComment: '',
+  parentMenuId: null,
+  genPath: '',
+  genType: '',
+  masterTableId: undefined,
+  subJoinColumnId: undefined,
+  subJoinMany: undefined,
+  treeParentColumnId: undefined,
+  treeNameColumnId: undefined
+})
+
+const rules = reactive({
+  templateType: [required],
+  frontType: [required],
+  scene: [required],
+  moduleName: [required],
+  businessName: [required],
+  businessPackage: [required],
+  className: [required],
+  classComment: [required],
+  masterTableId: [required],
+  subJoinColumnId: [required],
+  subJoinMany: [required],
+  treeParentColumnId: [required],
+  treeNameColumnId: [required]
+})
+
+const tables = ref([]) // 表定义列表
+const menus = ref<any[]>([])
+const menuTreeProps = {
+  label: 'name'
+}
+
+watch(
+  () => props.table,
+  async (table) => {
+    if (!table) return
+    formData.value = table as any
+    // 加载表列表
+    if (table.dataSourceConfigId >= 0) {
+      tables.value = await CodegenApi.getCodegenTableList(formData.value.dataSourceConfigId)
+    }
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+
+onMounted(async () => {
+  try {
+    // 加载菜单
+    const resp = await MenuApi.getSimpleMenusList()
+    menus.value = handleTree(resp)
+  } catch {}
+})
+
+defineExpose({
+  validate: async () => unref(formRef)?.validate()
+})
+</script>

+ 121 - 0
src/views/layer/formvector/FormVectorForm.vue

@@ -0,0 +1,121 @@
+<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="layername">
+        <el-select v-model="formData.layername" placeholder="请选择图层名">
+          <el-option label="请选择字典生成" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="父级编号" prop="parentId">
+        <el-input v-model="formData.parentId" placeholder="请输入父级编号" />
+      </el-form-item>
+      <el-form-item label="排序" prop="seq">
+        <el-input v-model="formData.seq" placeholder="请输入排序" />
+      </el-form-item>
+      <el-form-item label="图层颜色" prop="rgb">
+        <el-input v-model="formData.rgb" placeholder="请输入图层颜色" />
+      </el-form-item>
+      <el-form-item label="几何形状数据" prop="geom">
+        <el-input v-model="formData.geom" placeholder="请输入几何形状数据" />
+      </el-form-item>
+      <el-form-item label="数据描述" prop="description">
+        <Editor v-model="formData.description" height="150px" />
+      </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 { FormVectorApi, FormVectorVO } from '@/api/layer/formvector'
+
+/** 矢量图层表单 表单 */
+defineOptions({ name: 'FormVectorForm' })
+
+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,
+  layername: undefined,
+  parentId: undefined,
+  seq: undefined,
+  rgb: undefined,
+  geom: undefined,
+  description: undefined,
+})
+const formRules = reactive({
+  layername: [{ required: true, message: '图层名不能为空', trigger: 'change' }],
+  parentId: [{ required: true, message: '父级编号不能为空', trigger: 'blur' }],
+  seq: [{ 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 FormVectorApi.getFormVector(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 FormVectorVO
+    if (formType.value === 'create') {
+      await FormVectorApi.createFormVector(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await FormVectorApi.updateFormVector(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    layername: undefined,
+    parentId: undefined,
+    seq: undefined,
+    rgb: undefined,
+    geom: undefined,
+    description: undefined,
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 130 - 0
src/views/layer/formvectorattributes/FormVectorAttributesForm.vue

@@ -0,0 +1,130 @@
+<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="layername">
+        <el-input v-model="formData.layername" placeholder="请输入图层名" />
+      </el-form-item>
+      <el-form-item label="父级编号" prop="parentId">
+        <el-input v-model="formData.parentId" placeholder="请输入父级编号" />
+      </el-form-item>
+      <el-form-item label="图层颜色" prop="rgb">
+        <el-input v-model="formData.rgb" placeholder="请输入图层颜色" />
+      </el-form-item>
+      <el-form-item label="属性数据" prop="attributes">
+        <el-input v-model="formData.attributes" placeholder="请输入属性数据" />
+      </el-form-item>
+      <el-form-item label="数据描述" prop="description">
+        <Editor v-model="formData.description" height="150px" />
+      </el-form-item>
+      <el-form-item label="文件格式" prop="fileFormat">
+        <el-select v-model="formData.fileFormat" placeholder="请选择文件格式">
+          <el-option label="请选择字典生成" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="url地址" prop="urlAddress">
+        <el-input v-model="formData.urlAddress" placeholder="请输入url地址" />
+      </el-form-item>
+      <el-form-item label="文件地址" prop="fileAddress">
+        <el-input v-model="formData.fileAddress" 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 { FormVectorAttributesApi, FormVectorAttributesVO } from '@/api/layer/formvectorattributes'
+
+/** 矢量图层表单属性 表单 */
+defineOptions({ name: 'FormVectorAttributesForm' })
+
+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,
+  parentId: undefined,
+  rgb: undefined,
+  attributes: undefined,
+  description: undefined,
+  fileFormat: undefined,
+  urlAddress: undefined,
+  fileAddress: undefined,
+  layername: undefined,
+})
+const formRules = reactive({
+  parentId: [{ required: true, message: '父级编号不能为空', trigger: 'blur' }],
+  layername: [{ 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 FormVectorAttributesApi.getFormVectorAttributes(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 FormVectorAttributesVO
+    if (formType.value === 'create') {
+      await FormVectorAttributesApi.createFormVectorAttributes(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await FormVectorAttributesApi.updateFormVectorAttributes(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    parentId: undefined,
+    rgb: undefined,
+    attributes: undefined,
+    description: undefined,
+    fileFormat: undefined,
+    urlAddress: undefined,
+    fileAddress: undefined,
+    layername: undefined,
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 212 - 0
src/views/layer/gisname/GisNameForm.vue

@@ -0,0 +1,212 @@
+<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="shpName">
+        <el-input v-model="formData.shpName" placeholder="请输入图层名" />
+      </el-form-item>
+
+      <el-form-item label="显示配置" prop="showitem"  v-if="formType == 'update'">
+            <el-select
+              v-model="formData.showitem"
+              multiple
+              filterable
+              allow-create
+              default-first-option
+              placeholder="请选择项目相关图层">
+              <el-option
+                v-for="dict in datagisname"
+                :key="dict"
+                :label="dict"
+                :value="dict"
+              />
+            </el-select>
+      </el-form-item>
+
+      <el-form-item label="Label配置" prop="labelItem"  v-if="formType == 'update'">
+            <el-select
+              v-model="formData.labelItem"
+              multiple
+              filterable
+              default-first-option
+              placeholder="请选择项目相关图层">
+              <el-option
+                v-for="dict in datagisname"
+                :key="dict"
+                :label="dict"
+                :value="dict"
+              />
+            </el-select>
+      </el-form-item>
+
+      <el-form-item label="符号化模型" prop="symbolizeModel">
+        <el-input v-model="formData.symbolizeModel" placeholder="请输入符号化模型Url地址" />
+      </el-form-item>
+      <el-form-item label="文本显示" prop="textDisplay">
+        <el-select v-model="formData.textDisplay" placeholder="请选择文本显示">
+          <el-option label="请选择字典生成" value="" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="描述" prop="description">
+        <el-input v-model="formData.description" placeholder="请输入描述" />
+      </el-form-item>
+      <el-form-item label="文件地址" prop="fileAddress" v-if="formType != 'update'">
+        <!-- <UploadFile 
+          v-model="fakefileref" 
+          :limit = "10"
+          :fileType = "['cpg', 'dbf', 'prj', 'sbn', 'sbx','shp','shx']"
+          /> -->
+        <el-upload
+          ref="upload"
+          v-model:file-list="fakefileref"
+          :on-change="changeFile"
+          :drag = "true"
+          :accept = "'.shp'"
+          :limit="1"
+          :on-exceed="handleExceed"
+          :http-request = "()=>{}"
+        >
+          <el-button type="primary">Click to upload</el-button>
+          <template #tip>
+            <div class="el-upload__tip">
+              请选择'shp'格式文件上传,并在同目录存放'cpg', 'dbf', 'prj', 'sbn', 'sbx', 'shx'文件
+            </div>
+          </template>
+        </el-upload>
+      </el-form-item>
+      <!-- <el-form-item label="数据标识" prop="shpType">
+        <el-select v-model="formData.shpType" placeholder="请选择数据标识">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.LAYER_NAME_SHPTYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </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 { GisNameApi, GisNameVO } from '@/api/layer/gisname'
+import {genFileId} from 'element-plus'
+import {getgisAttr} from "@/api/layer/gisname";
+/** shp名 表单 */
+defineOptions({ name: 'GisNameForm' })
+
+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({
+  shpName: undefined,
+  description: undefined,
+  id: undefined,
+  shpType: 0,
+  symbolizeModel: undefined,
+  textDisplay: undefined,
+})
+const fakefileref = ref();
+const datagisname = ref([]);
+
+
+const upload = ref<UploadInstance>()
+
+const formRules = reactive({
+})
+const formRef = ref() // 表单 Ref
+const handleExceed: UploadProps['onExceed'] = (files) => {
+  upload.value!.clearFiles()
+  const file = files[0] as UploadRawFile
+  file.uid = genFileId()
+  upload.value!.handleStart(file)
+}
+const changeFile = (file: any) => {
+  console.log(file);
+  // fakefileref.value = file.raw;
+  formData.value.fileAddress = file.raw.name;
+}
+
+/** 打开弹窗 */
+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 GisNameApi.getGisName(id)
+      formData.value.showitem = formData.value.showitem=="[]"?null:formData.value.showitem;
+      formData.value.labelItem = formData.value.labelItem=="[]"?null:formData.value.labelItem;
+      console.log(formData.value.labelItem);
+      datagisname.value = await getgisAttr(formData.value.shpName);
+      console.log(datagisname.value);
+      formData.value.shpType = 0;
+    } 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 GisNameVO
+    data.shpType = 0;
+    if (formType.value === 'create') {
+      await GisNameApi.createGisName(data)
+      // await GisNameApi.LtyReadSHP({
+      //   shpPath: `E:\\gis_data\\ys\\Kunming\\${formData.value.fileAddress}`,
+      //   shpName: data.shpName
+      // });
+      message.success(t('common.createSuccess'))
+    } else {
+      //NOTE - data格式修改为json
+      await GisNameApi.updateGisName(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    shpName: undefined,
+    description: undefined,
+    id: undefined,
+    shpType: 0,
+    shpitem: undefined,
+    symbolizeModel: undefined,
+    textDisplay: undefined,
+
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 44 - 0
src/views/mall/promotion/coupon/formatter.ts

@@ -0,0 +1,44 @@
+import { CouponTemplateValidityTypeEnum, PromotionDiscountTypeEnum } from '@/utils/constants'
+import { formatDate } from '@/utils/formatTime'
+import { CouponTemplateVO } from '@/api/mall/promotion/coupon/couponTemplate'
+import { floatToFixed2 } from '@/utils'
+
+// 格式化【优惠金额/折扣】
+export const discountFormat = (row: CouponTemplateVO) => {
+  if (row.discountType === PromotionDiscountTypeEnum.PRICE.type) {
+    return `¥${floatToFixed2(row.discountPrice)}`
+  }
+  if (row.discountType === PromotionDiscountTypeEnum.PERCENT.type) {
+    return `${row.discountPercent}%`
+  }
+  return '未知【' + row.discountType + '】'
+}
+
+// 格式化【领取上限】
+export const takeLimitCountFormat = (row: CouponTemplateVO) => {
+  if (row.takeLimitCount === -1) {
+    return '无领取限制'
+  }
+  return `${row.takeLimitCount} 张/人`
+}
+
+// 格式化【有效期限】
+export const validityTypeFormat = (row: CouponTemplateVO) => {
+  if (row.validityType === CouponTemplateValidityTypeEnum.DATE.type) {
+    return `${formatDate(row.validStartTime)} 至 ${formatDate(row.validEndTime)}`
+  }
+  if (row.validityType === CouponTemplateValidityTypeEnum.TERM.type) {
+    return `领取后第 ${row.fixedStartTerm} - ${row.fixedEndTerm} 天内可用`
+  }
+  return '未知【' + row.validityType + '】'
+}
+
+// 格式化【剩余数量】
+export const remainedCountFormat = (row: CouponTemplateVO) => {
+  return row.totalCount - row.takeCount
+}
+
+// 格式化【最低消费】
+export const usePriceFormat = (row: CouponTemplateVO) => {
+  return `¥${floatToFixed2(row.usePrice)}`
+}

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


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