ydmyzx 6 months ago
parent
commit
11e49bad65
38 changed files with 5364 additions and 0 deletions
  1. 60 0
      src/api/mall/promotion/discount/discountActivity.ts
  2. 49 0
      src/api/system/dict/dict.data.ts
  3. 44 0
      src/api/system/dict/dict.type.ts
  4. 199 0
      src/components/Descriptions/src/DisbursementForm.vue
  5. 60 0
      src/components/DictTag/src/DictTag.vue
  6. 115 0
      src/components/Echart/src/Echart.vue
  7. 202 0
      src/components/Editor/src/Editor.vue
  8. 58 0
      src/components/Error/src/Error.vue
  9. 59 0
      src/components/FormCreate/src/components/DictSelect.vue
  10. 180 0
      src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
  11. 478 0
      src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
  12. 448 0
      src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue
  13. 280 0
      src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue
  14. 55 0
      src/components/bpmnProcessDesigner/package/penal/other/ElementOtherConfig.vue
  15. 169 0
      src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue
  16. 87 0
      src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue
  17. 70 0
      src/components/bpmnProcessDesigner/package/theme/element-variables.scss
  18. 6 0
      src/config/axios/errorCode.ts
  19. 457 0
      src/locales/en.ts
  20. 105 0
      src/store/modules/dict.ts
  21. 3 0
      src/types/elementPlus.d.ts
  22. 245 0
      src/utils/dict.ts
  23. 289 0
      src/utils/domUtils.ts
  24. 56 0
      src/utils/download.ts
  25. 325 0
      src/views/Home/echarts-data.ts
  26. 87 0
      src/views/infra/codegen/EditTable.vue
  27. 179 0
      src/views/mall/promotion/discountActivity/DiscountActivityForm.vue
  28. 119 0
      src/views/mall/promotion/discountActivity/discountActivity.data.ts
  29. 104 0
      src/views/mall/promotion/diy/page/DiyPageForm.vue
  30. 104 0
      src/views/mall/promotion/diy/template/DiyTemplateForm.vue
  31. BIN
      src/views/mall/promotion/kefu/components/asserts/emo.png
  32. 42 0
      src/views/mall/promotion/kefu/components/tools/EmojiSelectPopover.vue
  33. 126 0
      src/views/mall/promotion/kefu/components/tools/emoji.ts
  34. 87 0
      src/views/mp/draft/components/DraftTable.vue
  35. 75 0
      src/views/mp/draft/editor-config.ts
  36. 124 0
      src/views/system/dict/DictTypeForm.vue
  37. 183 0
      src/views/system/dict/data/DictDataForm.vue
  38. 35 0
      types/env.d.ts

+ 60 - 0
src/api/mall/promotion/discount/discountActivity.ts

@@ -0,0 +1,60 @@
+import request from '@/config/axios'
+import { Sku, Spu } from '@/api/mall/product/spu'
+
+export interface DiscountActivityVO {
+  id?: number
+  spuId?: number
+  name?: string
+  status?: number
+  remark?: string
+  startTime?: Date
+  endTime?: Date
+  products?: DiscountProductVO[]
+}
+// 限时折扣相关 属性
+export interface DiscountProductVO {
+  spuId: number
+  skuId: number
+  discountType: number
+  discountPercent: number
+  discountPrice: number
+}
+
+// 扩展 Sku 配置
+export type SkuExtension = Sku & {
+  productConfig: DiscountProductVO
+}
+
+export interface SpuExtension extends Spu {
+  skus: SkuExtension[] // 重写类型
+}
+
+// 查询限时折扣活动列表
+export const getDiscountActivityPage = async (params) => {
+  return await request.get({ url: '/promotion/discount-activity/page', params })
+}
+
+// 查询限时折扣活动详情
+export const getDiscountActivity = async (id: number) => {
+  return await request.get({ url: '/promotion/discount-activity/get?id=' + id })
+}
+
+// 新增限时折扣活动
+export const createDiscountActivity = async (data: DiscountActivityVO) => {
+  return await request.post({ url: '/promotion/discount-activity/create', data })
+}
+
+// 修改限时折扣活动
+export const updateDiscountActivity = async (data: DiscountActivityVO) => {
+  return await request.put({ url: '/promotion/discount-activity/update', data })
+}
+
+// 关闭限时折扣活动
+export const closeDiscountActivity = async (id: number) => {
+  return await request.put({ url: '/promotion/discount-activity/close?id=' + id })
+}
+
+// 删除限时折扣活动
+export const deleteDiscountActivity = async (id: number) => {
+  return await request.delete({ url: '/promotion/discount-activity/delete?id=' + id })
+}

+ 49 - 0
src/api/system/dict/dict.data.ts

@@ -0,0 +1,49 @@
+import request from '@/config/axios'
+
+export type DictDataVO = {
+  id: number | undefined
+  sort: number | undefined
+  label: string
+  value: string
+  dictType: string
+  status: number
+  colorType: string
+  cssClass: string
+  remark: string
+  createTime: Date
+}
+
+// 查询字典数据(精简)列表
+export const getSimpleDictDataList = () => {
+  return request.get({ url: '/system/dict-data/simple-list' })
+}
+
+// 查询字典数据列表
+export const getDictDataPage = (params: PageParam) => {
+  return request.get({ url: '/system/dict-data/page', params })
+}
+
+// 查询字典数据详情
+export const getDictData = (id: number) => {
+  return request.get({ url: '/system/dict-data/get?id=' + id })
+}
+
+// 新增字典数据
+export const createDictData = (data: DictDataVO) => {
+  return request.post({ url: '/system/dict-data/create', data })
+}
+
+// 修改字典数据
+export const updateDictData = (data: DictDataVO) => {
+  return request.put({ url: '/system/dict-data/update', data })
+}
+
+// 删除字典数据
+export const deleteDictData = (id: number) => {
+  return request.delete({ url: '/system/dict-data/delete?id=' + id })
+}
+
+// 导出字典类型数据
+export const exportDictData = (params) => {
+  return request.download({ url: '/system/dict-data/export', params })
+}

+ 44 - 0
src/api/system/dict/dict.type.ts

@@ -0,0 +1,44 @@
+import request from '@/config/axios'
+
+export type DictTypeVO = {
+  id: number | undefined
+  name: string
+  type: string
+  status: number
+  remark: string
+  createTime: Date
+}
+
+// 查询字典(精简)列表
+export const getSimpleDictTypeList = () => {
+  return request.get({ url: '/system/dict-type/list-all-simple' })
+}
+
+// 查询字典列表
+export const getDictTypePage = (params: PageParam) => {
+  return request.get({ url: '/system/dict-type/page', params })
+}
+
+// 查询字典详情
+export const getDictType = (id: number) => {
+  return request.get({ url: '/system/dict-type/get?id=' + id })
+}
+
+// 新增字典
+export const createDictType = (data: DictTypeVO) => {
+  return request.post({ url: '/system/dict-type/create', data })
+}
+
+// 修改字典
+export const updateDictType = (data: DictTypeVO) => {
+  return request.put({ url: '/system/dict-type/update', data })
+}
+
+// 删除字典
+export const deleteDictType = (id: number) => {
+  return request.delete({ url: '/system/dict-type/delete?id=' + id })
+}
+// 导出字典类型
+export const exportDictType = (params) => {
+  return request.download({ url: '/system/dict-type/export', params })
+}

+ 199 - 0
src/components/Descriptions/src/DisbursementForm.vue

@@ -0,0 +1,199 @@
+<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="userId">
+        <el-input v-model="formData.userId" placeholder="请输入申请人的用户编号" />
+      </el-form-item>
+      <el-form-item label="备注" prop="reason">
+        <el-input v-model="formData.reason" placeholder="请输入备注" />
+      </el-form-item>
+      <el-form-item label="流程实例的编号" prop="processInstanceId">
+        <el-input v-model="formData.processInstanceId" placeholder="请输入流程实例的编号" />
+      </el-form-item>
+      <el-form-item label="文件" prop="file">
+        <UploadFile v-model="formData.file" />
+      </el-form-item>
+      <el-form-item label="申请类型" prop="type">
+        <el-select v-model="formData.type" placeholder="请选择申请类型">
+          <el-option label="请选择字典生成" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="申请总金额" prop="funding">
+        <el-input v-model="formData.funding" placeholder="请输入申请总金额" />
+      </el-form-item>
+      <el-form-item label="审批结果" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio label="1">请选择字典生成</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="专项借款申请材料" prop="specialLoansFile">
+        <UploadFile v-model="formData.specialLoansFile" />
+      </el-form-item>
+      <el-form-item label="项目名称" prop="itemName">
+        <el-input v-model="formData.itemName" placeholder="请输入项目名称" />
+      </el-form-item>
+      <el-form-item label="项目类型" prop="itemType">
+        <el-select v-model="formData.itemType" placeholder="请选择项目类型">
+          <el-option label="请选择字典生成" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="项目归属(上级目录)" prop="itemBelong">
+        <el-input v-model="formData.itemBelong" placeholder="请输入项目归属(上级目录)" />
+      </el-form-item>
+      <el-form-item label="项目相关佐证材料(图片,如施工图)" prop="itemImage">
+        <UploadImg v-model="formData.itemImage" />
+      </el-form-item>
+      <el-form-item label="资金申请材料" prop="fundingApplicationFile">
+        <UploadFile v-model="formData.fundingApplicationFile" />
+      </el-form-item>
+      <el-form-item label="项目拨付结果(状态值)" prop="itemDisbursementStatus">
+        <el-radio-group v-model="formData.itemDisbursementStatus">
+          <el-radio label="1">请选择字典生成</el-radio>
+        </el-radio-group>
+      </el-form-item> -->
+      
+      <el-form-item label="绑定矢量" prop="itemShp">
+        <!-- <el-input v-model="formData.itemShp" placeholder="请输入项目相关shp上传(保存url,可null)" />
+          -->
+          <el-select
+              v-model="formData.itemShp"
+              multiple
+              filterable
+              allow-create
+              default-first-option
+              placeholder="请选择项目相关图层">
+              <el-option
+                v-for="dict in datagisname"
+                :key="dict.id"
+                :label="dict.shpName"
+                :value="dict.shpName"
+              />
+            </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 { DisbursementApi, DisbursementVO } from '@/api/bpm/disbursement'
+import {getSimpleGisNameList} from "@/api/layer/gisname";
+
+/** 资金拨付流程表	 表单 */
+defineOptions({ name: 'DisbursementForm' })
+
+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({
+  userId: undefined,
+  reason: undefined,
+  processInstanceId: undefined,
+  file: undefined,
+  id: undefined,
+  type: undefined,
+  funding: undefined,
+  status: undefined,
+  specialLoansFile: undefined,
+  itemName: undefined,
+  itemType: undefined,
+  itemBelong: undefined,
+  itemShp: undefined,
+  itemImage: undefined,
+  fundingApplicationFile: undefined,
+  itemDisbursementStatus: undefined,
+})
+const formRules = reactive({
+  userId: [{ required: true, message: '申请人的用户编号不能为空', trigger: 'blur' }],
+  reason: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
+  type: [{ required: true, message: '申请类型不能为空', trigger: 'change' }],
+  funding: [{ required: true, message: '申请总金额不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '审批结果不能为空', trigger: 'blur' }],
+})
+const formRef = ref() // 表单 Ref
+
+const datagisname = ref([]);
+
+
+const gisnamedatalist = async () => {
+  datagisname.value = await getSimpleGisNameList();
+}
+
+/** 打开弹窗 */
+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 DisbursementApi.getDisbursement(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 DisbursementVO
+    if (formType.value === 'create') {
+      await DisbursementApi.createDisbursement(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DisbursementApi.updateDisbursement(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    userId: undefined,
+    reason: undefined,
+    processInstanceId: undefined,
+    file: undefined,
+    id: undefined,
+    type: undefined,
+    funding: undefined,
+    status: undefined,
+    specialLoansFile: undefined,
+    itemName: undefined,
+    itemType: undefined,
+    itemBelong: undefined,
+    itemShp: undefined,
+    itemImage: undefined,
+    fundingApplicationFile: undefined,
+    itemDisbursementStatus: undefined,
+  }
+  formRef.value?.resetFields()
+  gisnamedatalist()
+}
+</script>

+ 60 - 0
src/components/DictTag/src/DictTag.vue

@@ -0,0 +1,60 @@
+<script lang="tsx">
+import { defineComponent, PropType, ref } from 'vue'
+import { isHexColor } from '@/utils/color'
+import { ElTag } from 'element-plus'
+import { DictDataType, getDictOptions } from '@/utils/dict'
+
+export default defineComponent({
+  name: 'DictTag',
+  props: {
+    type: {
+      type: String as PropType<string>,
+      required: true
+    },
+    value: {
+      type: [String, Number, Boolean] as PropType<string | number | boolean>,
+      required: true
+    }
+  },
+  setup(props) {
+    const dictData = ref<DictDataType>()
+    const getDictObj = (dictType: string, value: string) => {
+      const dictOptions = getDictOptions(dictType)
+      dictOptions.forEach((dict: DictDataType) => {
+        if (dict.value === value) {
+          if (dict.colorType + '' === 'primary' || dict.colorType + '' === 'default') {
+            dict.colorType = ''
+          }
+          dictData.value = dict
+        }
+      })
+    }
+    const rederDictTag = () => {
+      if (!props.type) {
+        return null
+      }
+      // 解决自定义字典标签值为零时标签不渲染的问题
+      if (props.value === undefined || props.value === null) {
+        return null
+      }
+      getDictObj(props.type, props.value.toString())
+      // 添加标签的文字颜色为白色,解决自定义背景颜色时标签文字看不清的问题
+      return (
+        <ElTag
+          style={dictData.value?.cssClass ? 'color: #fff' : ''}
+          type={dictData.value?.colorType}
+          color={
+            dictData.value?.cssClass && isHexColor(dictData.value?.cssClass)
+              ? dictData.value?.cssClass
+              : ''
+          }
+          disableTransitions={true}
+        >
+          {dictData.value?.label}
+        </ElTag>
+      )
+    }
+    return () => rederDictTag()
+  }
+})
+</script>

+ 115 - 0
src/components/Echart/src/Echart.vue

@@ -0,0 +1,115 @@
+<script lang="ts" setup>
+import type { EChartsOption } from 'echarts'
+import echarts from '@/plugins/echarts'
+import { debounce } from 'lodash-es'
+import 'echarts-wordcloud'
+import { propTypes } from '@/utils/propTypes'
+import { PropType } from 'vue'
+import { useAppStore } from '@/store/modules/app'
+import { isString } from '@/utils/is'
+import { useDesign } from '@/hooks/web/useDesign'
+
+defineOptions({ name: 'EChart' })
+
+const { getPrefixCls, variables } = useDesign()
+
+const prefixCls = getPrefixCls('echart')
+
+const appStore = useAppStore()
+
+const props = defineProps({
+  options: {
+    type: Object as PropType<EChartsOption>,
+    required: true
+  },
+  width: propTypes.oneOfType([Number, String]).def(''),
+  height: propTypes.oneOfType([Number, String]).def('500px')
+})
+
+const isDark = computed(() => appStore.getIsDark)
+
+const theme = computed(() => {
+  const echartTheme: boolean | string = unref(isDark) ? true : 'auto'
+
+  return echartTheme
+})
+
+const options = computed(() => {
+  return Object.assign(props.options, {
+    darkMode: unref(theme)
+  })
+})
+
+const elRef = ref<ElRef>()
+
+let echartRef: Nullable<echarts.ECharts> = null
+
+const contentEl = ref<Element>()
+
+const styles = computed(() => {
+  const width = isString(props.width) ? props.width : `${props.width}px`
+  const height = isString(props.height) ? props.height : `${props.height}px`
+
+  return {
+    width,
+    height
+  }
+})
+
+const initChart = () => {
+  if (unref(elRef) && props.options) {
+    echartRef = echarts.init(unref(elRef) as HTMLElement)
+    echartRef?.setOption(unref(options))
+  }
+}
+
+watch(
+  () => options.value,
+  (options) => {
+    if (echartRef) {
+      echartRef?.setOption(options)
+    }
+  },
+  {
+    deep: true
+  }
+)
+
+const resizeHandler = debounce(() => {
+  if (echartRef) {
+    echartRef.resize()
+  }
+}, 100)
+
+const contentResizeHandler = async (e: TransitionEvent) => {
+  if (e.propertyName === 'width') {
+    resizeHandler()
+  }
+}
+
+onMounted(() => {
+  initChart()
+
+  window.addEventListener('resize', resizeHandler)
+
+  contentEl.value = document.getElementsByClassName(`${variables.namespace}-layout-content`)[0]
+  unref(contentEl) &&
+    (unref(contentEl) as Element).addEventListener('transitionend', contentResizeHandler)
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', resizeHandler)
+  unref(contentEl) &&
+    (unref(contentEl) as Element).removeEventListener('transitionend', contentResizeHandler)
+})
+
+onActivated(() => {
+  if (echartRef) {
+    echartRef.resize()
+  }
+})
+</script>
+
+<template>
+  <div ref="elRef" :class="[$attrs.class, prefixCls]" :style="styles"></div>
+</template>

+ 202 - 0
src/components/Editor/src/Editor.vue

@@ -0,0 +1,202 @@
+<script lang="ts" setup>
+import { PropType } from 'vue'
+import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
+import { i18nChangeLanguage, IDomEditor, IEditorConfig } from '@wangeditor/editor'
+import { propTypes } from '@/utils/propTypes'
+import { isNumber } from '@/utils/is'
+import { ElMessage } from 'element-plus'
+import { useLocaleStore } from '@/store/modules/locale'
+import { getAccessToken, getTenantId } from '@/utils/auth'
+
+defineOptions({ name: 'Editor' })
+
+type InsertFnType = (url: string, alt: string, href: string) => void
+
+const localeStore = useLocaleStore()
+
+const currentLocale = computed(() => localeStore.getCurrentLocale)
+
+i18nChangeLanguage(unref(currentLocale).lang)
+
+const props = defineProps({
+  editorId: propTypes.string.def('wangeEditor-1'),
+  height: propTypes.oneOfType([Number, String]).def('500px'),
+  editorConfig: {
+    type: Object as PropType<Partial<IEditorConfig>>,
+    default: () => undefined
+  },
+  readonly: propTypes.bool.def(false),
+  modelValue: propTypes.string.def('')
+})
+
+const emit = defineEmits(['change', 'update:modelValue'])
+
+// 编辑器实例,必须用 shallowRef
+const editorRef = shallowRef<IDomEditor>()
+
+const valueHtml = ref('')
+
+watch(
+  () => props.modelValue,
+  (val: string) => {
+    if (val === unref(valueHtml)) return
+    valueHtml.value = val
+  },
+  {
+    immediate: true
+  }
+)
+
+// 监听
+watch(
+  () => valueHtml.value,
+  (val: string) => {
+    emit('update:modelValue', val)
+  }
+)
+
+const handleCreated = (editor: IDomEditor) => {
+  editorRef.value = editor
+}
+
+// 编辑器配置
+const editorConfig = computed((): IEditorConfig => {
+  return Object.assign(
+    {
+      placeholder: '请输入内容...',
+      readOnly: props.readonly,
+      customAlert: (s: string, t: string) => {
+        switch (t) {
+          case 'success':
+            ElMessage.success(s)
+            break
+          case 'info':
+            ElMessage.info(s)
+            break
+          case 'warning':
+            ElMessage.warning(s)
+            break
+          case 'error':
+            ElMessage.error(s)
+            break
+          default:
+            ElMessage.info(s)
+            break
+        }
+      },
+      autoFocus: false,
+      scroll: true,
+      MENU_CONF: {
+        ['uploadImage']: {
+          server: import.meta.env.VITE_UPLOAD_URL,
+          // 单个文件的最大体积限制,默认为 2M
+          maxFileSize: 5 * 1024 * 1024,
+          // 最多可上传几个文件,默认为 100
+          maxNumberOfFiles: 10,
+          // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
+          allowedFileTypes: ['image/*'],
+
+          // 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
+          meta: { updateSupport: 0 },
+          // 将 meta 拼接到 url 参数中,默认 false
+          metaWithUrl: true,
+
+          // 自定义增加 http  header
+          headers: {
+            Accept: '*',
+            Authorization: 'Bearer ' + getAccessToken(),
+            'tenant-id': getTenantId()
+          },
+
+          // 跨域是否传递 cookie ,默认为 false
+          withCredentials: true,
+
+          // 超时时间,默认为 10 秒
+          timeout: 5 * 1000, // 5 秒
+
+          // form-data fieldName,后端接口参数名称,默认值wangeditor-uploaded-image
+          fieldName: 'file',
+
+          // 上传之前触发
+          onBeforeUpload(file: File) {
+            console.log(file)
+            return file
+          },
+          // 上传进度的回调函数
+          onProgress(progress: number) {
+            // progress 是 0-100 的数字
+            console.log('progress', progress)
+          },
+          onSuccess(file: File, res: any) {
+            console.log('onSuccess', file, res)
+          },
+          onFailed(file: File, res: any) {
+            alert(res.message)
+            console.log('onFailed', file, res)
+          },
+          onError(file: File, err: any, res: any) {
+            alert(err.message)
+            console.error('onError', file, err, res)
+          },
+          // 自定义插入图片
+          customInsert(res: any, insertFn: InsertFnType) {
+            insertFn(res.data, 'image', res.data)
+          }
+        }
+      },
+      uploadImgShowBase64: true
+    },
+    props.editorConfig || {}
+  )
+})
+
+const editorStyle = computed(() => {
+  return {
+    height: isNumber(props.height) ? `${props.height}px` : props.height
+  }
+})
+
+// 回调函数
+const handleChange = (editor: IDomEditor) => {
+  emit('change', editor)
+}
+
+// 组件销毁时,及时销毁编辑器
+onBeforeUnmount(() => {
+  const editor = unref(editorRef.value)
+
+  // 销毁,并移除 editor
+  editor?.destroy()
+})
+
+const getEditorRef = async (): Promise<IDomEditor> => {
+  await nextTick()
+  return unref(editorRef.value) as IDomEditor
+}
+
+defineExpose({
+  getEditorRef
+})
+</script>
+
+<template>
+  <div class="border-1 border-solid border-[var(--tags-view-border-color)] z-10">
+    <!-- 工具栏 -->
+    <Toolbar
+      :editor="editorRef"
+      :editorId="editorId"
+      class="border-0 b-b-1 border-solid border-[var(--tags-view-border-color)]"
+    />
+    <!-- 编辑器 -->
+    <Editor
+      v-model="valueHtml"
+      :defaultConfig="editorConfig"
+      :editorId="editorId"
+      :style="editorStyle"
+      @on-change="handleChange"
+      @on-created="handleCreated"
+    />
+  </div>
+</template>
+
+<style src="@wangeditor/editor/dist/css/style.css"></style>

+ 58 - 0
src/components/Error/src/Error.vue

@@ -0,0 +1,58 @@
+<script lang="ts" setup>
+import pageError from '@/assets/svgs/404.svg'
+import networkError from '@/assets/svgs/500.svg'
+import noPermission from '@/assets/svgs/403.svg'
+import { propTypes } from '@/utils/propTypes'
+
+defineOptions({ name: 'Error' })
+
+interface ErrorMap {
+  url: string
+  message: string
+  buttonText: string
+}
+
+const { t } = useI18n()
+
+const errorMap: {
+  [key: string]: ErrorMap
+} = {
+  '404': {
+    url: pageError,
+    message: t('error.pageError'),
+    buttonText: t('error.returnToHome')
+  },
+  '500': {
+    url: networkError,
+    message: t('error.networkError'),
+    buttonText: t('error.returnToHome')
+  },
+  '403': {
+    url: noPermission,
+    message: t('error.noPermission'),
+    buttonText: t('error.returnToHome')
+  }
+}
+
+const props = defineProps({
+  type: propTypes.string.validate((v: string) => ['404', '500', '403'].includes(v)).def('404')
+})
+
+const emit = defineEmits(['errorClick'])
+
+const btnClick = () => {
+  emit('errorClick', props.type)
+}
+</script>
+
+<template>
+  <div class="flex justify-center">
+    <div class="text-center">
+      <img :src="errorMap[type].url" alt="" width="350" />
+      <div class="text-14px text-[var(--el-color-info)]">{{ errorMap[type].message }}</div>
+      <div class="mt-20px">
+        <ElButton type="primary" @click="btnClick">{{ errorMap[type].buttonText }}</ElButton>
+      </div>
+    </div>
+  </div>
+</template>

+ 59 - 0
src/components/FormCreate/src/components/DictSelect.vue

@@ -0,0 +1,59 @@
+<!-- 数据字典 Select 选择器 -->
+<template>
+  <el-select v-if="selectType === 'select'" class="w-1/1" v-bind="attrs">
+    <el-option
+      v-for="(dict, index) in getDictOptions"
+      :key="index"
+      :label="dict.label"
+      :value="dict.value"
+    />
+  </el-select>
+  <el-radio-group v-if="selectType === 'radio'" class="w-1/1" v-bind="attrs">
+    <el-radio v-for="(dict, index) in getDictOptions" :key="index" :value="dict.value">
+      {{ dict.label }}
+    </el-radio>
+  </el-radio-group>
+  <el-checkbox-group v-if="selectType === 'checkbox'" class="w-1/1" v-bind="attrs">
+    <el-checkbox
+      v-for="(dict, index) in getDictOptions"
+      :key="index"
+      :label="dict.label"
+      :value="dict.value"
+    />
+  </el-checkbox-group>
+</template>
+
+<script lang="ts" setup>
+import { getBoolDictOptions, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+
+defineOptions({ name: 'DictSelect' })
+
+const attrs = useAttrs()
+
+// 接受父组件参数
+interface Props {
+  dictType: string // 字典类型
+  valueType?: 'str' | 'int' | 'bool' // 字典值类型
+  selectType?: 'select' | 'radio' | 'checkbox' // 选择器类型,下拉框 select、多选框 checkbox、单选框 radio
+  formCreateInject?: any
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  valueType: 'str',
+  selectType: 'select'
+})
+
+// 获得字典配置
+const getDictOptions = computed(() => {
+  switch (props.valueType) {
+    case 'str':
+      return getStrDictOptions(props.dictType)
+    case 'int':
+      return getIntDictOptions(props.dictType)
+    case 'bool':
+      return getBoolDictOptions(props.dictType)
+    default:
+      return []
+  }
+})
+</script>

+ 180 - 0
src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue

@@ -0,0 +1,180 @@
+<template>
+  <div class="panel-tab__content">
+    <el-form label-width="90px" :model="needProps" :rules="rules">
+      <div v-if="needProps.type == 'bpmn:Process'">
+        <!-- 如果是 Process 信息的时候,使用自定义表单 -->
+        <el-form-item label="流程标识" prop="id">
+          <el-input
+            v-model="needProps.id"
+            placeholder="请输入流标标识"
+            :disabled="needProps.id !== undefined && needProps.id.length > 0"
+            @change="handleKeyUpdate"
+          />
+        </el-form-item>
+        <el-form-item label="流程名称" prop="name">
+          <el-input
+            v-model="needProps.name"
+            placeholder="请输入流程名称"
+            clearable
+            @change="handleNameUpdate"
+          />
+        </el-form-item>
+      </div>
+      <div v-else>
+        <el-form-item label="ID">
+          <el-input v-model="elementBaseInfo.id" clearable @change="updateBaseInfo('id')" />
+        </el-form-item>
+        <el-form-item label="名称">
+          <el-input v-model="elementBaseInfo.name" clearable @change="updateBaseInfo('name')" />
+        </el-form-item>
+      </div>
+    </el-form>
+  </div>
+</template>
+<script lang="ts" setup>
+defineOptions({ name: 'ElementBaseInfo' })
+
+const props = defineProps({
+  businessObject: {
+    type: Object,
+    default: () => {}
+  },
+  model: {
+    type: Object,
+    default: () => {}
+  }
+})
+const needProps = ref<any>({})
+const bpmnElement = ref()
+const elementBaseInfo = ref<any>({})
+// 流程表单的下拉框的数据
+// const forms = ref([])
+// 流程模型的校验
+const rules = reactive({
+  id: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }]
+})
+
+const bpmnInstances = () => (window as any)?.bpmnInstances
+const resetBaseInfo = () => {
+  console.log(window, 'window')
+  console.log(bpmnElement.value, 'bpmnElement')
+
+  bpmnElement.value = bpmnInstances()?.bpmnElement
+  // console.log(bpmnElement.value, 'resetBaseInfo11111111111')
+  elementBaseInfo.value = bpmnElement.value.businessObject
+  needProps.value['type'] = bpmnElement.value.businessObject.$type
+  // elementBaseInfo.value['typess'] = bpmnElement.value.businessObject.$type
+
+  // elementBaseInfo.value = JSON.parse(JSON.stringify(bpmnElement.value.businessObject))
+  // console.log(elementBaseInfo.value, 'elementBaseInfo22222222222')
+}
+const handleKeyUpdate = (value) => {
+  // 校验 value 的值,只有 XML NCName 通过的情况下,才进行赋值。否则,会导致流程图报错,无法绘制的问题
+  if (!value) {
+    return
+  }
+  if (!value.match(/[a-zA-Z_][\-_.0-9a-zA-Z$]*/)) {
+    console.log('key 不满足 XML NCName 规则,所以不进行赋值')
+    return
+  }
+  console.log('key 满足 XML NCName 规则,所以进行赋值')
+
+  // 在 BPMN 的 XML 中,流程标识 key,其实对应的是 id 节点
+  elementBaseInfo.value['id'] = value
+
+  setTimeout(() => {
+    updateBaseInfo('id')
+  }, 100)
+}
+const handleNameUpdate = (value) => {
+  console.log(elementBaseInfo, 'elementBaseInfo')
+  if (!value) {
+    return
+  }
+  elementBaseInfo.value['name'] = value
+
+  setTimeout(() => {
+    updateBaseInfo('name')
+  }, 100)
+}
+// const handleDescriptionUpdate=(value)=> {
+// TODO 芋艿:documentation 暂时无法修改,后续在看看
+// this.elementBaseInfo['documentation'] = value;
+// this.updateBaseInfo('documentation');
+// }
+const updateBaseInfo = (key) => {
+  console.log(key, 'key')
+  // 触发 elementBaseInfo 对应的字段
+  const attrObj = Object.create(null)
+  // console.log(attrObj, 'attrObj')
+  attrObj[key] = elementBaseInfo.value[key]
+  // console.log(attrObj, 'attrObj111')
+  // const attrObj = {
+  //   id: elementBaseInfo.value[key]
+  //   // di: { id: `${elementBaseInfo.value[key]}_di` }
+  // }
+  // console.log(elementBaseInfo, 'elementBaseInfo11111111111')
+  needProps.value = { ...elementBaseInfo.value, ...needProps.value }
+
+  if (key === 'id') {
+    // console.log('jinru')
+    console.log(window, 'window')
+    console.log(bpmnElement.value, 'bpmnElement')
+    console.log(toRaw(bpmnElement.value), 'bpmnElement')
+    bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+      id: elementBaseInfo.value[key],
+      di: { id: `${elementBaseInfo.value[key]}_di` }
+    })
+  } else {
+    console.log(attrObj, 'attrObj')
+    bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), attrObj)
+  }
+}
+
+watch(
+  () => props.businessObject,
+  (val) => {
+    // console.log(val, 'val11111111111111111111')
+    if (val) {
+      // nextTick(() => {
+      resetBaseInfo()
+      // })
+    }
+  }
+)
+
+watch(
+  () => props.model?.key,
+  (val) => {
+    // 针对上传的 bpmn 流程图时,保证 key 和 name 的更新
+    if (val) {
+      handleKeyUpdate(props.model.key)
+      handleNameUpdate(props.model.name)
+    }
+  }
+)
+
+// watch(
+//   () => ({ ...props }),
+//   (oldVal, newVal) => {
+//     console.log(oldVal, 'oldVal')
+//     console.log(newVal, 'newVal')
+//     if (newVal) {
+//       needProps.value = newVal
+//     }
+//   },
+//   {
+//     immediate: true
+//   }
+// )
+// 'model.key': {
+//   immediate: false,
+//   handler: function (val) {
+//     this.handleKeyUpdate(val)
+//   }
+// }
+onBeforeUnmount(() => {
+  bpmnElement.value = null
+})
+</script>

+ 478 - 0
src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue

@@ -0,0 +1,478 @@
+<template>
+  <div class="panel-tab__content">
+    <el-form label-width="80px">
+      <el-form-item label="流程表单">
+        <!--        <el-input v-model="formKey" clearable @change="updateElementFormKey" />-->
+        <el-select v-model="formKey" clearable @change="updateElementFormKey">
+          <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
+        </el-select>
+      </el-form-item>
+      <!--      <el-form-item label="业务标识">-->
+      <!--        <el-select v-model="businessKey" @change="updateElementBusinessKey">-->
+      <!--          <el-option v-for="i in fieldList" :key="i.id" :value="i.id" :label="i.label" />-->
+      <!--          <el-option label="无" value="" />-->
+      <!--        </el-select>-->
+      <!--      </el-form-item>-->
+    </el-form>
+
+    <!--字段列表-->
+    <!--    <div class="element-property list-property">-->
+    <!--      <el-divider><Icon icon="ep:coin" /> 表单字段</el-divider>-->
+    <!--      <el-table :data="fieldList" max-height="240" fit border>-->
+    <!--        <el-table-column label="序号" type="index" width="50px" />-->
+    <!--        <el-table-column label="字段名称" prop="label" min-width="80px" show-overflow-tooltip />-->
+    <!--        <el-table-column-->
+    <!--          label="字段类型"-->
+    <!--          prop="type"-->
+    <!--          min-width="80px"-->
+    <!--          :formatter="(row) => fieldType[row.type] || row.type"-->
+    <!--          show-overflow-tooltip-->
+    <!--        />-->
+    <!--        <el-table-column-->
+    <!--          label="默认值"-->
+    <!--          prop="defaultValue"-->
+    <!--          min-width="80px"-->
+    <!--          show-overflow-tooltip-->
+    <!--        />-->
+    <!--        <el-table-column label="操作" width="90px">-->
+    <!--          <template #default="scope">-->
+    <!--            <el-button type="primary" link @click="openFieldForm(scope, scope.$index)"-->
+    <!--              >编辑</el-button-->
+    <!--            >-->
+    <!--            <el-divider direction="vertical" />-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              style="color: #ff4d4f"-->
+    <!--              @click="removeField(scope, scope.$index)"-->
+    <!--              >移除</el-button-->
+    <!--            >-->
+    <!--          </template>-->
+    <!--        </el-table-column>-->
+    <!--      </el-table>-->
+    <!--    </div>-->
+    <!--    <div class="element-drawer__button">-->
+    <!--      <XButton type="primary" proIcon="ep:plus" title="添加字段" @click="openFieldForm(null, -1)" />-->
+    <!--    </div>-->
+
+    <!--字段配置侧边栏-->
+    <!--    <el-drawer-->
+    <!--      v-model="fieldModelVisible"-->
+    <!--      title="字段配置"-->
+    <!--      :size="`${width}px`"-->
+    <!--      append-to-body-->
+    <!--      destroy-on-close-->
+    <!--    >-->
+    <!--      <el-form :model="formFieldForm" label-width="90px">-->
+    <!--        <el-form-item label="字段ID">-->
+    <!--          <el-input v-model="formFieldForm.id" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="类型">-->
+    <!--          <el-select-->
+    <!--            v-model="formFieldForm.typeType"-->
+    <!--            placeholder="请选择字段类型"-->
+    <!--            clearable-->
+    <!--            @change="changeFieldTypeType"-->
+    <!--          >-->
+    <!--            <el-option v-for="(value, key) of fieldType" :label="value" :value="key" :key="key" />-->
+    <!--          </el-select>-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="类型名称" v-if="formFieldForm.typeType === 'custom'">-->
+    <!--          <el-input v-model="formFieldForm.type" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="名称">-->
+    <!--          <el-input v-model="formFieldForm.label" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="时间格式" v-if="formFieldForm.typeType === 'date'">-->
+    <!--          <el-input v-model="formFieldForm.datePattern" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="默认值">-->
+    <!--          <el-input v-model="formFieldForm.defaultValue" clearable />-->
+    <!--        </el-form-item>-->
+    <!--      </el-form>-->
+
+    <!--      &lt;!&ndash; 枚举值设置 &ndash;&gt;-->
+    <!--      <template v-if="formFieldForm.type === 'enum'">-->
+    <!--        <el-divider key="enum-divider" />-->
+    <!--        <p class="listener-filed__title" key="enum-title">-->
+    <!--          <span><Icon icon="ep:menu" />枚举值列表:</span>-->
+    <!--          <el-button type="primary" @click="openFieldOptionForm(null, -1, 'enum')"-->
+    <!--            >添加枚举值</el-button-->
+    <!--          >-->
+    <!--        </p>-->
+    <!--        <el-table :data="fieldEnumList" key="enum-table" max-height="240" fit border>-->
+    <!--          <el-table-column label="序号" width="50px" type="index" />-->
+    <!--          <el-table-column label="枚举值编号" prop="id" min-width="100px" show-overflow-tooltip />-->
+    <!--          <el-table-column label="枚举值名称" prop="name" min-width="100px" show-overflow-tooltip />-->
+    <!--          <el-table-column label="操作" width="90px">-->
+    <!--            <template #default="scope">-->
+    <!--              <el-button-->
+    <!--                type="primary"-->
+    <!--                link-->
+    <!--                @click="openFieldOptionForm(scope, scope.$index, 'enum')"-->
+    <!--                >编辑</el-button-->
+    <!--              >-->
+    <!--              <el-divider direction="vertical" />-->
+    <!--              <el-button-->
+    <!--                type="primary"-->
+    <!--                link-->
+    <!--                style="color: #ff4d4f"-->
+    <!--                @click="removeFieldOptionItem(scope, scope.$index, 'enum')"-->
+    <!--                >移除</el-button-->
+    <!--              >-->
+    <!--            </template>-->
+    <!--          </el-table-column>-->
+    <!--        </el-table>-->
+    <!--      </template>-->
+
+    <!--      &lt;!&ndash; 校验规则 &ndash;&gt;-->
+    <!--      <el-divider key="validation-divider" />-->
+    <!--      <p class="listener-filed__title" key="validation-title">-->
+    <!--        <span><Icon icon="ep:menu" />约束条件列表:</span>-->
+    <!--        <el-button type="primary" @click="openFieldOptionForm(null, -1, 'constraint')"-->
+    <!--          >添加约束</el-button-->
+    <!--        >-->
+    <!--      </p>-->
+    <!--      <el-table :data="fieldConstraintsList" key="validation-table" max-height="240" fit border>-->
+    <!--        <el-table-column label="序号" width="50px" type="index" />-->
+    <!--        <el-table-column label="约束名称" prop="name" min-width="100px" show-overflow-tooltip />-->
+    <!--        <el-table-column label="约束配置" prop="config" min-width="100px" show-overflow-tooltip />-->
+    <!--        <el-table-column label="操作" width="90px">-->
+    <!--          <template #default="scope">-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              @click="openFieldOptionForm(scope, scope.$index, 'constraint')"-->
+    <!--              >编辑</el-button-->
+    <!--            >-->
+    <!--            <el-divider direction="vertical" />-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              style="color: #ff4d4f"-->
+    <!--              @click="removeFieldOptionItem(scope, scope.$index, 'constraint')"-->
+    <!--              >移除</el-button-->
+    <!--            >-->
+    <!--          </template>-->
+    <!--        </el-table-column>-->
+    <!--      </el-table>-->
+
+    <!--      &lt;!&ndash; 表单属性 &ndash;&gt;-->
+    <!--      <el-divider key="property-divider" />-->
+    <!--      <p class="listener-filed__title" key="property-title">-->
+    <!--        <span><Icon icon="ep:menu" />字段属性列表:</span>-->
+    <!--        <el-button type="primary" @click="openFieldOptionForm(null, -1, 'property')"-->
+    <!--          >添加属性</el-button-->
+    <!--        >-->
+    <!--      </p>-->
+    <!--      <el-table :data="fieldPropertiesList" key="property-table" max-height="240" fit border>-->
+    <!--        <el-table-column label="序号" width="50px" type="index" />-->
+    <!--        <el-table-column label="属性编号" prop="id" min-width="100px" show-overflow-tooltip />-->
+    <!--        <el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip />-->
+    <!--        <el-table-column label="操作" width="90px">-->
+    <!--          <template #default="scope">-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              @click="openFieldOptionForm(scope, scope.$index, 'property')"-->
+    <!--              >编辑</el-button-->
+    <!--            >-->
+    <!--            <el-divider direction="vertical" />-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              style="color: #ff4d4f"-->
+    <!--              @click="removeFieldOptionItem(scope, scope.$index, 'property')"-->
+    <!--              >移除</el-button-->
+    <!--            >-->
+    <!--          </template>-->
+    <!--        </el-table-column>-->
+    <!--      </el-table>-->
+
+    <!--      &lt;!&ndash; 底部按钮 &ndash;&gt;-->
+    <!--      <div class="element-drawer__button">-->
+    <!--        <el-button>取 消</el-button>-->
+    <!--        <el-button type="primary" @click="saveField">保 存</el-button>-->
+    <!--      </div>-->
+    <!--    </el-drawer>-->
+
+    <!--    <el-dialog-->
+    <!--      v-model="fieldOptionModelVisible"-->
+    <!--      :title="optionModelTitle"-->
+    <!--      width="600px"-->
+    <!--      append-to-body-->
+    <!--      destroy-on-close-->
+    <!--    >-->
+    <!--      <el-form :model="fieldOptionForm" label-width="96px">-->
+    <!--        <el-form-item label="编号/ID" v-if="fieldOptionType !== 'constraint'" key="option-id">-->
+    <!--          <el-input v-model="fieldOptionForm.id" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="名称" v-if="fieldOptionType !== 'property'" key="option-name">-->
+    <!--          <el-input v-model="fieldOptionForm.name" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="配置" v-if="fieldOptionType === 'constraint'" key="option-config">-->
+    <!--          <el-input v-model="fieldOptionForm.config" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="值" v-if="fieldOptionType === 'property'" key="option-value">-->
+    <!--          <el-input v-model="fieldOptionForm.value" clearable />-->
+    <!--        </el-form-item>-->
+    <!--      </el-form>-->
+    <!--      <template #footer>-->
+    <!--        <el-button @click="fieldOptionModelVisible = false">取 消</el-button>-->
+    <!--        <el-button type="primary" @click="saveFieldOption">确 定</el-button>-->
+    <!--      </template>-->
+    <!--    </el-dialog>-->
+  </div>
+</template>
+
+<script lang="ts" setup>
+import * as FormApi from '@/api/bpm/form'
+
+defineOptions({ name: 'ElementForm' })
+
+const props = defineProps({
+  id: String,
+  type: String
+})
+const prefix = inject('prefix')
+const width = inject('width')
+
+const formKey = ref('')
+const businessKey = ref('')
+const optionModelTitle = ref('')
+const fieldList = ref<any[]>([])
+const formFieldForm = ref<any>({})
+const fieldType = ref({
+  long: '长整型',
+  string: '字符串',
+  boolean: '布尔类',
+  date: '日期类',
+  enum: '枚举类',
+  custom: '自定义类型'
+})
+const formFieldIndex = ref(-1) // 编辑中的字段, -1 为新增
+const formFieldOptionIndex = ref(-1) // 编辑中的字段配置项, -1 为新增
+const fieldModelVisible = ref(false)
+const fieldOptionModelVisible = ref(false)
+const fieldOptionForm = ref<any>({}) // 当前激活的字段配置项数据
+const fieldOptionType = ref('') // 当前激活的字段配置项弹窗 类型
+const fieldEnumList = ref<any[]>([]) // 枚举值列表
+const fieldConstraintsList = ref<any[]>([]) // 约束条件列表
+const fieldPropertiesList = ref<any[]>([]) // 绑定属性列表
+const bpmnELement = ref()
+const elExtensionElements = ref()
+const formData = ref()
+const otherExtensions = ref()
+
+const bpmnInstances = () => (window as any)?.bpmnInstances
+const resetFormList = () => {
+  bpmnELement.value = bpmnInstances().bpmnElement
+  formKey.value = bpmnELement.value.businessObject.formKey
+  if (formKey.value?.length > 0) {
+    formKey.value = parseInt(formKey.value)
+  }
+  // 获取元素扩展属性 或者 创建扩展属性
+  elExtensionElements.value =
+    bpmnELement.value.businessObject.get('extensionElements') ||
+    bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
+  // 获取元素表单配置 或者 创建新的表单配置
+  formData.value =
+    elExtensionElements.value.values.filter((ex) => ex.$type === `${prefix}:FormData`)?.[0] ||
+    bpmnInstances().moddle.create(`${prefix}:FormData`, { fields: [] })
+
+  // 业务标识 businessKey, 绑定在 formData 中
+  businessKey.value = formData.value.businessKey
+
+  // 保留剩余扩展元素,便于后面更新该元素对应属性
+  otherExtensions.value = elExtensionElements.value.values.filter(
+    (ex) => ex.$type !== `${prefix}:FormData`
+  )
+
+  // 复制原始值,填充表格
+  fieldList.value = JSON.parse(JSON.stringify(formData.value.fields || []))
+
+  // 更新元素扩展属性,避免后续报错
+  updateElementExtensions()
+}
+const updateElementFormKey = () => {
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnELement.value), {
+    formKey: formKey.value
+  })
+}
+const updateElementBusinessKey = () => {
+  bpmnInstances().modeling.updateModdleProperties(toRaw(bpmnELement.value), formData.value, {
+    businessKey: businessKey.value
+  })
+}
+// 根据类型调整字段type
+const changeFieldTypeType = (type) => {
+  // this.$set(this.formFieldForm, "type", type === "custom" ? "" : type);
+  formFieldForm.value['type'] = type === 'custom' ? '' : type
+}
+
+// 打开字段详情侧边栏
+const openFieldForm = (field, index) => {
+  formFieldIndex.value = index
+  if (index !== -1) {
+    const FieldObject = formData.value.fields[index]
+    formFieldForm.value = JSON.parse(JSON.stringify(field))
+    // 设置自定义类型
+    // this.$set(this.formFieldForm, "typeType", !this.fieldType[field.type] ? "custom" : field.type);
+    formFieldForm.value['typeType'] = !fieldType.value[field.type] ? 'custom' : field.type
+    // 初始化枚举值列表
+    field.type === 'enum' &&
+      (fieldEnumList.value = JSON.parse(JSON.stringify(FieldObject?.values || [])))
+    // 初始化约束条件列表
+    fieldConstraintsList.value = JSON.parse(
+      JSON.stringify(FieldObject?.validation?.constraints || [])
+    )
+    // 初始化自定义属性列表
+    fieldPropertiesList.value = JSON.parse(JSON.stringify(FieldObject?.properties?.values || []))
+  } else {
+    formFieldForm.value = {}
+    // 初始化枚举值列表
+    fieldEnumList.value = []
+    // 初始化约束条件列表
+    fieldConstraintsList.value = []
+    // 初始化自定义属性列表
+    fieldPropertiesList.value = []
+  }
+  fieldModelVisible.value = true
+}
+// 打开字段 某个 配置项 弹窗
+const openFieldOptionForm = (option, index, type) => {
+  fieldOptionModelVisible.value = true
+  fieldOptionType.value = type
+  formFieldOptionIndex.value = index
+  if (type === 'property') {
+    fieldOptionForm.value = option ? JSON.parse(JSON.stringify(option)) : {}
+    return (optionModelTitle.value = '属性配置')
+  }
+  if (type === 'enum') {
+    fieldOptionForm.value = option ? JSON.parse(JSON.stringify(option)) : {}
+    return (optionModelTitle.value = '枚举值配置')
+  }
+  fieldOptionForm.value = option ? JSON.parse(JSON.stringify(option)) : {}
+  return (optionModelTitle.value = '约束条件配置')
+}
+
+// 保存字段 某个 配置项
+const saveFieldOption = () => {
+  if (formFieldOptionIndex.value === -1) {
+    if (fieldOptionType.value === 'property') {
+      fieldPropertiesList.value.push(fieldOptionForm.value)
+    }
+    if (fieldOptionType.value === 'constraint') {
+      fieldConstraintsList.value.push(fieldOptionForm.value)
+    }
+    if (fieldOptionType.value === 'enum') {
+      fieldEnumList.value.push(fieldOptionForm.value)
+    }
+  } else {
+    fieldOptionType.value === 'property' &&
+      fieldPropertiesList.value.splice(formFieldOptionIndex.value, 1, fieldOptionForm.value)
+    fieldOptionType.value === 'constraint' &&
+      fieldConstraintsList.value.splice(formFieldOptionIndex.value, 1, fieldOptionForm.value)
+    fieldOptionType.value === 'enum' &&
+      fieldEnumList.value.splice(formFieldOptionIndex.value, 1, fieldOptionForm.value)
+  }
+  fieldOptionModelVisible.value = false
+  fieldOptionForm.value = {}
+}
+// 保存字段配置
+const saveField = () => {
+  const { id, type, label, defaultValue, datePattern } = formFieldForm.value
+  const Field = bpmnInstances().moddle.create(`${prefix}:FormField`, { id, type, label })
+  defaultValue && (Field.defaultValue = defaultValue)
+  datePattern && (Field.datePattern = datePattern)
+  // 构建属性
+  if (fieldPropertiesList.value && fieldPropertiesList.value.length) {
+    const fieldPropertyList = fieldPropertiesList.value.map((fp) => {
+      return bpmnInstances().moddle.create(`${prefix}:Property`, {
+        id: fp.id,
+        value: fp.value
+      })
+    })
+    Field.properties = bpmnInstances().moddle.create(`${prefix}:Properties`, {
+      values: fieldPropertyList
+    })
+  }
+  // 构建校验规则
+  if (fieldConstraintsList.value && fieldConstraintsList.value.length) {
+    const fieldConstraintList = fieldConstraintsList.value.map((fc) => {
+      return bpmnInstances().moddle.create(`${prefix}:Constraint`, {
+        name: fc.name,
+        config: fc.config
+      })
+    })
+    Field.validation = bpmnInstances().moddle.create(`${prefix}:Validation`, {
+      constraints: fieldConstraintList
+    })
+  }
+  // 构建枚举值
+  if (fieldEnumList.value && fieldEnumList.value.length) {
+    Field.values = fieldEnumList.value.map((fe) => {
+      return bpmnInstances().moddle.create(`${prefix}:Value`, { name: fe.name, id: fe.id })
+    })
+  }
+  // 更新数组 与 表单配置实例
+  if (formFieldIndex.value === -1) {
+    fieldList.value.push(formFieldForm.value)
+    formData.value.fields.push(Field)
+  } else {
+    fieldList.value.splice(formFieldIndex.value, 1, formFieldForm.value)
+    formData.value.fields.splice(formFieldIndex.value, 1, Field)
+  }
+  updateElementExtensions()
+  fieldModelVisible.value = false
+}
+
+// 移除某个 字段的 配置项
+const removeFieldOptionItem = (option, index, type) => {
+  // console.log(option, 'option')
+  if (type === 'property') {
+    fieldPropertiesList.value.splice(index, 1)
+    return
+  }
+  if (type === 'enum') {
+    fieldEnumList.value.splice(index, 1)
+    return
+  }
+  fieldConstraintsList.value.splice(index, 1)
+}
+// 移除 字段
+const removeField = (field, index) => {
+  console.log(field, 'field')
+  fieldList.value.splice(index, 1)
+  formData.value.fields.splice(index, 1)
+  updateElementExtensions()
+}
+
+const updateElementExtensions = () => {
+  // 更新回扩展元素
+  const newElExtensionElements = bpmnInstances().moddle.create(`bpmn:ExtensionElements`, {
+    values: otherExtensions.value.concat(formData.value)
+  })
+  // 更新到元素上
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnELement.value), {
+    extensionElements: newElExtensionElements
+  })
+}
+
+const formList = ref([]) // 流程表单的下拉框的数据
+onMounted(async () => {
+  formList.value = await FormApi.getFormSimpleList()
+})
+
+watch(
+  () => props.id,
+  (val) => {
+    val &&
+      val.length &&
+      nextTick(() => {
+        resetFormList()
+      })
+  },
+  { immediate: true }
+)
+</script>

+ 448 - 0
src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue

@@ -0,0 +1,448 @@
+<template>
+  <div class="panel-tab__content">
+    <el-table :data="elementListenersList" size="small" border>
+      <el-table-column label="序号" width="50px" type="index" />
+      <el-table-column label="事件类型" min-width="100px" prop="event" />
+      <el-table-column
+        label="监听器类型"
+        min-width="100px"
+        show-overflow-tooltip
+        :formatter="(row) => listenerTypeObject[row.listenerType]"
+      />
+      <el-table-column label="操作" width="100px">
+        <template #default="scope">
+          <el-button size="small" link @click="openListenerForm(scope.row, scope.$index)"
+            >编辑</el-button
+          >
+          <el-divider direction="vertical" />
+          <el-button size="small" link style="color: #ff4d4f" @click="removeListener(scope.$index)"
+            >移除</el-button
+          >
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="element-drawer__button">
+      <XButton
+        type="primary"
+        preIcon="ep:plus"
+        title="添加监听器"
+        size="small"
+        @click="openListenerForm(null)"
+      />
+      <XButton
+        type="success"
+        preIcon="ep:select"
+        title="选择监听器"
+        size="small"
+        @click="openProcessListenerDialog"
+      />
+    </div>
+
+    <!-- 监听器 编辑/创建 部分 -->
+    <el-drawer
+      v-model="listenerFormModelVisible"
+      title="执行监听器"
+      :size="`${width}px`"
+      append-to-body
+      destroy-on-close
+    >
+      <el-form :model="listenerForm" label-width="96px" ref="listenerFormRef">
+        <el-form-item
+          label="事件类型"
+          prop="event"
+          :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-select v-model="listenerForm.event">
+            <el-option label="start" value="start" />
+            <el-option label="end" value="end" />
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          label="监听器类型"
+          prop="listenerType"
+          :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-select v-model="listenerForm.listenerType">
+            <el-option
+              v-for="i in Object.keys(listenerTypeObject)"
+              :key="i"
+              :label="listenerTypeObject[i]"
+              :value="i"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          v-if="listenerForm.listenerType === 'classListener'"
+          label="Java类"
+          prop="class"
+          key="listener-class"
+          :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-input v-model="listenerForm.class" clearable />
+        </el-form-item>
+        <el-form-item
+          v-if="listenerForm.listenerType === 'expressionListener'"
+          label="表达式"
+          prop="expression"
+          key="listener-expression"
+          :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-input v-model="listenerForm.expression" clearable />
+        </el-form-item>
+        <el-form-item
+          v-if="listenerForm.listenerType === 'delegateExpressionListener'"
+          label="代理表达式"
+          prop="delegateExpression"
+          key="listener-delegate"
+          :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-input v-model="listenerForm.delegateExpression" clearable />
+        </el-form-item>
+        <template v-if="listenerForm.listenerType === 'scriptListener'">
+          <el-form-item
+            label="脚本格式"
+            prop="scriptFormat"
+            key="listener-script-format"
+            :rules="{ required: true, trigger: ['blur', 'change'], message: '请填写脚本格式' }"
+          >
+            <el-input v-model="listenerForm.scriptFormat" clearable />
+          </el-form-item>
+          <el-form-item
+            label="脚本类型"
+            prop="scriptType"
+            key="listener-script-type"
+            :rules="{ required: true, trigger: ['blur', 'change'], message: '请选择脚本类型' }"
+          >
+            <el-select v-model="listenerForm.scriptType">
+              <el-option label="内联脚本" value="inlineScript" />
+              <el-option label="外部脚本" value="externalScript" />
+            </el-select>
+          </el-form-item>
+          <el-form-item
+            v-if="listenerForm.scriptType === 'inlineScript'"
+            label="脚本内容"
+            prop="value"
+            key="listener-script"
+            :rules="{ required: true, trigger: ['blur', 'change'], message: '请填写脚本内容' }"
+          >
+            <el-input v-model="listenerForm.value" clearable />
+          </el-form-item>
+          <el-form-item
+            v-if="listenerForm.scriptType === 'externalScript'"
+            label="资源地址"
+            prop="resource"
+            key="listener-resource"
+            :rules="{ required: true, trigger: ['blur', 'change'], message: '请填写资源地址' }"
+          >
+            <el-input v-model="listenerForm.resource" clearable />
+          </el-form-item>
+        </template>
+      </el-form>
+      <el-divider />
+      <p class="listener-filed__title">
+        <span><Icon icon="ep:menu" />注入字段:</span>
+        <XButton type="primary" @click="openListenerFieldForm(null)" title="添加字段" />
+      </p>
+      <el-table
+        :data="fieldsListOfListener"
+        size="small"
+        max-height="240"
+        fit
+        border
+        style="flex: none"
+      >
+        <el-table-column label="序号" width="50px" type="index" />
+        <el-table-column label="字段名称" min-width="100px" prop="name" />
+        <el-table-column
+          label="字段类型"
+          min-width="80px"
+          show-overflow-tooltip
+          :formatter="(row) => fieldTypeObject[row.fieldType]"
+        />
+        <el-table-column
+          label="字段值/表达式"
+          min-width="100px"
+          show-overflow-tooltip
+          :formatter="(row) => row.string || row.expression"
+        />
+        <el-table-column label="操作" width="130px">
+          <template #default="scope">
+            <el-button size="small" link @click="openListenerFieldForm(scope.row, scope.$index)"
+              >编辑</el-button
+            >
+            <el-divider direction="vertical" />
+            <el-button
+              size="small"
+              link
+              style="color: #ff4d4f"
+              @click="removeListenerField(scope.$index)"
+              >移除</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="element-drawer__button">
+        <el-button @click="listenerFormModelVisible = false">取 消</el-button>
+        <el-button type="primary" @click="saveListenerConfig">保 存</el-button>
+      </div>
+    </el-drawer>
+
+    <!-- 注入西段 编辑/创建 部分 -->
+    <el-dialog
+      title="字段配置"
+      v-model="listenerFieldFormModelVisible"
+      width="600px"
+      append-to-body
+      destroy-on-close
+    >
+      <el-form
+        :model="listenerFieldForm"
+        label-width="96spx"
+        ref="listenerFieldFormRef"
+        style="height: 136px"
+      >
+        <el-form-item
+          label="字段名称:"
+          prop="name"
+          :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-input v-model="listenerFieldForm.name" clearable />
+        </el-form-item>
+        <el-form-item
+          label="字段类型:"
+          prop="fieldType"
+          :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-select v-model="listenerFieldForm.fieldType">
+            <el-option
+              v-for="i in Object.keys(fieldTypeObject)"
+              :key="i"
+              :label="fieldTypeObject[i]"
+              :value="i"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          v-if="listenerFieldForm.fieldType === 'string'"
+          label="字段值:"
+          prop="string"
+          key="field-string"
+          :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-input v-model="listenerFieldForm.string" clearable />
+        </el-form-item>
+        <el-form-item
+          v-if="listenerFieldForm.fieldType === 'expression'"
+          label="表达式:"
+          prop="expression"
+          key="field-expression"
+          :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-input v-model="listenerFieldForm.expression" clearable />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button size="small" @click="listenerFieldFormModelVisible = false">取 消</el-button>
+        <el-button size="small" type="primary" @click="saveListenerFiled">确 定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+
+  <!-- 选择弹窗 -->
+  <ProcessListenerDialog ref="processListenerDialogRef" @select="selectProcessListener" />
+</template>
+<script lang="ts" setup>
+import { ElMessageBox } from 'element-plus'
+import { createListenerObject, updateElementExtensions } from '../../utils'
+import {
+  initListenerType,
+  initListenerForm,
+  listenerType,
+  fieldType,
+  initListenerForm2
+} from './utilSelf'
+import ProcessListenerDialog from './ProcessListenerDialog.vue'
+
+defineOptions({ name: 'ElementListeners' })
+
+const props = defineProps({
+  id: String,
+  type: String
+})
+const prefix = inject('prefix')
+const width = inject('width')
+const elementListenersList = ref<any[]>([]) // 监听器列表
+const listenerForm = ref<any>({}) // 监听器详情表单
+const listenerFormModelVisible = ref(false) // 监听器 编辑 侧边栏显示状态
+const fieldsListOfListener = ref<any[]>([])
+const listenerFieldForm = ref<any>({}) // 监听器 注入字段 详情表单
+const listenerFieldFormModelVisible = ref(false) // 监听器 注入字段表单弹窗 显示状态
+const editingListenerIndex = ref(-1) // 监听器所在下标,-1 为新增
+const editingListenerFieldIndex = ref(-1) // 字段所在下标,-1 为新增
+const listenerTypeObject = ref(listenerType)
+const fieldTypeObject = ref(fieldType)
+const bpmnElement = ref()
+const otherExtensionList = ref()
+const bpmnElementListeners = ref()
+const listenerFormRef = ref()
+const listenerFieldFormRef = ref()
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+const resetListenersList = () => {
+  bpmnElement.value = bpmnInstances().bpmnElement
+  otherExtensionList.value = []
+  bpmnElementListeners.value =
+    bpmnElement.value.businessObject?.extensionElements?.values?.filter(
+      (ex) => ex.$type === `${prefix}:ExecutionListener`
+    ) ?? []
+  elementListenersList.value = bpmnElementListeners.value.map((listener) =>
+    initListenerType(listener)
+  )
+}
+// 打开 监听器详情 侧边栏
+const openListenerForm = (listener, index?) => {
+  // debugger
+  if (listener) {
+    listenerForm.value = initListenerForm(listener)
+    editingListenerIndex.value = index
+  } else {
+    listenerForm.value = {}
+    editingListenerIndex.value = -1 // 标记为新增
+  }
+  if (listener && listener.fields) {
+    fieldsListOfListener.value = listener.fields.map((field) => ({
+      ...field,
+      fieldType: field.string ? 'string' : 'expression'
+    }))
+  } else {
+    fieldsListOfListener.value = []
+    listenerForm.value['fields'] = []
+  }
+  // 打开侧边栏并清楚验证状态
+  listenerFormModelVisible.value = true
+  nextTick(() => {
+    if (listenerFormRef.value) {
+      listenerFormRef.value.clearValidate()
+    }
+  })
+}
+// 打开监听器字段编辑弹窗
+const openListenerFieldForm = (field, index?) => {
+  listenerFieldForm.value = field ? JSON.parse(JSON.stringify(field)) : {}
+  editingListenerFieldIndex.value = field ? index : -1
+  listenerFieldFormModelVisible.value = true
+  nextTick(() => {
+    if (listenerFieldFormRef.value) {
+      listenerFieldFormRef.value.clearValidate()
+    }
+  })
+}
+// 保存监听器注入字段
+const saveListenerFiled = async () => {
+  // debugger
+  let validateStatus = await listenerFieldFormRef.value.validate()
+  if (!validateStatus) return // 验证不通过直接返回
+  if (editingListenerFieldIndex.value === -1) {
+    fieldsListOfListener.value.push(listenerFieldForm.value)
+    listenerForm.value.fields.push(listenerFieldForm.value)
+  } else {
+    fieldsListOfListener.value.splice(editingListenerFieldIndex.value, 1, listenerFieldForm.value)
+    listenerForm.value.fields.splice(editingListenerFieldIndex.value, 1, listenerFieldForm.value)
+  }
+  listenerFieldFormModelVisible.value = false
+  nextTick(() => {
+    listenerFieldForm.value = {}
+  })
+}
+// 移除监听器字段
+const removeListenerField = (index) => {
+  // debugger
+  ElMessageBox.confirm('确认移除该字段吗?', '提示', {
+    confirmButtonText: '确 认',
+    cancelButtonText: '取 消'
+  })
+    .then(() => {
+      fieldsListOfListener.value.splice(index, 1)
+      listenerForm.value.fields.splice(index, 1)
+    })
+    .catch(() => console.info('操作取消'))
+}
+// 移除监听器
+const removeListener = (index) => {
+  debugger
+  ElMessageBox.confirm('确认移除该监听器吗?', '提示', {
+    confirmButtonText: '确 认',
+    cancelButtonText: '取 消'
+  })
+    .then(() => {
+      bpmnElementListeners.value.splice(index, 1)
+      elementListenersList.value.splice(index, 1)
+      updateElementExtensions(
+        bpmnElement.value,
+        otherExtensionList.value.concat(bpmnElementListeners.value)
+      )
+    })
+    .catch(() => console.info('操作取消'))
+}
+// 保存监听器配置
+const saveListenerConfig = async () => {
+  // debugger
+  let validateStatus = await listenerFormRef.value.validate()
+  if (!validateStatus) return // 验证不通过直接返回
+  const listenerObject = createListenerObject(listenerForm.value, false, prefix)
+  if (editingListenerIndex.value === -1) {
+    bpmnElementListeners.value.push(listenerObject)
+    elementListenersList.value.push(listenerForm.value)
+  } else {
+    bpmnElementListeners.value.splice(editingListenerIndex.value, 1, listenerObject)
+    elementListenersList.value.splice(editingListenerIndex.value, 1, listenerForm.value)
+  }
+  // 保存其他配置
+  otherExtensionList.value =
+    bpmnElement.value.businessObject?.extensionElements?.values?.filter(
+      (ex) => ex.$type !== `${prefix}:ExecutionListener`
+    ) ?? []
+  updateElementExtensions(
+    bpmnElement.value,
+    otherExtensionList.value.concat(bpmnElementListeners.value)
+  )
+  // 4. 隐藏侧边栏
+  listenerFormModelVisible.value = false
+  listenerForm.value = {}
+}
+
+// 打开监听器弹窗
+const processListenerDialogRef = ref()
+const openProcessListenerDialog = async () => {
+  processListenerDialogRef.value.open('execution')
+}
+const selectProcessListener = (listener) => {
+  const listenerForm = initListenerForm2(listener)
+  const listenerObject = createListenerObject(listenerForm, false, prefix)
+  bpmnElementListeners.value.push(listenerObject)
+  elementListenersList.value.push(listenerForm)
+
+  // 保存其他配置
+  otherExtensionList.value =
+    bpmnElement.value.businessObject?.extensionElements?.values?.filter(
+      (ex) => ex.$type !== `${prefix}:ExecutionListener`
+    ) ?? []
+  updateElementExtensions(
+    bpmnElement.value,
+    otherExtensionList.value.concat(bpmnElementListeners.value)
+  )
+}
+
+watch(
+  () => props.id,
+  (val) => {
+    val &&
+      val.length &&
+      nextTick(() => {
+        resetListenersList()
+      })
+  },
+  { immediate: true }
+)
+</script>

+ 280 - 0
src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue

@@ -0,0 +1,280 @@
+<template>
+  <div class="panel-tab__content">
+    <el-form label-width="90px">
+      <el-form-item label="快捷配置">
+        <el-button size="small" @click="changeConfig('依次审批')">依次审批</el-button>
+        <el-button size="small" @click="changeConfig('会签')">会签</el-button>
+        <el-button size="small" @click="changeConfig('或签')">或签</el-button>
+      </el-form-item>
+      <el-form-item label="会签类型">
+        <el-select v-model="loopCharacteristics" @change="changeLoopCharacteristicsType">
+          <el-option label="并行多重事件" value="ParallelMultiInstance" />
+          <el-option label="时序多重事件" value="SequentialMultiInstance" />
+          <el-option label="无" value="Null" />
+        </el-select>
+      </el-form-item>
+      <template
+        v-if="
+          loopCharacteristics === 'ParallelMultiInstance' ||
+          loopCharacteristics === 'SequentialMultiInstance'
+        "
+      >
+        <el-form-item label="循环数量" key="loopCardinality">
+          <el-input
+            v-model="loopInstanceForm.loopCardinality"
+            clearable
+            @change="updateLoopCardinality"
+          />
+        </el-form-item>
+        <el-form-item label="集合" key="collection" v-show="false">
+          <el-input v-model="loopInstanceForm.collection" clearable @change="updateLoopBase" />
+        </el-form-item>
+        <!-- add by 芋艿:由于「元素变量」暂时用不到,所以这里 display 为 none -->
+        <el-form-item label="元素变量" key="elementVariable" style="display: none">
+          <el-input v-model="loopInstanceForm.elementVariable" clearable @change="updateLoopBase" />
+        </el-form-item>
+        <el-form-item label="完成条件" key="completionCondition">
+          <el-input
+            v-model="loopInstanceForm.completionCondition"
+            clearable
+            @change="updateLoopCondition"
+          />
+        </el-form-item>
+        <!-- add by 芋艿:由于「异步状态」暂时用不到,所以这里 display 为 none -->
+        <el-form-item label="异步状态" key="async" style="display: none">
+          <el-checkbox
+            v-model="loopInstanceForm.asyncBefore"
+            label="异步前"
+            @change="updateLoopAsync('asyncBefore')"
+          />
+          <el-checkbox
+            v-model="loopInstanceForm.asyncAfter"
+            label="异步后"
+            @change="updateLoopAsync('asyncAfter')"
+          />
+          <el-checkbox
+            v-model="loopInstanceForm.exclusive"
+            v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
+            label="排除"
+            @change="updateLoopAsync('exclusive')"
+          />
+        </el-form-item>
+        <el-form-item
+          label="重试周期"
+          prop="timeCycle"
+          v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
+          key="timeCycle"
+        >
+          <el-input v-model="loopInstanceForm.timeCycle" clearable @change="updateLoopTimeCycle" />
+        </el-form-item>
+      </template>
+    </el-form>
+  </div>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'ElementMultiInstance' })
+
+const props = defineProps({
+  businessObject: Object,
+  type: String
+})
+const prefix = inject('prefix')
+const loopCharacteristics = ref('')
+//默认配置,用来覆盖原始不存在的选项,避免报错
+const defaultLoopInstanceForm = ref({
+  completionCondition: '',
+  loopCardinality: '',
+  extensionElements: [],
+  asyncAfter: false,
+  asyncBefore: false,
+  exclusive: false
+})
+const loopInstanceForm = ref<any>({})
+const bpmnElement = ref(null)
+const multiLoopInstance = ref(null)
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+const getElementLoop = (businessObject) => {
+  if (!businessObject.loopCharacteristics) {
+    loopCharacteristics.value = 'Null'
+    loopInstanceForm.value = {}
+    return
+  }
+  if (businessObject.loopCharacteristics.$type === 'bpmn:StandardLoopCharacteristics') {
+    loopCharacteristics.value = 'StandardLoop'
+    loopInstanceForm.value = {}
+    return
+  }
+  if (businessObject.loopCharacteristics.isSequential) {
+    loopCharacteristics.value = 'SequentialMultiInstance'
+  } else {
+    loopCharacteristics.value = 'ParallelMultiInstance'
+  }
+  // 合并配置
+  loopInstanceForm.value = {
+    ...defaultLoopInstanceForm.value,
+    ...businessObject.loopCharacteristics,
+    completionCondition: businessObject.loopCharacteristics?.completionCondition?.body ?? '',
+    loopCardinality: businessObject.loopCharacteristics?.loopCardinality?.body ?? ''
+  }
+  // 保留当前元素 businessObject 上的 loopCharacteristics 实例
+  multiLoopInstance.value = bpmnInstances().bpmnElement.businessObject.loopCharacteristics
+  // 更新表单
+  if (
+    businessObject.loopCharacteristics.extensionElements &&
+    businessObject.loopCharacteristics.extensionElements.values &&
+    businessObject.loopCharacteristics.extensionElements.values.length
+  ) {
+    loopInstanceForm.value['timeCycle'] =
+      businessObject.loopCharacteristics.extensionElements.values[0].body
+  }
+}
+
+const changeLoopCharacteristicsType = (type) => {
+  // this.loopInstanceForm = { ...this.defaultLoopInstanceForm }; // 切换类型取消原表单配置
+  // 取消多实例配置
+  if (type === 'Null') {
+    bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+      loopCharacteristics: null
+    })
+    return
+  }
+  // 配置循环
+  if (type === 'StandardLoop') {
+    const loopCharacteristicsObject = bpmnInstances().moddle.create(
+      'bpmn:StandardLoopCharacteristics'
+    )
+    bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+      loopCharacteristics: loopCharacteristicsObject
+    })
+    multiLoopInstance.value = null
+    return
+  }
+  // 时序
+  if (type === 'SequentialMultiInstance') {
+    multiLoopInstance.value = bpmnInstances().moddle.create(
+      'bpmn:MultiInstanceLoopCharacteristics',
+      { isSequential: true }
+    )
+  } else {
+    multiLoopInstance.value = bpmnInstances().moddle.create(
+      'bpmn:MultiInstanceLoopCharacteristics',
+      { collection: '${coll_userList}' }
+    )
+  }
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+    loopCharacteristics: toRaw(multiLoopInstance.value)
+  })
+}
+
+// 循环基数
+const updateLoopCardinality = (cardinality) => {
+  let loopCardinality = null
+  if (cardinality && cardinality.length) {
+    loopCardinality = bpmnInstances().moddle.create('bpmn:FormalExpression', {
+      body: cardinality
+    })
+  }
+  bpmnInstances().modeling.updateModdleProperties(
+    toRaw(bpmnElement.value),
+    multiLoopInstance.value,
+    {
+      loopCardinality
+    }
+  )
+}
+
+// 完成条件
+const updateLoopCondition = (condition) => {
+  let completionCondition = null
+  if (condition && condition.length) {
+    completionCondition = bpmnInstances().moddle.create('bpmn:FormalExpression', {
+      body: condition
+    })
+  }
+  bpmnInstances().modeling.updateModdleProperties(
+    toRaw(bpmnElement.value),
+    multiLoopInstance.value,
+    {
+      completionCondition
+    }
+  )
+}
+
+// 重试周期
+const updateLoopTimeCycle = (timeCycle) => {
+  const extensionElements = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
+    values: [
+      bpmnInstances().moddle.create(`${prefix}:FailedJobRetryTimeCycle`, {
+        body: timeCycle
+      })
+    ]
+  })
+  bpmnInstances().modeling.updateModdleProperties(
+    toRaw(bpmnElement.value),
+    multiLoopInstance.value,
+    {
+      extensionElements
+    }
+  )
+}
+
+// 直接更新的基础信息
+const updateLoopBase = () => {
+  bpmnInstances().modeling.updateModdleProperties(
+    toRaw(bpmnElement.value),
+    multiLoopInstance.value,
+    {
+      collection: loopInstanceForm.value.collection || null,
+      elementVariable: loopInstanceForm.value.elementVariable || null
+    }
+  )
+}
+
+// 各异步状态
+const updateLoopAsync = (key) => {
+  const { asyncBefore, asyncAfter } = loopInstanceForm.value
+  let asyncAttr = Object.create(null)
+  if (!asyncBefore && !asyncAfter) {
+    // this.$set(this.loopInstanceForm, "exclusive", false);
+    loopInstanceForm.value['exclusive'] = false
+    asyncAttr = { asyncBefore: false, asyncAfter: false, exclusive: false, extensionElements: null }
+  } else {
+    asyncAttr[key] = loopInstanceForm.value[key]
+  }
+  bpmnInstances().modeling.updateModdleProperties(
+    toRaw(bpmnElement.value),
+    multiLoopInstance.value,
+    asyncAttr
+  )
+}
+
+const changeConfig = (config) => {
+  if (config === '依次审批') {
+    changeLoopCharacteristicsType('SequentialMultiInstance')
+    updateLoopCardinality('1')
+    updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }')
+  } else if (config === '会签') {
+    changeLoopCharacteristicsType('ParallelMultiInstance')
+    updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }')
+  } else if (config === '或签') {
+    changeLoopCharacteristicsType('ParallelMultiInstance')
+    updateLoopCondition('${ nrOfCompletedInstances > 0 }')
+  }
+}
+
+onBeforeUnmount(() => {
+  multiLoopInstance.value = null
+  bpmnElement.value = null
+})
+
+watch(
+  () => props.businessObject,
+  (val) => {
+    bpmnElement.value = bpmnInstances().bpmnElement
+    getElementLoop(val)
+  },
+  { immediate: true }
+)
+</script>

+ 55 - 0
src/components/bpmnProcessDesigner/package/penal/other/ElementOtherConfig.vue

@@ -0,0 +1,55 @@
+<template>
+  <div class="panel-tab__content">
+    <div class="element-property input-property">
+      <div class="element-property__label">元素文档:</div>
+      <div class="element-property__value">
+        <el-input
+          type="textarea"
+          v-model="documentation"
+          resize="vertical"
+          :autosize="{ minRows: 2, maxRows: 4 }"
+          @input="updateDocumentation"
+          @blur="updateDocumentation"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'ElementOtherConfig' })
+const props = defineProps({
+  id: String
+})
+const documentation = ref('')
+const bpmnElement = ref()
+const bpmnInstances = () => (window as any).bpmnInstances
+const updateDocumentation = () => {
+  ;(bpmnElement.value && bpmnElement.value.id === props.id) ||
+    (bpmnElement.value = bpmnInstances().elementRegistry.get(props.id))
+  const documentations = bpmnInstances().bpmnFactory.create('bpmn:Documentation', {
+    text: documentation.value
+  })
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+    documentation: [documentations]
+  })
+}
+onBeforeUnmount(() => {
+  bpmnElement.value = null
+})
+
+watch(
+  () => props.id,
+  (id) => {
+    if (id && id.length) {
+      nextTick(() => {
+        const documentations = bpmnInstances().bpmnElement.businessObject?.documentation
+        documentation.value = documentations && documentations.length ? documentations[0].text : ''
+      })
+    } else {
+      documentation.value = ''
+    }
+  },
+  { immediate: true }
+)
+</script>

+ 169 - 0
src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue

@@ -0,0 +1,169 @@
+<template>
+  <div class="panel-tab__content">
+    <el-table :data="elementPropertyList" max-height="240" fit border>
+      <el-table-column label="序号" width="50px" type="index" />
+      <el-table-column label="属性名" prop="name" min-width="100px" show-overflow-tooltip />
+      <el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip />
+      <el-table-column label="操作" width="110px">
+        <template #default="scope">
+          <el-button link @click="openAttributesForm(scope.row, scope.$index)" size="small">
+            编辑
+          </el-button>
+          <el-divider direction="vertical" />
+          <el-button
+            link
+            size="small"
+            style="color: #ff4d4f"
+            @click="removeAttributes(scope.row, scope.$index)"
+          >
+            移除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="element-drawer__button">
+      <XButton
+        type="primary"
+        preIcon="ep:plus"
+        title="添加属性"
+        @click="openAttributesForm(null, -1)"
+      />
+    </div>
+
+    <el-dialog
+      v-model="propertyFormModelVisible"
+      title="属性配置"
+      width="600px"
+      append-to-body
+      destroy-on-close
+    >
+      <el-form :model="propertyForm" label-width="80px" ref="attributeFormRef">
+        <el-form-item label="属性名:" prop="name">
+          <el-input v-model="propertyForm.name" clearable />
+        </el-form-item>
+        <el-form-item label="属性值:" prop="value">
+          <el-input v-model="propertyForm.value" clearable />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="propertyFormModelVisible = false">取 消</el-button>
+        <el-button type="primary" @click="saveAttribute">确 定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ElMessageBox } from 'element-plus'
+defineOptions({ name: 'ElementProperties' })
+const props = defineProps({
+  id: String,
+  type: String
+})
+const prefix = inject('prefix')
+// const width = inject('width')
+
+const elementPropertyList = ref<any[]>([])
+const propertyForm = ref<any>({})
+const editingPropertyIndex = ref(-1)
+const propertyFormModelVisible = ref(false)
+const bpmnElement = ref()
+const otherExtensionList = ref()
+const bpmnElementProperties = ref()
+const bpmnElementPropertyList = ref()
+const attributeFormRef = ref()
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+const resetAttributesList = () => {
+  console.log(window, 'windowwindowwindowwindowwindowwindowwindow')
+  bpmnElement.value = bpmnInstances().bpmnElement
+  otherExtensionList.value = [] // 其他扩展配置
+  bpmnElementProperties.value =
+    // bpmnElement.value.businessObject?.extensionElements?.filter((ex) => {
+    bpmnElement.value.businessObject?.extensionElements?.values.filter((ex) => {
+      if (ex.$type !== `${prefix}:Properties`) {
+        otherExtensionList.value.push(ex)
+      }
+      return ex.$type === `${prefix}:Properties`
+    }) ?? []
+
+  // 保存所有的 扩展属性字段
+  bpmnElementPropertyList.value = bpmnElementProperties.value.reduce(
+    (pre, current) => pre.concat(current.values),
+    []
+  )
+  // 复制 显示
+  elementPropertyList.value = JSON.parse(JSON.stringify(bpmnElementPropertyList.value ?? []))
+}
+const openAttributesForm = (attr, index) => {
+  editingPropertyIndex.value = index
+  propertyForm.value = index === -1 ? {} : JSON.parse(JSON.stringify(attr))
+  propertyFormModelVisible.value = true
+  nextTick(() => {
+    if (attributeFormRef.value) attributeFormRef.value.clearValidate()
+  })
+}
+const removeAttributes = (attr, index) => {
+  console.log(attr, 'attr')
+  ElMessageBox.confirm('确认移除该属性吗?', '提示', {
+    confirmButtonText: '确 认',
+    cancelButtonText: '取 消'
+  })
+    .then(() => {
+      elementPropertyList.value.splice(index, 1)
+      bpmnElementPropertyList.value.splice(index, 1)
+      // 新建一个属性字段的保存列表
+      const propertiesObject = bpmnInstances().moddle.create(`${prefix}:Properties`, {
+        values: bpmnElementPropertyList.value
+      })
+      updateElementExtensions(propertiesObject)
+      resetAttributesList()
+    })
+    .catch(() => console.info('操作取消'))
+}
+const saveAttribute = () => {
+  console.log(propertyForm.value, 'propertyForm.value')
+  const { name, value } = propertyForm.value
+  if (editingPropertyIndex.value !== -1) {
+    bpmnInstances().modeling.updateModdleProperties(
+      toRaw(bpmnElement.value),
+      toRaw(bpmnElementPropertyList.value)[toRaw(editingPropertyIndex.value)],
+      {
+        name,
+        value
+      }
+    )
+  } else {
+    // 新建属性字段
+    const newPropertyObject = bpmnInstances().moddle.create(`${prefix}:Property`, {
+      name,
+      value
+    })
+    // 新建一个属性字段的保存列表
+    const propertiesObject = bpmnInstances().moddle.create(`${prefix}:Properties`, {
+      values: bpmnElementPropertyList.value.concat([newPropertyObject])
+    })
+    updateElementExtensions(propertiesObject)
+  }
+  propertyFormModelVisible.value = false
+  resetAttributesList()
+}
+const updateElementExtensions = (properties) => {
+  const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
+    values: otherExtensionList.value.concat([properties])
+  })
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+    extensionElements: extensions
+  })
+}
+
+watch(
+  () => props.id,
+  (val) => {
+    if (val) {
+      val && val.length && resetAttributesList()
+    }
+  },
+  { immediate: true }
+)
+</script>

+ 87 - 0
src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue

@@ -0,0 +1,87 @@
+<template>
+  <div class="panel-tab__content">
+    <el-form size="small" label-width="90px">
+      <!-- add by 芋艿:由于「异步延续」暂时用不到,所以这里 display 为 none -->
+      <el-form-item label="异步延续" style="display: none">
+        <el-checkbox
+          v-model="taskConfigForm.asyncBefore"
+          label="异步前"
+          @change="changeTaskAsync"
+        />
+        <el-checkbox v-model="taskConfigForm.asyncAfter" label="异步后" @change="changeTaskAsync" />
+        <el-checkbox
+          v-model="taskConfigForm.exclusive"
+          v-if="taskConfigForm.asyncAfter || taskConfigForm.asyncBefore"
+          label="排除"
+          @change="changeTaskAsync"
+        />
+      </el-form-item>
+      <component :is="witchTaskComponent" v-bind="$props" />
+    </el-form>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import UserTask from './task-components/UserTask.vue'
+import ScriptTask from './task-components/ScriptTask.vue'
+import ReceiveTask from './task-components/ReceiveTask.vue'
+
+defineOptions({ name: 'ElementTaskConfig' })
+
+const props = defineProps({
+  id: String,
+  type: String
+})
+const taskConfigForm = ref({
+  asyncAfter: false,
+  asyncBefore: false,
+  exclusive: false
+})
+const witchTaskComponent = ref()
+const installedComponent = ref({
+  // 手工任务与普通任务一致,不需要其他配置
+  // 接收消息任务,需要在全局下插入新的消息实例,并在该节点下的 messageRef 属性绑定该实例
+  // 发送任务、服务任务、业务规则任务共用一个相同配置
+  UserTask: 'UserTask', // 用户任务配置
+  ScriptTask: 'ScriptTask', // 脚本任务配置
+  ReceiveTask: 'ReceiveTask' // 消息接收任务
+})
+const bpmnElement = ref()
+
+const bpmnInstances = () => (window as any).bpmnInstances
+const changeTaskAsync = () => {
+  if (!taskConfigForm.value.asyncBefore && !taskConfigForm.value.asyncAfter) {
+    taskConfigForm.value.exclusive = false
+  }
+  bpmnInstances().modeling.updateProperties(bpmnInstances().bpmnElement, {
+    ...taskConfigForm.value
+  })
+}
+
+watch(
+  () => props.id,
+  () => {
+    bpmnElement.value = bpmnInstances().bpmnElement
+    taskConfigForm.value.asyncBefore = bpmnElement.value?.businessObject?.asyncBefore
+    taskConfigForm.value.asyncAfter = bpmnElement.value?.businessObject?.asyncAfter
+    taskConfigForm.value.exclusive = bpmnElement.value?.businessObject?.exclusive
+  },
+  { immediate: true }
+)
+watch(
+  () => props.type,
+  () => {
+    // witchTaskComponent.value = installedComponent.value[props.type]
+    if (props.type == installedComponent.value.UserTask) {
+      witchTaskComponent.value = UserTask
+    }
+    if (props.type == installedComponent.value.ScriptTask) {
+      witchTaskComponent.value = ScriptTask
+    }
+    if (props.type == installedComponent.value.ReceiveTask) {
+      witchTaskComponent.value = ReceiveTask
+    }
+  },
+  { immediate: true }
+)
+</script>

+ 70 - 0
src/components/bpmnProcessDesigner/package/theme/element-variables.scss

@@ -0,0 +1,70 @@
+/* 改变主题色变量 */
+$--color-primary: #1890ff;
+$--color-danger: #ff4d4f;
+
+/* 改变 icon 字体路径变量,必需 */
+$--font-path: '~element-ui/lib/theme-chalk/fonts';
+
+@import '~element-ui/packages/theme-chalk/src/index';
+
+.el-table td,
+.el-table th {
+  color: #333;
+}
+.el-drawer__header {
+  padding: 16px 16px 8px 16px;
+  margin: 0;
+  line-height: 24px;
+  font-size: 18px;
+  color: #303133;
+  box-sizing: border-box;
+  border-bottom: 1px solid #e8e8e8;
+}
+div[class^='el-drawer']:focus,
+span:focus {
+  outline: none;
+}
+.el-drawer__body {
+  box-sizing: border-box;
+  padding: 16px;
+  width: 100%;
+  overflow-y: auto;
+}
+
+.el-dialog {
+  margin-top: 50vh !important;
+  transform: translateY(-50%);
+  overflow: hidden;
+}
+.el-dialog__wrapper {
+  overflow: hidden;
+  max-height: 100vh;
+}
+.el-dialog__header {
+  padding: 16px 16px 8px 16px;
+  box-sizing: border-box;
+  border-bottom: 1px solid #e8e8e8;
+}
+.el-dialog__body {
+  padding: 16px;
+  max-height: 80vh;
+  box-sizing: border-box;
+  overflow-y: auto;
+}
+.el-dialog__footer {
+  padding: 16px;
+  box-sizing: border-box;
+  border-top: 1px solid #e8e8e8;
+}
+.el-dialog__close {
+  font-weight: 600;
+}
+.el-select {
+  width: 100%;
+}
+.el-divider:not(.el-divider--horizontal) {
+  margin: 0 8px;
+}
+.el-divider.el-divider--horizontal {
+  margin: 16px 0;
+}

+ 6 - 0
src/config/axios/errorCode.ts

@@ -0,0 +1,6 @@
+export default {
+  '401': '认证失败,无法访问系统资源',
+  '403': '当前操作没有权限',
+  '404': '访问资源不存在',
+  default: '系统未知错误,请反馈给管理员'
+}

+ 457 - 0
src/locales/en.ts

@@ -0,0 +1,457 @@
+export default {
+  common: {
+    inputText: 'Please input',
+    selectText: 'Please select',
+    startTimeText: 'Start time',
+    endTimeText: 'End time',
+    login: 'Login',
+    required: 'This is required',
+    loginOut: 'Login out',
+    document: 'Document',
+    profile: 'User Center',
+    reminder: 'Reminder',
+    loginOutMessage: 'Exit the system?',
+    back: 'Back',
+    ok: 'OK',
+    save: 'Save',
+    cancel: 'Cancel',
+    close: 'Close',
+    reload: 'Reload current',
+    success: 'Success',
+    closeTab: 'Close current',
+    closeTheLeftTab: 'Close left',
+    closeTheRightTab: 'Close right',
+    closeOther: 'Close other',
+    closeAll: 'Close all',
+    prevLabel: 'Prev',
+    nextLabel: 'Next',
+    skipLabel: 'Jump',
+    doneLabel: 'End',
+    menu: 'Menu',
+    menuDes: 'Menu bar rendered in routed structure',
+    collapse: 'Collapse',
+    collapseDes: 'Expand and zoom the menu bar',
+    tagsView: 'Tags view',
+    tagsViewDes: 'Used to record routing history',
+    tool: 'Tool',
+    toolDes: 'Used to set up custom systems',
+    query: 'Query',
+    reset: 'Reset',
+    shrink: 'Put away',
+    expand: 'Expand',
+    confirmTitle: 'System Hint',
+    exportMessage: 'Whether to confirm export data item?',
+    importMessage: 'Whether to confirm import data item?',
+    createSuccess: 'Create Success',
+    updateSuccess: 'Update Success',
+    delMessage: 'Delete the selected data?',
+    delDataMessage: 'Delete the data?',
+    delNoData: 'Please select the data to delete',
+    delSuccess: 'Deleted successfully',
+    index: 'Index',
+    status: 'Status',
+    createTime: 'Create Time',
+    updateTime: 'Update Time',
+    copy: 'Copy',
+    copySuccess: 'Copy Success',
+    copyError: 'Copy Error'
+  },
+  lock: {
+    lockScreen: 'Lock screen',
+    lock: 'Lock',
+    lockPassword: 'Lock screen password',
+    unlock: 'Click to unlock',
+    backToLogin: 'Back to login',
+    entrySystem: 'Entry the system',
+    placeholder: 'Please enter the lock screen password',
+    message: 'Lock screen password error'
+  },
+  error: {
+    noPermission: `Sorry, you don't have permission to access this page.`,
+    pageError: 'Sorry, the page you visited does not exist.',
+    networkError: 'Sorry, the server reported an error.',
+    returnToHome: 'Return to home'
+  },
+  permission: {
+    hasPermission: `Please set the operation permission label value`,
+    hasRole: `Please set the role permission tag value`
+  },
+  setting: {
+    projectSetting: 'Project setting',
+    theme: 'Theme',
+    layout: 'Layout',
+    systemTheme: 'System theme',
+    menuTheme: 'Menu theme',
+    interfaceDisplay: 'Interface display',
+    breadcrumb: 'Breadcrumb',
+    breadcrumbIcon: 'Breadcrumb icon',
+    collapseMenu: 'Collapse menu',
+    hamburgerIcon: 'Hamburger icon',
+    screenfullIcon: 'Screenfull icon',
+    sizeIcon: 'Size icon',
+    localeIcon: 'Locale icon',
+    messageIcon: 'Message icon',
+    tagsView: 'Tags view',
+    logo: 'Logo',
+    greyMode: 'Grey mode',
+    fixedHeader: 'Fixed header',
+    headerTheme: 'Header theme',
+    cutMenu: 'Cut Menu',
+    copy: 'Copy',
+    clearAndReset: 'Clear cache and reset',
+    copySuccess: 'Copy success',
+    copyFailed: 'Copy failed',
+    footer: 'Footer',
+    uniqueOpened: 'Unique opened',
+    tagsViewIcon: 'Tags view icon',
+    reExperienced: 'Please exit the login experience again',
+    fixedMenu: 'Fixed menu'
+  },
+  size: {
+    default: 'Default',
+    large: 'Large',
+    small: 'Small'
+  },
+  login: {
+    welcome: 'Welcome to the system',
+    message: 'Backstage management system',
+    tenantname: 'TenantName',
+    username: 'Username',
+    password: 'Password',
+    code: 'verification code',
+    login: 'Sign in',
+    relogin: 'Sign in again',
+    otherLogin: 'Sign in with',
+    register: 'Register',
+    checkPassword: 'Confirm password',
+    remember: 'Remember me',
+    hasUser: 'Existing account? Go to login',
+    forgetPassword: 'Forget password?',
+    tenantNamePlaceholder: 'Please Enter Tenant Name',
+    usernamePlaceholder: 'Please Enter Username',
+    passwordPlaceholder: 'Please Enter Password',
+    codePlaceholder: 'Please Enter Verification Code',
+    mobileTitle: 'Mobile sign in',
+    mobileNumber: 'Mobile Number',
+    mobileNumberPlaceholder: 'Plaease Enter Mobile Number',
+    backLogin: 'back',
+    getSmsCode: 'Get SMS Code',
+    btnMobile: 'Mobile sign in',
+    btnQRCode: 'QR code sign in',
+    qrcode: 'Scan the QR code to log in',
+    btnRegister: 'Sign up',
+    SmsSendMsg: 'code has been sent'
+  },
+  captcha: {
+    verification: 'Please complete security verification',
+    slide: 'Swipe right to complete verification',
+    point: 'Please click',
+    success: 'Verification succeeded',
+    fail: 'verification failed'
+  },
+  router: {
+    login: 'Login',
+    home: 'Home',
+    analysis: 'Analysis',
+    workplace: 'Workplace'
+  },
+  analysis: {
+    newUser: 'New user',
+    unreadInformation: 'Unread information',
+    transactionAmount: 'Transaction amount',
+    totalShopping: 'Total Shopping',
+    monthlySales: 'Monthly sales',
+    userAccessSource: 'User access source',
+    january: 'January',
+    february: 'February',
+    march: 'March',
+    april: 'April',
+    may: 'May',
+    june: 'June',
+    july: 'July',
+    august: 'August',
+    september: 'September',
+    october: 'October',
+    november: 'November',
+    december: 'December',
+    estimate: 'Estimate',
+    actual: 'Actual',
+    directAccess: 'Airect access',
+    mailMarketing: 'Mail marketing',
+    allianceAdvertising: 'Alliance advertising',
+    videoAdvertising: 'Video advertising',
+    searchEngines: 'Search engines',
+    weeklyUserActivity: 'Weekly user activity',
+    activeQuantity: 'Active quantity',
+    monday: 'Monday',
+    tuesday: 'Tuesday',
+    wednesday: 'Wednesday',
+    thursday: 'Thursday',
+    friday: 'Friday',
+    saturday: 'Saturday',
+    sunday: 'Sunday'
+  },
+  workplace: {
+    welcome: 'Hello',
+    happyDay: 'Wish you happy every day!',
+    toady: `It's sunny today`,
+    notice: 'Announcement',
+    project: 'Project',
+    access: 'Project access',
+    toDo: 'To do',
+    introduction: 'A serious introduction',
+    shortcutOperation: 'Quick entry',
+    operation: 'Operation',
+    index: 'Index',
+    personal: 'Personal',
+    team: 'Team',
+    quote: 'Quote',
+    contribution: 'Contribution',
+    hot: 'Hot',
+    yield: 'Yield',
+    dynamic: 'Dynamic',
+    push: 'push',
+    follow: 'Follow'
+  },
+  form: {
+    input: 'Input',
+    inputNumber: 'InputNumber',
+    default: 'Default',
+    icon: 'Icon',
+    mixed: 'Mixed',
+    textarea: 'Textarea',
+    slot: 'Slot',
+    position: 'Position',
+    autocomplete: 'Autocomplete',
+    select: 'Select',
+    selectGroup: 'Select Group',
+    selectV2: 'SelectV2',
+    cascader: 'Cascader',
+    switch: 'Switch',
+    rate: 'Rate',
+    colorPicker: 'Color Picker',
+    transfer: 'Transfer',
+    render: 'Render',
+    radio: 'Radio',
+    button: 'Button',
+    checkbox: 'Checkbox',
+    slider: 'Slider',
+    datePicker: 'Date Picker',
+    shortcuts: 'Shortcuts',
+    today: 'Today',
+    yesterday: 'Yesterday',
+    aWeekAgo: 'A week ago',
+    week: 'Week',
+    year: 'Year',
+    month: 'Month',
+    dates: 'Dates',
+    daterange: 'Date Range',
+    monthrange: 'Month Range',
+    dateTimePicker: 'DateTimePicker',
+    dateTimerange: 'Datetime Range',
+    timePicker: 'Time Picker',
+    timeSelect: 'Time Select',
+    inputPassword: 'input Password',
+    passwordStrength: 'Password Strength',
+    operate: 'operate',
+    change: 'Change',
+    restore: 'Restore',
+    disabled: 'Disabled',
+    disablement: 'Disablement',
+    delete: 'Delete',
+    add: 'Add',
+    setValue: 'Set value',
+    resetValue: 'Reset value',
+    set: 'Set',
+    subitem: 'Subitem',
+    formValidation: 'Form validation',
+    verifyReset: 'Verify reset',
+    remark: 'Remark'
+  },
+  watermark: {
+    watermark: 'Watermark'
+  },
+  table: {
+    table: 'Table',
+    index: 'Index',
+    title: 'Title',
+    author: 'Author',
+    createTime: 'Create time',
+    action: 'Action',
+    pagination: 'pagination',
+    reserveIndex: 'Reserve index',
+    restoreIndex: 'Restore index',
+    showSelections: 'Show selections',
+    hiddenSelections: 'Restore selections',
+    showExpandedRows: 'Show expanded rows',
+    hiddenExpandedRows: 'Hidden expanded rows',
+    header: 'Header'
+  },
+  action: {
+    create: 'Create',
+    add: 'Add',
+    del: 'Delete',
+    delete: 'Delete',
+    edit: 'Edit',
+    update: 'Update',
+    preview: 'Preview',
+    more: 'More',
+    sync: 'Sync',
+    save: 'Save',
+    detail: 'Detail',
+    export: 'Export',
+    import: 'Import',
+    generate: 'Generate',
+    logout: 'Login Out',
+    test: 'Test',
+    typeCreate: 'Dict Type Create',
+    typeUpdate: 'Dict Type Eidt',
+    dataCreate: 'Dict Data Create',
+    dataUpdate: 'Dict Data Eidt',
+    fileUpload: 'File Upload'
+  },
+  dialog: {
+    dialog: 'Dialog',
+    open: 'Open',
+    close: 'Close'
+  },
+  sys: {
+    api: {
+      operationFailed: 'Operation failed',
+      errorTip: 'Error Tip',
+      errorMessage: 'The operation failed, the system is abnormal!',
+      timeoutMessage: 'Login timed out, please log in again!',
+      apiTimeoutMessage: 'The interface request timed out, please refresh the page and try again!',
+      apiRequestFailed: 'The interface request failed, please try again later!',
+      networkException: 'network anomaly',
+      networkExceptionMsg:
+        'Please check if your network connection is normal! The network is abnormal',
+
+      errMsg401: 'The user does not have permission (token, user name, password error)!',
+      errMsg403: 'The user is authorized, but access is forbidden!',
+      errMsg404: 'Network request error, the resource was not found!',
+      errMsg405: 'Network request error, request method not allowed!',
+      errMsg408: 'Network request timed out!',
+      errMsg500: 'Server error, please contact the administrator!',
+      errMsg501: 'The network is not implemented!',
+      errMsg502: 'Network Error!',
+      errMsg503: 'The service is unavailable, the server is temporarily overloaded or maintained!',
+      errMsg504: 'Network timeout!',
+      errMsg505: 'The http version does not support the request!',
+      errMsg901: 'Demo mode, no write operations are possible!'
+    },
+    app: {
+      logoutTip: 'Reminder',
+      logoutMessage: 'Confirm to exit the system?',
+      menuLoading: 'Menu loading...'
+    },
+    exception: {
+      backLogin: 'Back Login',
+      backHome: 'Back Home',
+      subTitle403: "Sorry, you don't have access to this page.",
+      subTitle404: 'Sorry, the page you visited does not exist.',
+      subTitle500: 'Sorry, the server is reporting an error.',
+      noDataTitle: 'No data on the current page.',
+      networkErrorTitle: 'Network Error',
+      networkErrorSubTitle:
+        'Sorry, Your network connection has been disconnected, please check your network!'
+    },
+    lock: {
+      unlock: 'Click to unlock',
+      alert: 'Lock screen password error',
+      backToLogin: 'Back to login',
+      entry: 'Enter the system',
+      placeholder: 'Please enter the lock screen password or user password'
+    },
+    login: {
+      backSignIn: 'Back sign in',
+      mobileSignInFormTitle: 'Mobile sign in',
+      qrSignInFormTitle: 'Qr code sign in',
+      signInFormTitle: 'Sign in',
+      signUpFormTitle: 'Sign up',
+      forgetFormTitle: 'Reset password',
+
+      signInTitle: 'Backstage management system',
+      signInDesc: 'Enter your personal details and get started!',
+      policy: 'I agree to the xxx Privacy Policy',
+      scanSign: `scanning the code to complete the login`,
+
+      loginButton: 'Sign in',
+      registerButton: 'Sign up',
+      rememberMe: 'Remember me',
+      forgetPassword: 'Forget Password?',
+      otherSignIn: 'Sign in with',
+
+      // notify
+      loginSuccessTitle: 'Login successful',
+      loginSuccessDesc: 'Welcome back',
+
+      // placeholder
+      accountPlaceholder: 'Please input username',
+      passwordPlaceholder: 'Please input password',
+      smsPlaceholder: 'Please input sms code',
+      mobilePlaceholder: 'Please input mobile',
+      policyPlaceholder: 'Register after checking',
+      diffPwd: 'The two passwords are inconsistent',
+
+      userName: 'Username',
+      password: 'Password',
+      confirmPassword: 'Confirm Password',
+      email: 'Email',
+      smsCode: 'SMS code',
+      mobile: 'Mobile'
+    }
+  },
+  profile: {
+    user: {
+      title: 'Personal Information',
+      username: 'User Name',
+      nickname: 'Nick Name',
+      mobile: 'Phone Number',
+      email: 'User Mail',
+      dept: 'Department',
+      posts: 'Position',
+      roles: 'Own Role',
+      sex: 'Sex',
+      man: 'Man',
+      woman: 'Woman',
+      createTime: 'Created Date'
+    },
+    info: {
+      title: 'Basic Information',
+      basicInfo: 'Basic Information',
+      resetPwd: 'Reset Password',
+      userSocial: 'Social Information'
+    },
+    rules: {
+      nickname: 'Please Enter User Nickname',
+      mail: 'Please Input The Email Address',
+      truemail: 'Please Input The Correct Email Address',
+      phone: 'Please Enter The Phone Number',
+      truephone: 'Please Enter The Correct Phone Number'
+    },
+    password: {
+      oldPassword: 'Old PassWord',
+      newPassword: 'New Password',
+      confirmPassword: 'Confirm Password',
+      oldPwdMsg: 'Please Enter Old Password',
+      newPwdMsg: 'Please Enter New Password',
+      cfPwdMsg: 'Please Enter Confirm Password',
+      diffPwd: 'The Passwords Entered Twice No Match'
+    }
+  },
+  cropper: {
+    selectImage: 'Select Image',
+    uploadSuccess: 'Uploaded success!',
+    modalTitle: 'Avatar upload',
+    okText: 'Confirm and upload',
+    btn_reset: 'Reset',
+    btn_rotate_left: 'Counterclockwise rotation',
+    btn_rotate_right: 'Clockwise rotation',
+    btn_scale_x: 'Flip horizontal',
+    btn_scale_y: 'Flip vertical',
+    btn_zoom_in: 'Zoom in',
+    btn_zoom_out: 'Zoom out',
+    preview: 'Preivew'
+  }
+}

+ 105 - 0
src/store/modules/dict.ts

@@ -0,0 +1,105 @@
+import { defineStore } from 'pinia'
+import { store } from '../index'
+// @ts-ignore
+import { DictDataVO } from '@/api/system/dict/types'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+const { wsCache } = useCache('sessionStorage')
+import { getSimpleDictDataList } from '@/api/system/dict/dict.data'
+
+export interface DictValueType {
+  value: any
+  label: string
+  clorType?: string
+  cssClass?: string
+}
+export interface DictTypeType {
+  dictType: string
+  dictValue: DictValueType[]
+}
+export interface DictState {
+  dictMap: Map<string, any>
+  isSetDict: boolean
+}
+
+export const useDictStore = defineStore('dict', {
+  state: (): DictState => ({
+    dictMap: new Map<string, any>(),
+    isSetDict: false
+  }),
+  getters: {
+    getDictMap(): Recordable {
+      const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)
+      if (dictMap) {
+        this.dictMap = dictMap
+      }
+      return this.dictMap
+    },
+    getIsSetDict(): boolean {
+      return this.isSetDict
+    }
+  },
+  actions: {
+    async setDictMap() {
+      const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)
+      if (dictMap) {
+        this.dictMap = dictMap
+        this.isSetDict = true
+      } else {
+        const res = await getSimpleDictDataList()
+        // 设置数据
+        const dictDataMap = new Map<string, any>()
+        res.forEach((dictData: DictDataVO) => {
+          // 获得 dictType 层级
+          const enumValueObj = dictDataMap[dictData.dictType]
+          if (!enumValueObj) {
+            dictDataMap[dictData.dictType] = []
+          }
+          // 处理 dictValue 层级
+          dictDataMap[dictData.dictType].push({
+            value: dictData.value,
+            label: dictData.label,
+            colorType: dictData.colorType,
+            cssClass: dictData.cssClass
+          })
+        })
+        console.log(dictDataMap)
+        this.dictMap = dictDataMap
+        this.isSetDict = true
+        wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 60 秒 过期
+      }
+    },
+    getDictByType(type: string) {
+      if (!this.isSetDict) {
+        this.setDictMap()
+      }
+      return this.dictMap[type]
+    },
+    async resetDict() {
+      wsCache.delete(CACHE_KEY.DICT_CACHE)
+      const res = await getSimpleDictDataList()
+      // 设置数据
+      const dictDataMap = new Map<string, any>()
+      res.forEach((dictData: DictDataVO) => {
+        // 获得 dictType 层级
+        const enumValueObj = dictDataMap[dictData.dictType]
+        if (!enumValueObj) {
+          dictDataMap[dictData.dictType] = []
+        }
+        // 处理 dictValue 层级
+        dictDataMap[dictData.dictType].push({
+          value: dictData.value,
+          label: dictData.label,
+          colorType: dictData.colorType,
+          cssClass: dictData.cssClass
+        })
+      })
+      this.dictMap = dictDataMap
+      this.isSetDict = true
+      wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 60 秒 过期
+    }
+  }
+})
+
+export const useDictStoreWithOut = () => {
+  return useDictStore(store)
+}

+ 3 - 0
src/types/elementPlus.d.ts

@@ -0,0 +1,3 @@
+export type ElementPlusSize = 'default' | 'small' | 'large'
+
+export type ElementPlusInfoType = 'success' | 'info' | 'warning' | 'danger'

+ 245 - 0
src/utils/dict.ts

@@ -0,0 +1,245 @@
+/**
+ * 数据字典工具类
+ */
+import { useDictStoreWithOut } from '@/store/modules/dict'
+import { ElementPlusInfoType } from '@/types/elementPlus'
+
+const dictStore = useDictStoreWithOut()
+
+/**
+ * 获取 dictType 对应的数据字典数组
+ *
+ * @param dictType 数据类型
+ * @returns {*|Array} 数据字典数组
+ */
+export interface DictDataType {
+  dictType: string
+  label: string
+  value: string | number | boolean
+  colorType: ElementPlusInfoType | ''
+  cssClass: string
+}
+
+export interface NumberDictDataType extends DictDataType {
+  value: number
+}
+
+export interface StringDictDataType extends DictDataType {
+  value: string
+}
+
+export const getDictOptions = (dictType: string) => {
+  return dictStore.getDictByType(dictType) || []
+}
+
+export const getIntDictOptions = (dictType: string): NumberDictDataType[] => {
+  // 获得通用的 DictDataType 列表
+  const dictOptions: DictDataType[] = getDictOptions(dictType)
+  // 转换成 number 类型的 NumberDictDataType 类型
+  // why 需要特殊转换:避免 IDEA 在 v-for="dict in getIntDictOptions(...)" 时,el-option 的 key 会告警
+  const dictOption: NumberDictDataType[] = []
+  dictOptions.forEach((dict: DictDataType) => {
+    dictOption.push({
+      ...dict,
+      value: parseInt(dict.value + '')
+    })
+  })
+  return dictOption
+}
+
+export const getStrDictOptions = (dictType: string) => {
+  // 获得通用的 DictDataType 列表
+  const dictOptions: DictDataType[] = getDictOptions(dictType)
+  // 转换成 string 类型的 StringDictDataType 类型
+  // why 需要特殊转换:避免 IDEA 在 v-for="dict in getStrDictOptions(...)" 时,el-option 的 key 会告警
+  const dictOption: StringDictDataType[] = []
+  dictOptions.forEach((dict: DictDataType) => {
+    dictOption.push({
+      ...dict,
+      value: dict.value + ''
+    })
+  })
+  return dictOption
+}
+
+export const getBoolDictOptions = (dictType: string) => {
+  const dictOption: DictDataType[] = []
+  const dictOptions: DictDataType[] = getDictOptions(dictType)
+  dictOptions.forEach((dict: DictDataType) => {
+    dictOption.push({
+      ...dict,
+      value: dict.value + '' === 'true'
+    })
+  })
+  return dictOption
+}
+
+/**
+ * 获取指定字典类型的指定值对应的字典对象
+ * @param dictType 字典类型
+ * @param value 字典值
+ * @return DictDataType 字典对象
+ */
+export const getDictObj = (dictType: string, value: any): DictDataType | undefined => {
+  const dictOptions: DictDataType[] = getDictOptions(dictType)
+  for (const dict of dictOptions) {
+    if (dict.value === value + '') {
+      return dict
+    }
+  }
+}
+
+/**
+ * 获得字典数据的文本展示
+ *
+ * @param dictType 字典类型
+ * @param value 字典数据的值
+ * @return 字典名称
+ */
+export const getDictLabel = (dictType: string, value: any): string => {
+  const dictOptions: DictDataType[] = getDictOptions(dictType)
+  const dictLabel = ref('')
+  dictOptions.forEach((dict: DictDataType) => {
+    if (dict.value === value + '') {
+      dictLabel.value = dict.label
+    }
+  })
+  return dictLabel.value
+}
+
+export enum DICT_TYPE {
+  USER_TYPE = 'user_type',
+  COMMON_STATUS = 'common_status',
+  TERMINAL = 'terminal', // 终端
+  DATE_INTERVAL = 'date_interval', // 数据间隔
+
+  // ========== SYSTEM 模块 ==========
+  SYSTEM_USER_SEX = 'system_user_sex',
+  SYSTEM_USER_SHP_NAME = 'system_user_shp_name',
+  SYSTEM_USER_THREED_TILES = 'system_user_threed_tiles',
+  SYSTEM_MENU_TYPE = 'system_menu_type',
+  SYSTEM_ROLE_TYPE = 'system_role_type',
+  SYSTEM_DATA_SCOPE = 'system_data_scope',
+  SYSTEM_NOTICE_TYPE = 'system_notice_type',
+  SYSTEM_LOGIN_TYPE = 'system_login_type',
+  SYSTEM_LOGIN_RESULT = 'system_login_result',
+  SYSTEM_SMS_CHANNEL_CODE = 'system_sms_channel_code',
+  SYSTEM_SMS_TEMPLATE_TYPE = 'system_sms_template_type',
+  SYSTEM_SMS_SEND_STATUS = 'system_sms_send_status',
+  SYSTEM_SMS_RECEIVE_STATUS = 'system_sms_receive_status',
+  SYSTEM_ERROR_CODE_TYPE = 'system_error_code_type',
+  SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type',
+  SYSTEM_MAIL_SEND_STATUS = 'system_mail_send_status',
+  SYSTEM_NOTIFY_TEMPLATE_TYPE = 'system_notify_template_type',
+  SYSTEM_SOCIAL_TYPE = 'system_social_type',
+
+  // ========== INFRA 模块 ==========
+  INFRA_BOOLEAN_STRING = 'infra_boolean_string',
+  INFRA_JOB_STATUS = 'infra_job_status',
+  INFRA_JOB_LOG_STATUS = 'infra_job_log_status',
+  INFRA_API_ERROR_LOG_PROCESS_STATUS = 'infra_api_error_log_process_status',
+  INFRA_CONFIG_TYPE = 'infra_config_type',
+  INFRA_CODEGEN_TEMPLATE_TYPE = 'infra_codegen_template_type',
+  INFRA_CODEGEN_FRONT_TYPE = 'infra_codegen_front_type',
+  INFRA_CODEGEN_SCENE = 'infra_codegen_scene',
+  INFRA_FILE_STORAGE = 'infra_file_storage',
+  INFRA_OPERATE_TYPE = 'infra_operate_type',
+
+  // ========== BPM 模块 ==========
+  BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
+  BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy',
+  BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',
+  BPM_TASK_STATUS = 'bpm_task_status',
+  BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type',
+  BPM_FUND_APPLY_TYPE = 'bpm_fund_apply_type',
+  BPM_PROCESS_LISTENER_TYPE = 'bpm_process_listener_type',
+  BPM_PROCESS_LISTENER_VALUE_TYPE = 'bpm_process_listener_value_type',
+  BPM_DISBUESEMENT_ITEM_TYPE = 'bpm_disbursement_item_type',
+
+  // ========== PAY 模块 ==========
+  PAY_CHANNEL_CODE = 'pay_channel_code', // 支付渠道编码类型
+  PAY_ORDER_STATUS = 'pay_order_status', // 商户支付订单状态
+  PAY_REFUND_STATUS = 'pay_refund_status', // 退款订单状态
+  PAY_NOTIFY_STATUS = 'pay_notify_status', // 商户支付回调状态
+  PAY_NOTIFY_TYPE = 'pay_notify_type', // 商户支付回调状态
+  PAY_TRANSFER_STATUS = 'pay_transfer_status', // 转账订单状态
+  PAY_TRANSFER_TYPE = 'pay_transfer_type', // 转账订单状态
+
+  // ========== MP 模块 ==========
+  MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型
+  MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型
+
+  // ========== Member 会员模块 ==========
+  MEMBER_POINT_BIZ_TYPE = 'member_point_biz_type', // 积分的业务类型
+  MEMBER_EXPERIENCE_BIZ_TYPE = 'member_experience_biz_type', // 会员经验业务类型
+
+  // ========== MALL - 商品模块 ==========
+  PRODUCT_SPU_STATUS = 'product_spu_status', //商品状态
+
+  // ========== MALL - 交易模块 ==========
+  EXPRESS_CHARGE_MODE = 'trade_delivery_express_charge_mode', //快递的计费方式
+  TRADE_AFTER_SALE_STATUS = 'trade_after_sale_status', // 售后 - 状态
+  TRADE_AFTER_SALE_WAY = 'trade_after_sale_way', // 售后 - 方式
+  TRADE_AFTER_SALE_TYPE = 'trade_after_sale_type', // 售后 - 类型
+  TRADE_ORDER_TYPE = 'trade_order_type', // 订单 - 类型
+  TRADE_ORDER_STATUS = 'trade_order_status', // 订单 - 状态
+  TRADE_ORDER_ITEM_AFTER_SALE_STATUS = 'trade_order_item_after_sale_status', // 订单项 - 售后状态
+  TRADE_DELIVERY_TYPE = 'trade_delivery_type', // 配送方式
+  BROKERAGE_ENABLED_CONDITION = 'brokerage_enabled_condition', // 分佣模式
+  BROKERAGE_BIND_MODE = 'brokerage_bind_mode', // 分销关系绑定模式
+  BROKERAGE_BANK_NAME = 'brokerage_bank_name', // 佣金提现银行
+  BROKERAGE_WITHDRAW_TYPE = 'brokerage_withdraw_type', // 佣金提现类型
+  BROKERAGE_RECORD_BIZ_TYPE = 'brokerage_record_biz_type', // 佣金业务类型
+  BROKERAGE_RECORD_STATUS = 'brokerage_record_status', // 佣金状态
+  BROKERAGE_WITHDRAW_STATUS = 'brokerage_withdraw_status', // 佣金提现状态
+
+  // ========== MALL - 营销模块 ==========
+  PROMOTION_DISCOUNT_TYPE = 'promotion_discount_type', // 优惠类型
+  PROMOTION_PRODUCT_SCOPE = 'promotion_product_scope', // 营销的商品范围
+  PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE = 'promotion_coupon_template_validity_type', // 优惠劵模板的有限期类型
+  PROMOTION_COUPON_STATUS = 'promotion_coupon_status', // 优惠劵的状态
+  PROMOTION_COUPON_TAKE_TYPE = 'promotion_coupon_take_type', // 优惠劵的领取方式
+  PROMOTION_ACTIVITY_STATUS = 'promotion_activity_status', // 优惠活动的状态
+  PROMOTION_CONDITION_TYPE = 'promotion_condition_type', // 营销的条件类型枚举
+  PROMOTION_BARGAIN_RECORD_STATUS = 'promotion_bargain_record_status', // 砍价记录的状态
+  PROMOTION_COMBINATION_RECORD_STATUS = 'promotion_combination_record_status', // 拼团记录的状态
+  PROMOTION_BANNER_POSITION = 'promotion_banner_position', // banner 定位
+
+  // ========== CRM - 客户管理模块 ==========
+  CRM_AUDIT_STATUS = 'crm_audit_status', // CRM 审批状态
+  CRM_BIZ_TYPE = 'crm_biz_type', // CRM 业务类型
+  CRM_BUSINESS_END_STATUS_TYPE = 'crm_business_end_status_type', // CRM 商机结束状态类型
+  CRM_RECEIVABLE_RETURN_TYPE = 'crm_receivable_return_type', // CRM 回款的还款方式
+  CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry', // CRM 客户所属行业
+  CRM_CUSTOMER_LEVEL = 'crm_customer_level', // CRM 客户级别
+  CRM_CUSTOMER_SOURCE = 'crm_customer_source', // CRM 客户来源
+  CRM_PRODUCT_STATUS = 'crm_product_status', // CRM 商品状态
+  CRM_PERMISSION_LEVEL = 'crm_permission_level', // CRM 数据权限的级别
+  CRM_PRODUCT_UNIT = 'crm_product_unit', // CRM 产品单位
+  CRM_FOLLOW_UP_TYPE = 'crm_follow_up_type', // CRM 跟进方式
+
+  // ========== ERP - 企业资源计划模块  ==========
+  ERP_AUDIT_STATUS = 'erp_audit_status', // ERP 审批状态
+  ERP_STOCK_RECORD_BIZ_TYPE = 'erp_stock_record_biz_type', // 库存明细的业务类型
+
+  // ========== AI - 人工智能模块  ==========
+  AI_PLATFORM = 'ai_platform', // AI 平台
+  AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态
+  AI_MUSIC_STATUS = 'ai_music_status', // AI 音乐状态
+  AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式
+  AI_WRITE_TYPE = 'ai_write_type', // AI 写作类型
+  AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度
+  AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式
+  AI_WRITE_TONE = 'ai_write_tone', // AI 写作语气
+  AI_WRITE_LANGUAGE = 'ai_write_language', // AI 写作语言
+
+  // ========== LAYER - 图层模块  ==========
+  LAYER_CESIUM_FORMAT='layer_cesium_format',
+  LAYER_GISFROM_TYPE='layer_gisfrom_type',//gisform拼错了
+  LAYER_NAME_SHPTYPE ="layer_name_shpType",
+  LAYER_VISUALIZEMESSAGE_PLACE_TYPE='layer_visualizemessage_place_type',
+  LAYER_VISUALIZEMESSAGE_USE_TYPE='layer_visualizemessage_use_type',
+
+  // ========== GISMODEL - 模型模块  ==========
+  GISMODEL_WHETHER_MODEL_IS_PUBLIC='gismodel_whether_model_is_public',
+}

+ 289 - 0
src/utils/domUtils.ts

@@ -0,0 +1,289 @@
+import { isServer } from './is'
+const ieVersion = isServer ? 0 : Number((document as any).documentMode)
+const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g
+const MOZ_HACK_REGEXP = /^moz([A-Z])/
+
+export interface ViewportOffsetResult {
+  left: number
+  top: number
+  right: number
+  bottom: number
+  rightIncludeBody: number
+  bottomIncludeBody: number
+}
+
+/* istanbul ignore next */
+const trim = function (string: string) {
+  return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '')
+}
+
+/* istanbul ignore next */
+const camelCase = function (name: string) {
+  return name
+    .replace(SPECIAL_CHARS_REGEXP, function (_, __, letter, offset) {
+      return offset ? letter.toUpperCase() : letter
+    })
+    .replace(MOZ_HACK_REGEXP, 'Moz$1')
+}
+
+/* istanbul ignore next */
+export function hasClass(el: Element, cls: string) {
+  if (!el || !cls) return false
+  if (cls.indexOf(' ') !== -1) {
+    throw new Error('className should not contain space.')
+  }
+  if (el.classList) {
+    return el.classList.contains(cls)
+  } else {
+    return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1
+  }
+}
+
+/* istanbul ignore next */
+export function addClass(el: Element, cls: string) {
+  if (!el) return
+  let curClass = el.className
+  const classes = (cls || '').split(' ')
+
+  for (let i = 0, j = classes.length; i < j; i++) {
+    const clsName = classes[i]
+    if (!clsName) continue
+
+    if (el.classList) {
+      el.classList.add(clsName)
+    } else if (!hasClass(el, clsName)) {
+      curClass += ' ' + clsName
+    }
+  }
+  if (!el.classList) {
+    el.className = curClass
+  }
+}
+
+/* istanbul ignore next */
+export function removeClass(el: Element, cls: string) {
+  if (!el || !cls) return
+  const classes = cls.split(' ')
+  let curClass = ' ' + el.className + ' '
+
+  for (let i = 0, j = classes.length; i < j; i++) {
+    const clsName = classes[i]
+    if (!clsName) continue
+
+    if (el.classList) {
+      el.classList.remove(clsName)
+    } else if (hasClass(el, clsName)) {
+      curClass = curClass.replace(' ' + clsName + ' ', ' ')
+    }
+  }
+  if (!el.classList) {
+    el.className = trim(curClass)
+  }
+}
+
+export function getBoundingClientRect(element: Element): DOMRect | number {
+  if (!element || !element.getBoundingClientRect) {
+    return 0
+  }
+  return element.getBoundingClientRect()
+}
+
+/**
+ * 获取当前元素的left、top偏移
+ *   left:元素最左侧距离文档左侧的距离
+ *   top:元素最顶端距离文档顶端的距离
+ *   right:元素最右侧距离文档右侧的距离
+ *   bottom:元素最底端距离文档底端的距离
+ *   rightIncludeBody:元素最左侧距离文档右侧的距离
+ *   bottomIncludeBody:元素最底端距离文档最底部的距离
+ *
+ * @description:
+ */
+export function getViewportOffset(element: Element): ViewportOffsetResult {
+  const doc = document.documentElement
+
+  const docScrollLeft = doc.scrollLeft
+  const docScrollTop = doc.scrollTop
+  const docClientLeft = doc.clientLeft
+  const docClientTop = doc.clientTop
+
+  const pageXOffset = window.pageXOffset
+  const pageYOffset = window.pageYOffset
+
+  const box = getBoundingClientRect(element)
+
+  const { left: retLeft, top: rectTop, width: rectWidth, height: rectHeight } = box as DOMRect
+
+  const scrollLeft = (pageXOffset || docScrollLeft) - (docClientLeft || 0)
+  const scrollTop = (pageYOffset || docScrollTop) - (docClientTop || 0)
+  const offsetLeft = retLeft + pageXOffset
+  const offsetTop = rectTop + pageYOffset
+
+  const left = offsetLeft - scrollLeft
+  const top = offsetTop - scrollTop
+
+  const clientWidth = window.document.documentElement.clientWidth
+  const clientHeight = window.document.documentElement.clientHeight
+  return {
+    left: left,
+    top: top,
+    right: clientWidth - rectWidth - left,
+    bottom: clientHeight - rectHeight - top,
+    rightIncludeBody: clientWidth - left,
+    bottomIncludeBody: clientHeight - top
+  }
+}
+
+/* istanbul ignore next */
+export const on = function (
+  element: HTMLElement | Document | Window,
+  event: string,
+  handler: EventListenerOrEventListenerObject
+): void {
+  if (element && event && handler) {
+    element.addEventListener(event, handler, false)
+  }
+}
+
+/* istanbul ignore next */
+export const off = function (
+  element: HTMLElement | Document | Window,
+  event: string,
+  handler: any
+): void {
+  if (element && event && handler) {
+    element.removeEventListener(event, handler, false)
+  }
+}
+
+/* istanbul ignore next */
+export const once = function (el: HTMLElement, event: string, fn: EventListener): void {
+  const listener = function (this: any, ...args: unknown[]) {
+    if (fn) {
+      // @ts-ignore
+      fn.apply(this, args)
+    }
+    off(el, event, listener)
+  }
+  on(el, event, listener)
+}
+
+/* istanbul ignore next */
+export const getStyle =
+  ieVersion < 9
+    ? function (element: Element | any, styleName: string) {
+        if (isServer) return
+        if (!element || !styleName) return null
+        styleName = camelCase(styleName)
+        if (styleName === 'float') {
+          styleName = 'styleFloat'
+        }
+        try {
+          switch (styleName) {
+            case 'opacity':
+              try {
+                return element.filters.item('alpha').opacity / 100
+              } catch (e) {
+                return 1.0
+              }
+            default:
+              return element.style[styleName] || element.currentStyle
+                ? element.currentStyle[styleName]
+                : null
+          }
+        } catch (e) {
+          return element.style[styleName]
+        }
+      }
+    : function (element: Element | any, styleName: string) {
+        if (isServer) return
+        if (!element || !styleName) return null
+        styleName = camelCase(styleName)
+        if (styleName === 'float') {
+          styleName = 'cssFloat'
+        }
+        try {
+          const computed = (document as any).defaultView.getComputedStyle(element, '')
+          return element.style[styleName] || computed ? computed[styleName] : null
+        } catch (e) {
+          return element.style[styleName]
+        }
+      }
+
+/* istanbul ignore next */
+export function setStyle(element: Element | any, styleName: any, value: any) {
+  if (!element || !styleName) return
+
+  if (typeof styleName === 'object') {
+    for (const prop in styleName) {
+      if (Object.prototype.hasOwnProperty.call(styleName, prop)) {
+        setStyle(element, prop, styleName[prop])
+      }
+    }
+  } else {
+    styleName = camelCase(styleName)
+    if (styleName === 'opacity' && ieVersion < 9) {
+      element.style.filter = isNaN(value) ? '' : 'alpha(opacity=' + value * 100 + ')'
+    } else {
+      element.style[styleName] = value
+    }
+  }
+}
+
+/* istanbul ignore next */
+export const isScroll = (el: Element, vertical: any) => {
+  if (isServer) return
+
+  const determinedDirection = vertical !== null || vertical !== undefined
+  const overflow = determinedDirection
+    ? vertical
+      ? getStyle(el, 'overflow-y')
+      : getStyle(el, 'overflow-x')
+    : getStyle(el, 'overflow')
+
+  return overflow.match(/(scroll|auto)/)
+}
+
+/* istanbul ignore next */
+export const getScrollContainer = (el: Element, vertical?: any) => {
+  if (isServer) return
+
+  let parent: any = el
+  while (parent) {
+    if ([window, document, document.documentElement].includes(parent)) {
+      return window
+    }
+    if (isScroll(parent, vertical)) {
+      return parent
+    }
+    parent = parent.parentNode
+  }
+
+  return parent
+}
+
+/* istanbul ignore next */
+export const isInContainer = (el: Element, container: any) => {
+  if (isServer || !el || !container) return false
+
+  const elRect = el.getBoundingClientRect()
+  let containerRect
+
+  if ([window, document, document.documentElement, null, undefined].includes(container)) {
+    containerRect = {
+      top: 0,
+      right: window.innerWidth,
+      bottom: window.innerHeight,
+      left: 0
+    }
+  } else {
+    containerRect = container.getBoundingClientRect()
+  }
+
+  return (
+    elRect.top < containerRect.bottom &&
+    elRect.bottom > containerRect.top &&
+    elRect.right > containerRect.left &&
+    elRect.left < containerRect.right
+  )
+}

+ 56 - 0
src/utils/download.ts

@@ -0,0 +1,56 @@
+const download0 = (data: Blob, fileName: string, mineType: string) => {
+  // 创建 blob
+  const blob = new Blob([data], { type: mineType })
+  // 创建 href 超链接,点击进行下载
+  window.URL = window.URL || window.webkitURL
+  const href = URL.createObjectURL(blob)
+  const downA = document.createElement('a')
+  downA.href = href
+  downA.download = fileName
+  downA.click()
+  // 销毁超连接
+  window.URL.revokeObjectURL(href)
+}
+
+const download = {
+  // 下载 Excel 方法
+  excel: (data: Blob, fileName: string) => {
+    download0(data, fileName, 'application/vnd.ms-excel')
+  },
+  // 下载 Word 方法
+  word: (data: Blob, fileName: string) => {
+    download0(data, fileName, 'application/msword')
+  },
+  // 下载 Zip 方法
+  zip: (data: Blob, fileName: string) => {
+    download0(data, fileName, 'application/zip')
+  },
+  // 下载 Html 方法
+  html: (data: Blob, fileName: string) => {
+    download0(data, fileName, 'text/html')
+  },
+  // 下载 Markdown 方法
+  markdown: (data: Blob, fileName: string) => {
+    download0(data, fileName, 'text/markdown')
+  },
+  // 下载图片(允许跨域)
+  image: (url: string) => {
+    const image = new Image()
+    image.setAttribute('crossOrigin', 'anonymous')
+    image.src = url
+    image.onload = () => {
+      const canvas = document.createElement('canvas')
+      canvas.width = image.width
+      canvas.height = image.height
+      const ctx = canvas.getContext('2d') as CanvasDrawImage
+      ctx.drawImage(image, 0, 0, image.width, image.height)
+      const url = canvas.toDataURL('image/png')
+      const a = document.createElement('a')
+      a.href = url
+      a.download = 'image.png'
+      a.click()
+    }
+  }
+}
+
+export default download

+ 325 - 0
src/views/Home/echarts-data.ts

@@ -0,0 +1,325 @@
+import { EChartsOption } from 'echarts'
+
+const { t } = useI18n()
+
+export const lineOptions: EChartsOption = {
+  // title: {
+  //   text: t('analysis.monthlySales'),
+  //   left: 'center'
+  // },
+  xAxis: {
+    data: [
+      t('analysis.january'),
+      t('analysis.february'),
+      t('analysis.march'),
+      t('analysis.april'),
+      t('analysis.may'),
+      t('analysis.june'),
+      t('analysis.july'),
+      t('analysis.august'),
+      t('analysis.september'),
+      t('analysis.october'),
+      t('analysis.november'),
+      t('analysis.december')
+    ],
+    boundaryGap: false,
+    axisTick: {
+      show: false
+    }
+  },
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    top: 80,
+    containLabel: true
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'cross'
+    },
+    padding: [5, 10]
+  },
+  yAxis: {
+    axisTick: {
+      show: false
+    }
+  },
+  legend: {
+    data: ['项目','待办'],
+    top: 50
+  },
+  series: [
+    {
+      name: '项目',
+      smooth: true,
+      type: 'line',
+      data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123],
+      animationDuration: 2800,
+      animationEasing: 'cubicInOut'
+    },
+    {
+      name: '待办',
+      smooth: true,
+      type: 'line',
+      itemStyle: {},
+      data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123],
+      animationDuration: 2800,
+      animationEasing: 'quadraticOut'
+    }
+  ]
+}
+
+export const pieOptions: EChartsOption = {
+  title: {
+    text: t('analysis.userAccessSource'),
+    left: 'center'
+  },
+  tooltip: {
+    trigger: 'item',
+    formatter: '{a} <br/>{b} : {c} ({d}%)'
+  },
+  legend: {
+    orient: 'vertical',
+    left: 'left',
+    data: [
+      t('analysis.directAccess'),
+      t('analysis.mailMarketing'),
+      t('analysis.allianceAdvertising'),
+      t('analysis.videoAdvertising'),
+      t('analysis.searchEngines')
+    ]
+  },
+  series: [
+    {
+      name: t('analysis.userAccessSource'),
+      type: 'pie',
+      radius: '55%',
+      center: ['50%', '60%'],
+      data: [
+        { value: 335, name: t('analysis.directAccess') },
+        { value: 310, name: t('analysis.mailMarketing') },
+        { value: 234, name: t('analysis.allianceAdvertising') },
+        { value: 135, name: t('analysis.videoAdvertising') },
+        { value: 1548, name: t('analysis.searchEngines') }
+      ]
+    }
+  ]
+}
+
+export const barOptions: EChartsOption = {
+  title: {
+    text: t('analysis.weeklyUserActivity'),
+    left: 'center'
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  grid: {
+    left: 50,
+    right: 20,
+    bottom: 20
+  },
+  xAxis: {
+    type: 'category',
+    data: [
+      t('analysis.monday'),
+      t('analysis.tuesday'),
+      t('analysis.wednesday'),
+      t('analysis.thursday'),
+      t('analysis.friday'),
+      t('analysis.saturday'),
+      t('analysis.sunday')
+    ],
+    axisTick: {
+      alignWithLabel: true
+    }
+  },
+  yAxis: {
+    type: 'value'
+  },
+  series: [
+    {
+      name: t('analysis.activeQuantity'),
+      data: [13253, 34235, 26321, 12340, 24643, 1322, 1324],
+      type: 'bar'
+    }
+  ]
+}
+
+export const radarOption: EChartsOption = {
+  legend: {
+    data: [t('workplace.personal'), t('workplace.team')]
+  },
+  radar: {
+    // shape: 'circle',
+    indicator: [
+      { name: t('workplace.quote'), max: 65 },
+      { name: t('workplace.contribution'), max: 160 },
+      { name: t('workplace.hot'), max: 300 },
+      { name: t('workplace.yield'), max: 130 },
+      { name: t('workplace.follow'), max: 100 }
+    ]
+  },
+  series: [
+    {
+      name: `xxx${t('workplace.index')}`,
+      type: 'radar',
+      data: [
+        {
+          value: [42, 30, 20, 35, 80],
+          name: t('workplace.personal')
+        },
+        {
+          value: [50, 140, 290, 100, 90],
+          name: t('workplace.team')
+        }
+      ]
+    }
+  ]
+}
+
+export const wordOptions = {
+  series: [
+    {
+      type: 'wordCloud',
+      gridSize: 2,
+      sizeRange: [12, 50],
+      rotationRange: [-90, 90],
+      shape: 'pentagon',
+      width: 600,
+      height: 400,
+      drawOutOfBound: true,
+      textStyle: {
+        color: function () {
+          return (
+            'rgb(' +
+            [
+              Math.round(Math.random() * 160),
+              Math.round(Math.random() * 160),
+              Math.round(Math.random() * 160)
+            ].join(',') +
+            ')'
+          )
+        }
+      },
+      emphasis: {
+        textStyle: {
+          shadowBlur: 10,
+          shadowColor: '#333'
+        }
+      },
+      data: [
+        {
+          name: 'Sam S Club',
+          value: 10000,
+          textStyle: {
+            color: 'black'
+          },
+          emphasis: {
+            textStyle: {
+              color: 'red'
+            }
+          }
+        },
+        {
+          name: 'Macys',
+          value: 6181
+        },
+        {
+          name: 'Amy Schumer',
+          value: 4386
+        },
+        {
+          name: 'Jurassic World',
+          value: 4055
+        },
+        {
+          name: 'Charter Communications',
+          value: 2467
+        },
+        {
+          name: 'Chick Fil A',
+          value: 2244
+        },
+        {
+          name: 'Planet Fitness',
+          value: 1898
+        },
+        {
+          name: 'Pitch Perfect',
+          value: 1484
+        },
+        {
+          name: 'Express',
+          value: 1112
+        },
+        {
+          name: 'Home',
+          value: 965
+        },
+        {
+          name: 'Johnny Depp',
+          value: 847
+        },
+        {
+          name: 'Lena Dunham',
+          value: 582
+        },
+        {
+          name: 'Lewis Hamilton',
+          value: 555
+        },
+        {
+          name: 'KXAN',
+          value: 550
+        },
+        {
+          name: 'Mary Ellen Mark',
+          value: 462
+        },
+        {
+          name: 'Farrah Abraham',
+          value: 366
+        },
+        {
+          name: 'Rita Ora',
+          value: 360
+        },
+        {
+          name: 'Serena Williams',
+          value: 282
+        },
+        {
+          name: 'NCAA baseball tournament',
+          value: 273
+        },
+        {
+          name: 'Point Break',
+          value: 265
+        }
+      ]
+    }
+  ]
+}
+//项目数
+export const projectOptions = {
+  xAxis: {
+    type: 'category',
+    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+  },
+  yAxis: {
+    type: 'value'
+  },
+  series: [
+    {
+      data: [820, 932, 901, 934, 1290, 1330, 1320],
+      type: 'line',
+      smooth: true
+    }
+  ]
+};

+ 87 - 0
src/views/infra/codegen/EditTable.vue

@@ -0,0 +1,87 @@
+<template>
+  <ContentWrap v-loading="formLoading">
+    <el-tabs v-model="activeName">
+      <el-tab-pane label="基本信息" name="basicInfo">
+        <basic-info-form ref="basicInfoRef" :table="formData.table" />
+      </el-tab-pane>
+      <el-tab-pane label="字段信息" name="colum">
+        <colum-info-form ref="columInfoRef" :columns="formData.columns" />
+      </el-tab-pane>
+      <el-tab-pane label="生成信息" name="generateInfo">
+        <generate-info-form
+          ref="generateInfoRef"
+          :table="formData.table"
+          :columns="formData.columns"
+        />
+      </el-tab-pane>
+    </el-tabs>
+    <el-form>
+      <el-form-item style="float: right">
+        <el-button :loading="formLoading" type="primary" @click="submitForm">保存</el-button>
+        <el-button @click="close">返回</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { BasicInfoForm, ColumInfoForm, GenerateInfoForm } from './components'
+import * as CodegenApi from '@/api/infra/codegen'
+
+defineOptions({ name: 'InfraCodegenEditTable' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const { push, currentRoute } = useRouter() // 路由
+const { query } = useRoute() // 查询参数
+const { delView } = useTagsViewStore() // 视图操作
+
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const activeName = ref('colum') // Tag 激活的窗口
+const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>()
+const columInfoRef = ref<ComponentRef<typeof ColumInfoForm>>()
+const generateInfoRef = ref<ComponentRef<typeof GenerateInfoForm>>()
+const formData = ref<CodegenApi.CodegenUpdateReqVO>({
+  table: {},
+  columns: []
+})
+
+/** 获得详情 */
+const getDetail = async () => {
+  const id = query.id as unknown as number
+  if (!id) {
+    return
+  }
+  formLoading.value = true
+  try {
+    formData.value = await CodegenApi.getCodegenTable(id)
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 提交按钮 */
+const submitForm = async () => {
+  // 参数校验
+  if (!unref(formData)) return
+  await unref(basicInfoRef)?.validate()
+  await unref(generateInfoRef)?.validate()
+  try {
+    // 提交请求
+    await CodegenApi.updateCodegenTable(formData.value)
+    message.success(t('common.updateSuccess'))
+    close()
+  } catch {}
+}
+
+/** 关闭按钮 */
+const close = () => {
+  delView(unref(currentRoute))
+  push('/infra/codegen')
+}
+
+/** 初始化 */
+onMounted(() => {
+  getDetail()
+})
+</script>

+ 179 - 0
src/views/mall/promotion/discountActivity/DiscountActivityForm.vue

@@ -0,0 +1,179 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="65%">
+    <Form
+      ref="formRef"
+      v-loading="formLoading"
+      :isCol="true"
+      :rules="rules"
+      :schema="allSchemas.formSchema"
+    >
+      <!-- 先选择 -->
+      <!-- TODO @zhangshuai:商品允许选择多个 -->
+      <!-- TODO @zhangshuai:选择后的 SKU,需要后面加个【删除】按钮 -->
+      <!-- TODO @zhangshuai:展示的金额,貌似不对,大了 100 倍,需要看下 -->
+      <!-- TODO @zhangshuai:“优惠类型”,是每个 SKU 可以自定义已设置哈。因为每个商品 SKU 的折扣和减少价格,可能不同。具体交互,可以注册一个 youzan.com 看看;它的交互方式是,如果设置了“优惠金额”,则算“减价”;如果再次设置了“折扣百分比”,就算“打折”;这样形成一个互斥的优惠类型 -->
+      <template #spuId>
+        <el-button @click="spuSelectRef.open()">选择商品</el-button>
+        <SpuAndSkuList
+          ref="spuAndSkuListRef"
+          :rule-config="ruleConfig"
+          :spu-list="spuList"
+          :spu-property-list-p="spuPropertyList"
+        >
+          <el-table-column align="center" label="优惠金额" min-width="168">
+            <template #default="{ row: sku }">
+              <el-input-number v-model="sku.productConfig.discountPrice" :min="0" class="w-100%" />
+            </template>
+          </el-table-column>
+          <el-table-column align="center" label="折扣百分比(%)" min-width="168">
+            <template #default="{ row: sku }">
+              <el-input-number v-model="sku.productConfig.discountPercent" class="w-100%" />
+            </template>
+          </el-table-column>
+        </SpuAndSkuList>
+      </template>
+    </Form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+  <SpuSelect ref="spuSelectRef" :isSelectSku="true" @confirm="selectSpu" />
+</template>
+<script lang="ts" setup>
+import { SpuAndSkuList, SpuProperty, SpuSelect } from '../components'
+import { allSchemas, rules } from './discountActivity.data'
+import { cloneDeep } from 'lodash-es'
+import * as DiscountActivityApi from '@/api/mall/promotion/discount/discountActivity'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
+
+defineOptions({ name: 'PromotionDiscountActivityForm' })
+
+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 formRef = ref() // 表单 Ref
+// ================= 商品选择相关 =================
+
+const spuSelectRef = ref() // 商品和属性选择 Ref
+const spuAndSkuListRef = ref() // sku 限时折扣  配置组件Ref
+const ruleConfig: RuleConfig[] = []
+const spuList = ref<DiscountActivityApi.SpuExtension[]>([]) // 选择的 spu
+const spuPropertyList = ref<SpuProperty<DiscountActivityApi.SpuExtension>[]>([])
+const selectSpu = (spuId: number, skuIds: number[]) => {
+  formRef.value.setValues({ spuId })
+  getSpuDetails(spuId, skuIds)
+}
+/**
+ * 获取 SPU 详情
+ */
+const getSpuDetails = async (
+  spuId: number,
+  skuIds: number[] | undefined,
+  products?: DiscountActivityApi.DiscountProductVO[]
+) => {
+  const spuProperties: SpuProperty<DiscountActivityApi.SpuExtension>[] = []
+  const res = (await ProductSpuApi.getSpuDetailList([spuId])) as DiscountActivityApi.SpuExtension[]
+  if (res.length == 0) {
+    return
+  }
+  spuList.value = []
+  // 因为只能选择一个
+  const spu = res[0]
+  const selectSkus =
+    typeof skuIds === 'undefined' ? spu?.skus : spu?.skus?.filter((sku) => skuIds.includes(sku.id!))
+  selectSkus?.forEach((sku) => {
+    let config: DiscountActivityApi.DiscountProductVO = {
+      skuId: sku.id!,
+      spuId: spu.id,
+      discountType: 1,
+      discountPercent: 0,
+      discountPrice: 0
+    }
+    if (typeof products !== 'undefined') {
+      const product = products.find((item) => item.skuId === sku.id)
+      config = product || config
+    }
+    sku.productConfig = config
+  })
+  spu.skus = selectSkus as DiscountActivityApi.SkuExtension[]
+  spuProperties.push({
+    spuId: spu.id!,
+    spuDetail: spu,
+    propertyList: getPropertyList(spu)
+  })
+  spuList.value.push(spu)
+  spuPropertyList.value = spuProperties
+}
+
+// ================= end =================
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  await resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      const data = (await DiscountActivityApi.getDiscountActivity(
+        id
+      )) as DiscountActivityApi.DiscountActivityVO
+      const supId = data.products[0].spuId
+      await getSpuDetails(supId!, data.products?.map((sku) => sku.skuId), data.products)
+      formRef.value.setValues(data)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.getElFormRef().validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formRef.value.formModel as DiscountActivityApi.DiscountActivityVO
+    // 获取 折扣商品配置
+    const products = cloneDeep(spuAndSkuListRef.value.getSkuConfigs('productConfig'))
+    products.forEach((item: DiscountActivityApi.DiscountProductVO) => {
+      item.discountType = data['discountType']
+    })
+    data.products = products
+    // 真正提交
+    if (formType.value === 'create') {
+      await DiscountActivityApi.createDiscountActivity(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DiscountActivityApi.updateDiscountActivity(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = async () => {
+  spuList.value = []
+  spuPropertyList.value = []
+  await nextTick()
+  formRef.value.getElFormRef().resetFields()
+}
+</script>

+ 119 - 0
src/views/mall/promotion/discountActivity/discountActivity.data.ts

@@ -0,0 +1,119 @@
+import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
+import { dateFormatter2 } from '@/utils/formatTime'
+
+// TODO @zhangshai:
+// 表单校验
+export const rules = reactive({
+  spuId: [required],
+  name: [required],
+  startTime: [required],
+  endTime: [required],
+  discountType: [required]
+})
+
+// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
+const crudSchemas = reactive<CrudSchema[]>([
+  {
+    label: '活动名称',
+    field: 'name',
+    isSearch: true,
+    form: {
+      colProps: {
+        span: 24
+      }
+    },
+    table: {
+      width: 120
+    }
+  },
+  {
+    label: '活动开始时间',
+    field: 'startTime',
+    formatter: dateFormatter2,
+    isSearch: true,
+    search: {
+      component: 'DatePicker',
+      componentProps: {
+        valueFormat: 'YYYY-MM-DD',
+        type: 'daterange'
+      }
+    },
+    form: {
+      component: 'DatePicker',
+      componentProps: {
+        type: 'date',
+        valueFormat: 'x'
+      }
+    },
+    table: {
+      width: 120
+    }
+  },
+  {
+    label: '活动结束时间',
+    field: 'endTime',
+    formatter: dateFormatter2,
+    isSearch: true,
+    search: {
+      component: 'DatePicker',
+      componentProps: {
+        valueFormat: 'YYYY-MM-DD',
+        type: 'daterange'
+      }
+    },
+    form: {
+      component: 'DatePicker',
+      componentProps: {
+        type: 'date',
+        valueFormat: 'x'
+      }
+    },
+    table: {
+      width: 120
+    }
+  },
+  {
+    label: '优惠类型',
+    field: 'discountType',
+    dictType: DICT_TYPE.PROMOTION_DISCOUNT_TYPE,
+    dictClass: 'number',
+    isSearch: true,
+    form: {
+      component: 'Radio',
+      value: 1
+    }
+  },
+  {
+    label: '活动商品',
+    field: 'spuId',
+    isTable: true,
+    isSearch: false,
+    form: {
+      colProps: {
+        span: 24
+      }
+    },
+    table: {
+      width: 300
+    }
+  },
+  {
+    label: '备注',
+    field: 'remark',
+    isSearch: false,
+    form: {
+      component: 'Input',
+      componentProps: {
+        type: 'textarea',
+        rows: 4
+      },
+      colProps: {
+        span: 24
+      }
+    },
+    table: {
+      width: 300
+    }
+  }
+])
+export const { allSchemas } = useCrudSchemas(crudSchemas)

+ 104 - 0
src/views/mall/promotion/diy/page/DiyPageForm.vue

@@ -0,0 +1,104 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+    >
+      <el-form-item label="页面名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入页面名称" />
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input v-model="formData.remark" placeholder="请输入备注" />
+      </el-form-item>
+      <el-form-item label="预览图" prop="previewPicUrls">
+        <UploadImgs v-model="formData.previewPicUrls" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import * as DiyPageApi from '@/api/mall/promotion/diy/page'
+
+/** 装修页面表单 */
+defineOptions({ name: 'DiyPageForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  remark: undefined,
+  previewPicUrls: []
+})
+const formRules = reactive({
+  name: [{ 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 DiyPageApi.getDiyPage(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as DiyPageApi.DiyPageVO
+    if (formType.value === 'create') {
+      await DiyPageApi.createDiyPage(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DiyPageApi.updateDiyPage(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    remark: undefined,
+    previewPicUrls: []
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 104 - 0
src/views/mall/promotion/diy/template/DiyTemplateForm.vue

@@ -0,0 +1,104 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+    >
+      <el-form-item label="模板名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入模板名称" />
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" />
+      </el-form-item>
+      <el-form-item label="预览图" prop="previewPicUrls">
+        <UploadImgs v-model="formData.previewPicUrls" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import * as DiyTemplateApi from '@/api/mall/promotion/diy/template'
+
+/** 装修模板表单 */
+defineOptions({ name: 'DiyTemplateForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  remark: undefined,
+  previewPicUrls: []
+})
+const formRules = reactive({
+  name: [{ 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 DiyTemplateApi.getDiyTemplate(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as DiyTemplateApi.DiyTemplateVO
+    if (formType.value === 'create') {
+      await DiyTemplateApi.createDiyTemplate(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DiyTemplateApi.updateDiyTemplate(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    remark: undefined,
+    previewPicUrls: []
+  }
+  formRef.value?.resetFields()
+}
+</script>

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


+ 42 - 0
src/views/mall/promotion/kefu/components/tools/EmojiSelectPopover.vue

@@ -0,0 +1,42 @@
+<!-- emoji 表情选择组件 -->
+<template>
+  <el-popover :width="500" placement="top" trigger="click">
+    <template #reference>
+      <Icon :size="30" class="ml-10px cursor-pointer" icon="twemoji:grinning-face" />
+    </template>
+    <ElScrollbar height="300px">
+      <ul class="ml-2 flex flex-wrap px-2">
+        <li
+          v-for="(item, index) in emojiList"
+          :key="index"
+          :style="{
+            borderColor: 'var(--el-color-primary)',
+            color: 'var(--el-color-primary)'
+          }"
+          :title="item.name"
+          class="icon-item mr-2 mt-1 w-1/10 flex cursor-pointer items-center justify-center border border-solid p-2"
+          @click="handleSelect(item)"
+        >
+          <img :src="item.url" class="w-24px h-24px" />
+        </li>
+      </ul>
+    </ElScrollbar>
+  </el-popover>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'EmojiSelectPopover' })
+import { Emoji, useEmoji } from './emoji'
+
+const { getEmojiList } = useEmoji()
+const emojiList = computed(() => getEmojiList())
+
+/** 选择 emoji 表情 */
+const emits = defineEmits<{
+  (e: 'select-emoji', v: Emoji)
+}>()
+const handleSelect = (item: Emoji) => {
+  // 整个 emoji 数据传递出去,方便以后输入框直接显示表情
+  emits('select-emoji', item)
+}
+</script>

+ 126 - 0
src/views/mall/promotion/kefu/components/tools/emoji.ts

@@ -0,0 +1,126 @@
+import { isEmpty } from '@/utils/is'
+
+const emojiList = [
+  { name: '[笑掉牙]', file: 'xiaodiaoya.png' },
+  { name: '[可爱]', file: 'keai.png' },
+  { name: '[冷酷]', file: 'lengku.png' },
+  { name: '[闭嘴]', file: 'bizui.png' },
+  { name: '[生气]', file: 'shengqi.png' },
+  { name: '[惊恐]', file: 'jingkong.png' },
+  { name: '[瞌睡]', file: 'keshui.png' },
+  { name: '[大笑]', file: 'daxiao.png' },
+  { name: '[爱心]', file: 'aixin.png' },
+  { name: '[坏笑]', file: 'huaixiao.png' },
+  { name: '[飞吻]', file: 'feiwen.png' },
+  { name: '[疑问]', file: 'yiwen.png' },
+  { name: '[开心]', file: 'kaixin.png' },
+  { name: '[发呆]', file: 'fadai.png' },
+  { name: '[流泪]', file: 'liulei.png' },
+  { name: '[汗颜]', file: 'hanyan.png' },
+  { name: '[惊悚]', file: 'jingshu.png' },
+  { name: '[困~]', file: 'kun.png' },
+  { name: '[心碎]', file: 'xinsui.png' },
+  { name: '[天使]', file: 'tianshi.png' },
+  { name: '[晕]', file: 'yun.png' },
+  { name: '[啊]', file: 'a.png' },
+  { name: '[愤怒]', file: 'fennu.png' },
+  { name: '[睡着]', file: 'shuizhuo.png' },
+  { name: '[面无表情]', file: 'mianwubiaoqing.png' },
+  { name: '[难过]', file: 'nanguo.png' },
+  { name: '[犯困]', file: 'fankun.png' },
+  { name: '[好吃]', file: 'haochi.png' },
+  { name: '[呕吐]', file: 'outu.png' },
+  { name: '[龇牙]', file: 'ziya.png' },
+  { name: '[懵比]', file: 'mengbi.png' },
+  { name: '[白眼]', file: 'baiyan.png' },
+  { name: '[饿死]', file: 'esi.png' },
+  { name: '[凶]', file: 'xiong.png' },
+  { name: '[感冒]', file: 'ganmao.png' },
+  { name: '[流汗]', file: 'liuhan.png' },
+  { name: '[笑哭]', file: 'xiaoku.png' },
+  { name: '[流口水]', file: 'liukoushui.png' },
+  { name: '[尴尬]', file: 'ganga.png' },
+  { name: '[惊讶]', file: 'jingya.png' },
+  { name: '[大惊]', file: 'dajing.png' },
+  { name: '[不好意思]', file: 'buhaoyisi.png' },
+  { name: '[大闹]', file: 'danao.png' },
+  { name: '[不可思议]', file: 'bukesiyi.png' },
+  { name: '[爱你]', file: 'aini.png' },
+  { name: '[红心]', file: 'hongxin.png' },
+  { name: '[点赞]', file: 'dianzan.png' },
+  { name: '[恶魔]', file: 'emo.png' }
+]
+
+export interface Emoji {
+  name: string
+  url: string
+}
+
+export const useEmoji = () => {
+  const emojiPathList = ref<any[]>([])
+
+  /** 加载本地图片 */
+  const initStaticEmoji = async () => {
+    const pathList = import.meta.glob(
+      '@/views/mall/promotion/kefu/components/asserts/*.{png,jpg,jpeg,svg}'
+    )
+    for (const path in pathList) {
+      const imageModule: any = await pathList[path]()
+      emojiPathList.value.push(imageModule.default)
+    }
+  }
+
+  /** 初始化 */
+  onMounted(async () => {
+    if (isEmpty(emojiPathList.value)) {
+      await initStaticEmoji()
+    }
+  })
+
+  /**
+   * 将文本中的表情替换成图片
+   *
+   * @param data 文本
+   * @return 替换后的文本
+   */
+  const replaceEmoji = (content: string) => {
+    let newData = content
+    if (typeof newData !== 'object') {
+      const reg = /\[(.+?)]/g // [] 中括号
+      const zhEmojiName = newData.match(reg)
+      if (zhEmojiName) {
+        zhEmojiName.forEach((item) => {
+          const emojiFile = getEmojiFileByName(item)
+          newData = newData.replace(
+            item,
+            `<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${emojiFile}"/>`
+          )
+        })
+      }
+    }
+    return newData
+  }
+
+  /**
+   * 获得所有表情
+   *
+   * @return 表情列表
+   */
+  function getEmojiList(): Emoji[] {
+    return emojiList.map((item) => ({
+      url: getEmojiFileByName(item.name),
+      name: item.name
+    })) as Emoji[]
+  }
+
+  function getEmojiFileByName(name: string) {
+    for (const emoji of emojiList) {
+      if (emoji.name === name) {
+        return emojiPathList.value.find((item: string) => item.indexOf(emoji.file) > -1)
+      }
+    }
+    return false
+  }
+
+  return { replaceEmoji, getEmojiList }
+}

+ 87 - 0
src/views/mp/draft/components/DraftTable.vue

@@ -0,0 +1,87 @@
+<template>
+  <div class="waterfall" v-loading="props.loading">
+    <template v-for="item in props.list" :key="item.articleId">
+      <div class="waterfall-item" v-if="item.content && item.content.newsItem">
+        <WxNews :articles="item.content.newsItem" />
+        <!-- 操作按钮 -->
+        <el-row>
+          <el-button
+            type="success"
+            circle
+            @click="emit('publish', item)"
+            v-hasPermi="['mp:free-publish:submit']"
+          >
+            <Icon icon="fa:upload" />
+          </el-button>
+          <el-button
+            type="primary"
+            circle
+            @click="emit('update', item)"
+            v-hasPermi="['mp:draft:update']"
+          >
+            <Icon icon="ep:edit" />
+          </el-button>
+          <el-button
+            type="danger"
+            circle
+            @click="emit('delete', item)"
+            v-hasPermi="['mp:draft:delete']"
+          >
+            <Icon icon="ep:delete" />
+          </el-button>
+        </el-row>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import WxNews from '@/views/mp/components/wx-news'
+
+import { Article } from './types'
+
+const props = defineProps<{
+  list: Article[]
+  loading: boolean
+}>()
+
+const emit = defineEmits<{
+  (e: 'publish', v: Article)
+  (e: 'update', v: Article)
+  (e: 'delete', v: Article)
+}>()
+</script>
+
+<style lang="scss" scoped>
+.waterfall {
+  width: 100%;
+  column-gap: 10px;
+  column-count: 5;
+  margin: 0 auto;
+
+  .waterfall-item {
+    padding: 10px;
+    margin-bottom: 10px;
+    break-inside: avoid;
+    border: 1px solid #eaeaea;
+  }
+}
+
+@media (width >= 992px) and (width <= 1300px) {
+  .waterfall {
+    column-count: 3;
+  }
+}
+
+@media (width >= 768px) and (width <= 991px) {
+  .waterfall {
+    column-count: 2;
+  }
+}
+
+@media (width <= 767px) {
+  .waterfall {
+    column-count: 1;
+  }
+}
+</style>

+ 75 - 0
src/views/mp/draft/editor-config.ts

@@ -0,0 +1,75 @@
+import { IEditorConfig } from '@wangeditor/editor'
+import { getAccessToken, getTenantId } from '@/utils/auth'
+
+const message = useMessage()
+
+type InsertFnType = (url: string, alt: string, href: string) => void
+
+export const createEditorConfig = (
+  server: string,
+  accountId: number | undefined
+): Partial<IEditorConfig> => {
+  return {
+    MENU_CONF: {
+      ['uploadImage']: {
+        server,
+        // 单个文件的最大体积限制,默认为 2M
+        maxFileSize: 5 * 1024 * 1024,
+        // 最多可上传几个文件,默认为 100
+        maxNumberOfFiles: 10,
+        // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
+        allowedFileTypes: ['image/*'],
+
+        // 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
+        meta: {
+          accountId: accountId,
+          type: 'image'
+        },
+        // 将 meta 拼接到 url 参数中,默认 false
+        metaWithUrl: true,
+
+        // 自定义增加 http  header
+        headers: {
+          Accept: '*',
+          Authorization: 'Bearer ' + getAccessToken(),
+          'tenant-id': getTenantId()
+        },
+
+        // 跨域是否传递 cookie ,默认为 false
+        withCredentials: true,
+
+        // 超时时间,默认为 10 秒
+        timeout: 5 * 1000, // 5 秒
+
+        // form-data fieldName,后端接口参数名称,默认值wangeditor-uploaded-image
+        fieldName: 'file',
+
+        // 上传之前触发
+        onBeforeUpload(file: File) {
+          console.log(file)
+          return file
+        },
+        // 上传进度的回调函数
+        onProgress(progress: number) {
+          // progress 是 0-100 的数字
+          console.log('progress', progress)
+        },
+        onSuccess(file: File, res: any) {
+          console.log('onSuccess', file, res)
+        },
+        onFailed(file: File, res: any) {
+          message.alertError(res.message)
+          console.log('onFailed', file, res)
+        },
+        onError(file: File, err: any, res: any) {
+          message.alertError(err.message)
+          console.error('onError', file, err, res)
+        },
+        // 自定义插入图片
+        customInsert(res: any, insertFn: InsertFnType) {
+          insertFn(res.data.url, 'image', res.data.url)
+        }
+      }
+    }
+  }
+}

+ 124 - 0
src/views/system/dict/DictTypeForm.vue

@@ -0,0 +1,124 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+    >
+      <el-form-item label="字典名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入字典名称" />
+      </el-form-item>
+      <el-form-item label="字典类型" prop="type">
+        <el-input
+          v-model="formData.type"
+          :disabled="typeof formData.id !== 'undefined'"
+          placeholder="请输入参数名称"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as DictTypeApi from '@/api/system/dict/dict.type'
+import { CommonStatusEnum } from '@/utils/constants'
+
+defineOptions({ name: 'SystemDictTypeForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: '',
+  type: '',
+  status: CommonStatusEnum.ENABLE,
+  remark: ''
+})
+const formRules = reactive({
+  name: [{ required: true, message: '字典名称不能为空', trigger: 'blur' }],
+  type: [{ required: true, message: '字典类型不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
+})
+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 DictTypeApi.getDictType(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as DictTypeApi.DictTypeVO
+    if (formType.value === 'create') {
+      await DictTypeApi.createDictType(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DictTypeApi.updateDictType(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    type: '',
+    name: '',
+    status: CommonStatusEnum.ENABLE,
+    remark: ''
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 183 - 0
src/views/system/dict/data/DictDataForm.vue

@@ -0,0 +1,183 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+    >
+      <el-form-item label="字典类型" prop="type">
+        <el-input
+          v-model="formData.dictType"
+          :disabled="typeof formData.id !== 'undefined'"
+          placeholder="请输入参数名称"
+        />
+      </el-form-item>
+      <el-form-item label="数据标签" prop="label">
+        <el-input v-model="formData.label" placeholder="请输入数据标签" />
+      </el-form-item>
+      <el-form-item label="数据键值" prop="value">
+        <el-input v-model="formData.value" placeholder="请输入数据键值" />
+      </el-form-item>
+      <el-form-item label="显示排序" prop="sort">
+        <el-input-number v-model="formData.sort" :min="0" controls-position="right" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="颜色类型" prop="colorType">
+        <el-select v-model="formData.colorType">
+          <el-option
+            v-for="item in colorTypeOptions"
+            :key="item.value"
+            :label="item.label + '(' + item.value + ')'"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="CSS Class" prop="cssClass">
+        <el-input v-model="formData.cssClass" placeholder="请输入 CSS Class" />
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as DictDataApi from '@/api/system/dict/dict.data'
+import { CommonStatusEnum } from '@/utils/constants'
+
+defineOptions({ name: 'SystemDictDataForm' })
+
+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,
+  sort: undefined,
+  label: '',
+  value: '',
+  dictType: '',
+  status: CommonStatusEnum.ENABLE,
+  colorType: '',
+  cssClass: '',
+  remark: ''
+})
+const formRules = reactive({
+  label: [{ required: true, message: '数据标签不能为空', trigger: 'blur' }],
+  value: [{ required: true, message: '数据键值不能为空', trigger: 'blur' }],
+  sort: [{ required: true, message: '数据顺序不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
+})
+const formRef = ref() // 表单 Ref
+
+// 数据标签回显样式
+const colorTypeOptions = readonly([
+  {
+    value: 'default',
+    label: '默认'
+  },
+  {
+    value: 'primary',
+    label: '主要'
+  },
+  {
+    value: 'success',
+    label: '成功'
+  },
+  {
+    value: 'info',
+    label: '信息'
+  },
+  {
+    value: 'warning',
+    label: '警告'
+  },
+  {
+    value: 'danger',
+    label: '危险'
+  }
+])
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number, dictType?: string) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  if (dictType) {
+    formData.value.dictType = dictType
+  }
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await DictDataApi.getDictData(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as DictDataApi.DictDataVO
+    if (formType.value === 'create') {
+      await DictDataApi.createDictData(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DictDataApi.updateDictData(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    sort: undefined,
+    label: '',
+    value: '',
+    dictType: '',
+    status: CommonStatusEnum.ENABLE,
+    colorType: '',
+    cssClass: '',
+    remark: ''
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 35 - 0
types/env.d.ts

@@ -0,0 +1,35 @@
+/// <reference types="vite/client" />
+
+declare module '*.vue' {
+  import { DefineComponent } from 'vue'
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
+  const component: DefineComponent<{}, {}, any>
+  export default component
+}
+
+interface ImportMetaEnv {
+  readonly VITE_APP_TITLE: string
+  readonly VITE_PORT: number
+  readonly VITE_OPEN: string
+  readonly VITE_DEV: string
+  readonly VITE_APP_CAPTCHA_ENABLE: string
+  readonly VITE_APP_TENANT_ENABLE: string
+  readonly VITE_APP_DEFAULT_LOGIN_TENANT: string
+  readonly VITE_APP_DEFAULT_LOGIN_USERNAME: string
+  readonly VITE_APP_DEFAULT_LOGIN_PASSWORD: string
+  readonly VITE_APP_DOCALERT_ENABLE: string
+  readonly VITE_BASE_URL: string
+  readonly VITE_UPLOAD_URL: string
+  readonly VITE_API_URL: string
+  readonly VITE_BASE_PATH: string
+  readonly VITE_DROP_DEBUGGER: string
+  readonly VITE_DROP_CONSOLE: string
+  readonly VITE_SOURCEMAP: string
+  readonly VITE_OUT_DIR: string
+}
+
+declare global {
+  interface ImportMeta {
+    readonly env: ImportMetaEnv
+  }
+}