浏览代码

Merge remote-tracking branch 'yudao/master'

puhui999 7 月之前
父节点
当前提交
bf3718853f

+ 5 - 4
.vscode/settings.json

@@ -62,16 +62,16 @@
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "[typescript]": {
-    "editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "[typescriptreact]": {
-    "editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "[html]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "[css]": {
-    "editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "[less]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
@@ -86,8 +86,9 @@
     "source.fixAll.eslint": "explicit",
     "source.fixAll.stylelint": "explicit"
   },
+  "editor.formatOnSave": true,
   "[vue]": {
-    "editor.defaultFormatter": "octref.vetur"
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "i18n-ally.localesPaths": ["src/locales"],
   "i18n-ally.keystyle": "nested",

文件差异内容过多而无法显示
+ 17486 - 0
package-lock.json


+ 1 - 1
package.json

@@ -36,7 +36,7 @@
     "@wangeditor/editor-for-vue": "^5.1.10",
     "@zxcvbn-ts/core": "^3.0.4",
     "animate.css": "^4.1.1",
-    "axios": "^1.6.8",
+    "axios": "1.9.0",
     "benz-amr-recorder": "^1.1.5",
     "bpmn-js-token-simulation": "^0.36.0",
     "camunda-bpmn-moddle": "^7.0.1",

+ 18 - 2
src/components/Dialog/src/Dialog.vue

@@ -1,9 +1,11 @@
 <script lang="ts" setup>
 import { propTypes } from '@/utils/propTypes'
 import { isNumber } from '@/utils/is'
+
 defineOptions({ name: 'Dialog' })
 
 const slots = useSlots()
+const emits = defineEmits(['update:modelValue'])
 
 const props = defineProps({
   modelValue: propTypes.bool.def(false),
@@ -55,6 +57,17 @@ const dialogStyle = computed(() => {
     height: unref(dialogHeight)
   }
 })
+
+const closing = ref(false)
+
+function closeHandler() {
+  emits('update:modelValue', false)
+  closing.value = true
+}
+
+function closedHandler() {
+  closing.value = false
+}
 </script>
 
 <template>
@@ -68,7 +81,8 @@ const dialogStyle = computed(() => {
     draggable
     class="com-dialog"
     :show-close="false"
-    @close="$emit('update:modelValue', false)"
+    @close="closeHandler"
+    @closed="closedHandler"
   >
     <template #header="{ close }">
       <div class="relative h-54px flex items-center justify-between pl-15px pr-15px">
@@ -102,7 +116,9 @@ const dialogStyle = computed(() => {
     </ElScrollbar>
     <slot v-else></slot>
     <template v-if="slots.footer" #footer>
-      <slot name="footer"></slot>
+      <div :style="{ 'pointer-events': closing ? 'none' : 'auto' }">
+        <slot name="footer"></slot>
+      </div>
     </template>
   </ElDialog>
 </template>

+ 15 - 17
src/components/bpmnProcessDesigner/package/designer/plugins/translate/customTranslate.js

@@ -23,22 +23,20 @@
 
 export default function customTranslate(translations) {
   return function (template, replacements) {
-    replacements = replacements || {}
-    // Translate
-    template = translations[template] || template
+    replacements = replacements || {};
+    // 将模板和翻译字典的键统一转换为小写进行匹配
+    const lowerTemplate = template.toLowerCase();
+    const translation = Object.keys(translations).find(key => key.toLowerCase() === lowerTemplate);
 
-    // Replace
+    // 如果找到匹配的翻译,使用翻译后的模板
+    if (translation) {
+      template = translations[translation];
+    }
+
+    // 替换模板中的占位符
     return template.replace(/{([^}]+)}/g, function (_, key) {
-      let str = replacements[key]
-      if (
-        translations[replacements[key]] !== null &&
-        translations[replacements[key]] !== undefined
-      ) {
-        // eslint-disable-next-line no-mixed-spaces-and-tabs
-        str = translations[replacements[key]]
-        // eslint-disable-next-line no-mixed-spaces-and-tabs
-      }
-      return str || '{' + key + '}'
-    })
-  }
-}
+      // 如果替换值存在,返回替换值;否则返回原始占位符
+      return replacements[key] !== undefined ? replacements[key] : `{${key}}`;
+    });
+  };
+}

+ 2 - 2
src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js

@@ -45,8 +45,8 @@ export default {
   'Service Task': '服务任务',
   'Script Task': '脚本任务',
   'Call Activity': '调用活动',
-  'Sub Process (collapsed)': '子流程(折叠的)',
-  'Sub Process (expanded)': '子流程(展开的)',
+  'Sub-Process (collapsed)': '子流程(折叠的)',
+  'Sub-Process (expanded)': '子流程(展开的)',
   'Start Event': '开始事件',
   StartEvent: '开始事件',
   'Intermediate Throw Event': '中间事件',

+ 54 - 1
src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue

@@ -68,6 +68,11 @@
           :business-object="elementBusinessObject"
         />
       </el-collapse-item>
+      <!-- 新增的时间事件配置项 -->
+      <el-collapse-item v-if="elementType === 'IntermediateCatchEvent'" name="timeEvent">
+        <template #title><Icon icon="ep:timer" />时间事件</template>
+        <TimeEventConfig :businessObject="bpmnElement.value?.businessObject" :key="elementId" />
+      </el-collapse-item>
     </el-collapse>
   </div>
 </template>
@@ -83,6 +88,8 @@ import ElementProperties from './properties/ElementProperties.vue'
 // import ElementForm from './form/ElementForm.vue'
 import UserTaskListeners from './listeners/UserTaskListeners.vue'
 import { getTaskCollapseItemName, isTaskCollapseItemShow } from './task/data'
+import TimeEventConfig from './time-event-config/TimeEventConfig.vue'
+import { ref, watch, onMounted } from 'vue'
 
 defineOptions({ name: 'MyPropertiesPanel' })
 
@@ -121,6 +128,8 @@ const formVisible = ref(false) // 表单配置
 const bpmnElement = ref()
 const isReady = ref(false)
 
+const type = ref('time')
+const condition = ref('')
 provide('prefix', props.prefix)
 provide('width', props.width)
 
@@ -141,7 +150,7 @@ const initBpmnInstances = () => {
     }
 
     // 检查所有实例是否都存在
-    const allInstancesExist = Object.values(instances).every(instance => instance)
+    const allInstancesExist = Object.values(instances).every((instance) => instance)
     if (allInstancesExist) {
       const w = window as any
       w.bpmnInstances = instances
@@ -255,4 +264,48 @@ watch(
     activeTab.value = 'base'
   }
 )
+
+function updateNode() {
+  const moddle = window.bpmnInstances?.moddle
+  const modeling = window.bpmnInstances?.modeling
+  const elementRegistry = window.bpmnInstances?.elementRegistry
+  if (!moddle || !modeling || !elementRegistry) return
+
+  const element = elementRegistry.get(props.businessObject.id)
+  if (!element) return
+
+  let timerDef = moddle.create('bpmn:TimerEventDefinition', {})
+  if (type.value === 'time') {
+    timerDef.timeDate = moddle.create('bpmn:FormalExpression', { body: condition.value })
+  } else if (type.value === 'duration') {
+    timerDef.timeDuration = moddle.create('bpmn:FormalExpression', { body: condition.value })
+  } else if (type.value === 'cycle') {
+    timerDef.timeCycle = moddle.create('bpmn:FormalExpression', { body: condition.value })
+  }
+
+  modeling.updateModdleProperties(element, element.businessObject, {
+    eventDefinitions: [timerDef]
+  })
+}
+
+// 初始化和监听
+function syncFromBusinessObject() {
+  if (props.businessObject) {
+    const timerDef = (props.businessObject.eventDefinitions || [])[0]
+    if (timerDef) {
+      if (timerDef.timeDate) {
+        type.value = 'time'
+        condition.value = timerDef.timeDate.body
+      } else if (timerDef.timeDuration) {
+        type.value = 'duration'
+        condition.value = timerDef.timeDuration.body
+      } else if (timerDef.timeCycle) {
+        type.value = 'cycle'
+        condition.value = timerDef.timeCycle.body
+      }
+    }
+  }
+}
+onMounted(syncFromBusinessObject)
+watch(() => props.businessObject, syncFromBusinessObject, { deep: true })
 </script>

+ 285 - 0
src/components/bpmnProcessDesigner/package/penal/time-event-config/CycleConfig.vue

@@ -0,0 +1,285 @@
+<template>
+  <el-tabs v-model="tab">
+    <el-tab-pane label="CRON表达式" name="cron">
+      <div style="margin-bottom: 10px">
+        <el-input
+          v-model="cronStr"
+          readonly
+          style="width: 400px; font-weight: bold"
+          :key="'cronStr'"
+        />
+      </div>
+      <div style="display: flex; gap: 8px; margin-bottom: 8px">
+        <el-input v-model="fields.second" placeholder="秒" style="width: 80px" :key="'second'" />
+        <el-input v-model="fields.minute" placeholder="分" style="width: 80px" :key="'minute'" />
+        <el-input v-model="fields.hour" placeholder="时" style="width: 80px" :key="'hour'" />
+        <el-input v-model="fields.day" placeholder="天" style="width: 80px" :key="'day'" />
+        <el-input v-model="fields.month" placeholder="月" style="width: 80px" :key="'month'" />
+        <el-input v-model="fields.week" placeholder="周" style="width: 80px" :key="'week'" />
+        <el-input v-model="fields.year" placeholder="年" style="width: 80px" :key="'year'" />
+      </div>
+      <el-tabs v-model="activeField" type="card" style="margin-bottom: 8px">
+        <el-tab-pane v-for="f in cronFieldList" :label="f.label" :name="f.key" :key="f.key">
+          <div style="margin-bottom: 8px">
+            <el-radio-group v-model="cronMode[f.key]" :key="'radio-' + f.key">
+              <el-radio label="every" :key="'every-' + f.key">每{{ f.label }}</el-radio>
+              <el-radio label="range" :key="'range-' + f.key"
+                >从
+                <el-input-number
+                  v-model="cronRange[f.key][0]"
+                  :min="f.min"
+                  :max="f.max"
+                  size="small"
+                  style="width: 60px"
+                  :key="'range0-' + f.key"
+                />
+                到
+                <el-input-number
+                  v-model="cronRange[f.key][1]"
+                  :min="f.min"
+                  :max="f.max"
+                  size="small"
+                  style="width: 60px"
+                  :key="'range1-' + f.key"
+                />
+                之间每{{ f.label }}</el-radio
+              >
+              <el-radio label="step" :key="'step-' + f.key"
+                >从第
+                <el-input-number
+                  v-model="cronStep[f.key][0]"
+                  :min="f.min"
+                  :max="f.max"
+                  size="small"
+                  style="width: 60px"
+                  :key="'step0-' + f.key"
+                />
+                开始每
+                <el-input-number
+                  v-model="cronStep[f.key][1]"
+                  :min="1"
+                  :max="f.max"
+                  size="small"
+                  style="width: 60px"
+                  :key="'step1-' + f.key"
+                />
+                {{ f.label }}</el-radio
+              >
+              <el-radio label="appoint" :key="'appoint-' + f.key">指定</el-radio>
+            </el-radio-group>
+          </div>
+          <div v-if="cronMode[f.key] === 'appoint'">
+            <el-checkbox-group v-model="cronAppoint[f.key]" :key="'group-' + f.key">
+              <el-checkbox
+                v-for="n in f.max + 1"
+                :label="pad(n - 1)"
+                :key="'cb-' + f.key + '-' + (n - 1)"
+                >{{ pad(n - 1) }}</el-checkbox
+              >
+            </el-checkbox-group>
+          </div>
+        </el-tab-pane>
+      </el-tabs>
+    </el-tab-pane>
+    <el-tab-pane label="标准格式" name="iso" :key="'iso-tab'">
+      <div style="margin-bottom: 10px">
+        <el-input
+          v-model="isoStr"
+          placeholder="如R1/2025-05-21T21:59:54/P3DT30M30S"
+          style="width: 400px; font-weight: bold"
+          :key="'isoStr'"
+        />
+      </div>
+      <div style="margin-bottom: 10px"
+        >循环次数:<el-input-number v-model="repeat" :min="1" style="width: 100px" :key="'repeat'"
+      /></div>
+      <div style="margin-bottom: 10px"
+        >日期时间:<el-date-picker
+          v-model="isoDate"
+          type="datetime"
+          placeholder="选择日期时间"
+          style="width: 200px"
+          :key="'isoDate'"
+      /></div>
+      <div style="margin-bottom: 10px"
+        >当前时长:<el-input
+          v-model="isoDuration"
+          placeholder="如P3DT30M30S"
+          style="width: 200px"
+          :key="'isoDuration'"
+      /></div>
+      <div>
+        <div
+          >秒:<el-button
+            v-for="s in [5, 10, 30, 50]"
+            @click="setDuration('S', s)"
+            :key="'sec-' + s"
+            >{{ s }}</el-button
+          >自定义</div
+        >
+        <div
+          >分:<el-button
+            v-for="m in [5, 10, 30, 50]"
+            @click="setDuration('M', m)"
+            :key="'min-' + m"
+            >{{ m }}</el-button
+          >自定义</div
+        >
+        <div
+          >小时:<el-button
+            v-for="h in [4, 8, 12, 24]"
+            @click="setDuration('H', h)"
+            :key="'hour-' + h"
+            >{{ h }}</el-button
+          >自定义</div
+        >
+        <div
+          >天:<el-button
+            v-for="d in [1, 2, 3, 4]"
+            @click="setDuration('D', d)"
+            :key="'day-' + d"
+            >{{ d }}</el-button
+          >自定义</div
+        >
+        <div
+          >月:<el-button
+            v-for="mo in [1, 2, 3, 4]"
+            @click="setDuration('M', mo)"
+            :key="'mon-' + mo"
+            >{{ mo }}</el-button
+          >自定义</div
+        >
+        <div
+          >年:<el-button
+            v-for="y in [1, 2, 3, 4]"
+            @click="setDuration('Y', y)"
+            :key="'year-' + y"
+            >{{ y }}</el-button
+          >自定义</div
+        >
+      </div>
+    </el-tab-pane>
+  </el-tabs>
+</template>
+<script setup>
+import { ref, watch, computed } from 'vue'
+const props = defineProps({ value: String })
+const emit = defineEmits(['change'])
+
+const tab = ref('cron')
+const cronStr = ref(props.value || '* * * * * ?')
+const fields = ref({
+  second: '*',
+  minute: '*',
+  hour: '*',
+  day: '*',
+  month: '*',
+  week: '?',
+  year: ''
+})
+const cronFieldList = [
+  { key: 'second', label: '秒', min: 0, max: 59 },
+  { key: 'minute', label: '分', min: 0, max: 59 },
+  { key: 'hour', label: '时', min: 0, max: 23 },
+  { key: 'day', label: '天', min: 1, max: 31 },
+  { key: 'month', label: '月', min: 1, max: 12 },
+  { key: 'week', label: '周', min: 1, max: 7 },
+  { key: 'year', label: '年', min: 1970, max: 2099 }
+]
+const activeField = ref('second')
+const cronMode = ref({
+  second: 'appoint',
+  minute: 'every',
+  hour: 'every',
+  day: 'every',
+  month: 'every',
+  week: 'every',
+  year: 'every'
+})
+const cronAppoint = ref({
+  second: ['00', '01'],
+  minute: [],
+  hour: [],
+  day: [],
+  month: [],
+  week: [],
+  year: []
+})
+const cronRange = ref({
+  second: [0, 1],
+  minute: [0, 1],
+  hour: [0, 1],
+  day: [1, 2],
+  month: [1, 2],
+  week: [1, 2],
+  year: [1970, 1971]
+})
+const cronStep = ref({
+  second: [1, 1],
+  minute: [1, 1],
+  hour: [1, 1],
+  day: [1, 1],
+  month: [1, 1],
+  week: [1, 1],
+  year: [1970, 1]
+})
+
+function pad(n) {
+  return n < 10 ? '0' + n : '' + n
+}
+
+watch(
+  [fields, cronMode, cronAppoint, cronRange, cronStep],
+  () => {
+    // 组装cron表达式
+    let arr = cronFieldList.map((f) => {
+      if (cronMode.value[f.key] === 'every') return '*'
+      if (cronMode.value[f.key] === 'appoint') return cronAppoint.value[f.key].join(',') || '*'
+      if (cronMode.value[f.key] === 'range')
+        return `${cronRange.value[f.key][0]}-${cronRange.value[f.key][1]}`
+      if (cronMode.value[f.key] === 'step')
+        return `${cronStep.value[f.key][0]}/${cronStep.value[f.key][1]}`
+      return fields.value[f.key] || '*'
+    })
+    // week和year特殊处理
+    arr[5] = arr[5] || '?'
+    cronStr.value = arr.join(' ')
+    if (tab.value === 'cron') emit('change', cronStr.value)
+  },
+  { deep: true }
+)
+
+// 标准格式
+const isoStr = ref('')
+const repeat = ref(1)
+const isoDate = ref('')
+const isoDuration = ref('')
+function setDuration(type, val) {
+  // 组装ISO 8601字符串
+  let d = isoDuration.value
+  if (!d.includes(type)) d += val + type
+  else d = d.replace(new RegExp(`\\d+${type}`), val + type)
+  isoDuration.value = d
+  updateIsoStr()
+}
+function updateIsoStr() {
+  let str = `R${repeat.value}`
+  if (isoDate.value)
+    str +=
+      '/' +
+      (typeof isoDate.value === 'string' ? isoDate.value : new Date(isoDate.value).toISOString())
+  if (isoDuration.value) str += '/' + isoDuration.value
+  isoStr.value = str
+  if (tab.value === 'iso') emit('change', isoStr.value)
+}
+watch([repeat, isoDate, isoDuration], updateIsoStr)
+watch(
+  () => props.value,
+  (val) => {
+    if (!val) return
+    if (tab.value === 'cron') cronStr.value = val
+    if (tab.value === 'iso') isoStr.value = val
+  },
+  { immediate: true }
+)
+</script>

+ 86 - 0
src/components/bpmnProcessDesigner/package/penal/time-event-config/DurationConfig.vue

@@ -0,0 +1,86 @@
+<template>
+  <div>
+    <div style="margin-bottom: 10px"
+      >当前选择:<el-input v-model="isoString" readonly style="width: 300px"
+    /></div>
+    <div v-for="unit in units" :key="unit.key" style="margin-bottom: 8px">
+      <span>{{ unit.label }}:</span>
+      <el-button-group>
+        <el-button
+          v-for="val in unit.presets"
+          :key="val"
+          size="mini"
+          @click="setUnit(unit.key, val)"
+          >{{ val }}</el-button
+        >
+        <el-input
+          v-model.number="custom[unit.key]"
+          size="mini"
+          style="width: 60px; margin-left: 8px"
+          placeholder="自定义"
+          @change="setUnit(unit.key, custom[unit.key])"
+        />
+      </el-button-group>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, watch, computed } from 'vue'
+const props = defineProps({ value: String })
+const emit = defineEmits(['change'])
+
+const units = [
+  { key: 'Y', label: '年', presets: [1, 2, 3, 4] },
+  { key: 'M', label: '月', presets: [1, 2, 3, 4] },
+  { key: 'D', label: '天', presets: [1, 2, 3, 4] },
+  { key: 'H', label: '时', presets: [4, 8, 12, 24] },
+  { key: 'm', label: '分', presets: [5, 10, 30, 50] },
+  { key: 'S', label: '秒', presets: [5, 10, 30, 50] }
+]
+const custom = ref({ Y: '', M: '', D: '', H: '', m: '', S: '' })
+const isoString = ref('')
+
+function setUnit(key, val) {
+  if (!val || isNaN(val)) {
+    custom.value[key] = ''
+    return
+  }
+  custom.value[key] = val
+  updateIsoString()
+}
+
+function updateIsoString() {
+  let str = 'P'
+  if (custom.value.Y) str += custom.value.Y + 'Y'
+  if (custom.value.M) str += custom.value.M + 'M'
+  if (custom.value.D) str += custom.value.D + 'D'
+  if (custom.value.H || custom.value.m || custom.value.S) str += 'T'
+  if (custom.value.H) str += custom.value.H + 'H'
+  if (custom.value.m) str += custom.value.m + 'M'
+  if (custom.value.S) str += custom.value.S + 'S'
+  isoString.value = str === 'P' ? '' : str
+  emit('change', isoString.value)
+}
+
+watch(
+  () => props.value,
+  (val) => {
+    if (!val) return
+    // 解析ISO 8601字符串到custom
+    const match = val.match(
+      /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/
+    )
+    if (match) {
+      custom.value.Y = match[1] || ''
+      custom.value.M = match[2] || ''
+      custom.value.D = match[3] || ''
+      custom.value.H = match[4] || ''
+      custom.value.m = match[5] || ''
+      custom.value.S = match[6] || ''
+      updateIsoString()
+    }
+  },
+  { immediate: true }
+)
+</script>

+ 312 - 0
src/components/bpmnProcessDesigner/package/penal/time-event-config/TimeEventConfig.vue

@@ -0,0 +1,312 @@
+<template>
+  <div class="panel-tab__content">
+    <div style="margin-top: 10px">
+      <span>类型:</span>
+      <el-button-group>
+        <el-button size="mini" :type="type === 'time' ? 'primary' : ''" @click="setType('time')"
+          >时间</el-button
+        >
+        <el-button
+          size="mini"
+          :type="type === 'duration' ? 'primary' : ''"
+          @click="setType('duration')"
+          >持续</el-button
+        >
+        <el-button size="mini" :type="type === 'cycle' ? 'primary' : ''" @click="setType('cycle')"
+          >循环</el-button
+        >
+      </el-button-group>
+      <el-icon v-if="valid" color="green" style="margin-left: 8px"><CircleCheckFilled /></el-icon>
+    </div>
+    <div style="margin-top: 10px; display: flex; align-items: center">
+      <span>条件:</span>
+      <el-input
+        v-model="condition"
+        :placeholder="placeholder"
+        style="width: calc(100% - 100px)"
+        :readonly="type !== 'duration' && type !== 'cycle'"
+        @focus="handleInputFocus"
+        @blur="updateNode"
+      >
+        <template #suffix>
+          <el-tooltip v-if="!valid" content="格式错误" placement="top">
+            <el-icon color="orange"><WarningFilled /></el-icon>
+          </el-tooltip>
+          <el-tooltip :content="helpText" placement="top">
+            <el-icon color="#409EFF" style="cursor: pointer" @click="showHelp = true"
+              ><QuestionFilled
+            /></el-icon>
+          </el-tooltip>
+          <el-button
+            v-if="type === 'time'"
+            @click="showDatePicker = true"
+            style="margin-left: 4px"
+            circle
+            size="small"
+          >
+            <Icon icon="ep:calendar" />
+          </el-button>
+          <el-button
+            v-if="type === 'duration'"
+            @click="showDurationDialog = true"
+            style="margin-left: 4px"
+            circle
+            size="small"
+          >
+            <Icon icon="ep:timer" />
+          </el-button>
+          <el-button
+            v-if="type === 'cycle'"
+            @click="showCycleDialog = true"
+            style="margin-left: 4px"
+            circle
+            size="small"
+          >
+            <Icon icon="ep:setting" />
+          </el-button>
+        </template>
+      </el-input>
+    </div>
+    <!-- 时间选择器 -->
+    <el-dialog
+      v-model="showDatePicker"
+      title="选择时间"
+      width="400px"
+      @close="showDatePicker = false"
+    >
+      <el-date-picker
+        v-model="dateValue"
+        type="datetime"
+        placeholder="选择日期时间"
+        style="width: 100%"
+        @change="onDateChange"
+      />
+      <template #footer>
+        <el-button @click="showDatePicker = false">取消</el-button>
+        <el-button type="primary" @click="onDateConfirm">确定</el-button>
+      </template>
+    </el-dialog>
+    <!-- 持续时长选择器 -->
+    <el-dialog
+      v-model="showDurationDialog"
+      title="时间配置"
+      width="600px"
+      @close="showDurationDialog = false"
+    >
+      <DurationConfig :value="condition" @change="onDurationChange" />
+      <template #footer>
+        <el-button @click="showDurationDialog = false">取消</el-button>
+        <el-button type="primary" @click="onDurationConfirm">确定</el-button>
+      </template>
+    </el-dialog>
+    <!-- 循环配置器 -->
+    <el-dialog
+      v-model="showCycleDialog"
+      title="时间配置"
+      width="800px"
+      @close="showCycleDialog = false"
+    >
+      <CycleConfig :value="condition" @change="onCycleChange" />
+      <template #footer>
+        <el-button @click="showCycleDialog = false">取消</el-button>
+        <el-button type="primary" @click="onCycleConfirm">确定</el-button>
+      </template>
+    </el-dialog>
+    <!-- 帮助说明 -->
+    <el-dialog v-model="showHelp" title="格式说明" width="600px" @close="showHelp = false">
+      <div v-html="helpHtml"></div>
+      <template #footer>
+        <el-button @click="showHelp = false">关闭</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, watch, onMounted } from 'vue'
+import { CircleCheckFilled, WarningFilled, QuestionFilled } from '@element-plus/icons-vue'
+import DurationConfig from './DurationConfig.vue'
+import CycleConfig from './CycleConfig.vue'
+import { createListenerObject, updateElementExtensions } from '../../utils'
+const bpmnInstances = () => (window as any).bpmnInstances
+const props = defineProps({ businessObject: Object })
+const type = ref('time')
+const condition = ref('')
+const valid = ref(true)
+const showDatePicker = ref(false)
+const showDurationDialog = ref(false)
+const showCycleDialog = ref(false)
+const showHelp = ref(false)
+const dateValue = ref(null)
+const bpmnElement = ref(null)
+
+const placeholder = computed(() => {
+  if (type.value === 'time') return '请输入时间'
+  if (type.value === 'duration') return '请输入持续时长'
+  if (type.value === 'cycle') return '请输入循环表达式'
+  return ''
+})
+const helpText = computed(() => {
+  if (type.value === 'time') return '选择具体时间'
+  if (type.value === 'duration') return 'ISO 8601格式,如PT1H'
+  if (type.value === 'cycle') return 'CRON表达式或ISO 8601周期'
+  return ''
+})
+const helpHtml = computed(() => {
+  if (type.value === 'duration') {
+    return `指定定时器之前要等待多长时间。S表示秒,M表示分,D表示天;P表示时间段,T表示精确到时间的时间段。<br>
+    时间格式依然为ISO 8601格式,一年两个月三天四小时五分六秒内,可以写成P1Y2M3DT4H5M6S。<br>
+    P是开始标记,T是时间和日期分割标记,没有日期只有时间T是不能省去的,比如1小时执行一次应写成PT1H。`
+  }
+  if (type.value === 'cycle') {
+    return `支持CRON表达式(如0 0/30 * * * ?)或ISO 8601周期(如R3/PT10M)。`
+  }
+  return ''
+})
+
+// 初始化和监听
+function syncFromBusinessObject() {
+  if (props.businessObject) {
+    const timerDef = (props.businessObject.eventDefinitions || [])[0]
+    if (timerDef) {
+      if (timerDef.timeDate) {
+        type.value = 'time'
+        condition.value = timerDef.timeDate.body
+      } else if (timerDef.timeDuration) {
+        type.value = 'duration'
+        condition.value = timerDef.timeDuration.body
+      } else if (timerDef.timeCycle) {
+        type.value = 'cycle'
+        condition.value = timerDef.timeCycle.body
+      }
+    }
+  }
+}
+onMounted(syncFromBusinessObject)
+
+// 切换类型
+function setType(t) {
+  type.value = t
+  condition.value = ''
+  updateNode()
+}
+
+// 输入校验
+watch([type, condition], () => {
+  valid.value = validate()
+  // updateNode() // 可以注释掉,避免频繁触发
+})
+
+function validate() {
+  if (type.value === 'time') {
+    return !!condition.value && !isNaN(Date.parse(condition.value))
+  }
+  if (type.value === 'duration') {
+    return /^P.*$/.test(condition.value)
+  }
+  if (type.value === 'cycle') {
+    return /^([0-9*\/?, ]+|R\d*\/P.*)$/.test(condition.value)
+  }
+  return true
+}
+
+// 选择时间
+function onDateChange(val) {
+  dateValue.value = val
+}
+function onDateConfirm() {
+  if (dateValue.value) {
+    condition.value = new Date(dateValue.value).toISOString()
+    showDatePicker.value = false
+    updateNode()
+  }
+}
+
+// 持续时长
+function onDurationChange(val) {
+  condition.value = val
+}
+function onDurationConfirm() {
+  showDurationDialog.value = false
+  updateNode()
+}
+
+// 循环
+function onCycleChange(val) {
+  condition.value = val
+}
+function onCycleConfirm() {
+  showCycleDialog.value = false
+  updateNode()
+}
+
+// 输入框聚焦时弹窗(可选)
+function handleInputFocus() {
+  if (type.value === 'time') showDatePicker.value = true
+  if (type.value === 'duration') showDurationDialog.value = true
+  if (type.value === 'cycle') showCycleDialog.value = true
+}
+
+// 同步到节点
+function updateNode() {
+  const moddle = window.bpmnInstances?.moddle
+  const modeling = window.bpmnInstances?.modeling
+  const elementRegistry = window.bpmnInstances?.elementRegistry
+  if (!moddle || !modeling || !elementRegistry) return
+
+  // 获取元素
+  if (!props.businessObject || !props.businessObject.id) return
+  const element = elementRegistry.get(props.businessObject.id)
+  if (!element) return
+
+  // 1. 复用原有 timerDef,或新建
+  let timerDef =
+    element.businessObject.eventDefinitions && element.businessObject.eventDefinitions[0]
+  if (!timerDef) {
+    timerDef = bpmnInstances().bpmnFactory.create('bpmn:TimerEventDefinition', {})
+    modeling.updateProperties(element, {
+      eventDefinitions: [timerDef]
+    })
+  }
+
+  // 2. 清空原有
+  delete timerDef.timeDate
+  delete timerDef.timeDuration
+  delete timerDef.timeCycle
+
+  // 3. 设置新的
+  if (type.value === 'time' && condition.value) {
+    timerDef.timeDate = bpmnInstances().bpmnFactory.create('bpmn:FormalExpression', {
+      body: condition.value
+    })
+  } else if (type.value === 'duration' && condition.value) {
+    timerDef.timeDuration = bpmnInstances().bpmnFactory.create('bpmn:FormalExpression', {
+      body: condition.value
+    })
+  } else if (type.value === 'cycle' && condition.value) {
+    timerDef.timeCycle = bpmnInstances().bpmnFactory.create('bpmn:FormalExpression', {
+      body: condition.value
+    })
+  }
+
+  bpmnInstances().modeling.updateProperties(toRaw(element), {
+    eventDefinitions: [timerDef]
+  })
+}
+
+watch(
+  () => props.businessObject,
+  (val) => {
+    if (val) {
+      nextTick(() => {
+        syncFromBusinessObject()
+      })
+    }
+  },
+  { immediate: true }
+)
+</script>
+
+<style scoped>
+/* 相关样式 */
+</style>

+ 41 - 4
src/store/modules/app.ts

@@ -1,11 +1,12 @@
-import { defineStore } from 'pinia'
-import { store } from '../index'
-import { humpToUnderline, setCssVar } from '@/utils'
-import { ElMessage } from 'element-plus'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
 import { ElementPlusSize } from '@/types/elementPlus'
 import { LayoutType } from '@/types/layout'
 import { ThemeTypes } from '@/types/theme'
+import { humpToUnderline, setCssVar } from '@/utils'
+import { getCssColorVariable, hexToRGB, mix } from '@/utils/color'
+import { ElMessage } from 'element-plus'
+import { defineStore } from 'pinia'
+import { store } from '../index'
 
 const { wsCache } = useCache()
 
@@ -183,6 +184,40 @@ export const useAppStore = defineStore('app', {
     }
   },
   actions: {
+    setPrimaryLight() {
+      if (this.theme.elColorPrimary) {
+        const elColorPrimary = this.theme.elColorPrimary
+        const color = this.isDark ? '#000000' : '#ffffff'
+        const lightList = [3, 5, 7, 8, 9]
+        lightList.forEach((v) => {
+          setCssVar(`--el-color-primary-light-${v}`, mix(color, elColorPrimary, v / 10))
+        })
+        setCssVar(`--el-color-primary-dark-2`, mix(color, elColorPrimary, 0.2))
+
+        this.setAllColorRgbVars()
+      }
+    },
+
+    // 处理element自带的主题色和辅助色的-rgb切换主题变化,如:--el-color-primary-rgb
+    setAllColorRgbVars() {
+      // 需要处理的颜色类型列表
+      const colorTypes = ['primary', 'success', 'warning', 'danger', 'error', 'info']
+
+      colorTypes.forEach((type) => {
+        // 获取当前颜色值
+        const colorValue = getCssColorVariable(`--el-color-${type}`)
+        if (colorValue) {
+          // 转换为rgba并提取RGB部分
+          const rgbaString = hexToRGB(colorValue, 1)
+          const rgbValues = rgbaString.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i)
+          if (rgbValues) {
+            const [, r, g, b] = rgbValues
+            // 设置对应的RGB变量
+            setCssVar(`--el-color-${type}-rgb`, `${r}, ${g}, ${b}`)
+          }
+        }
+      })
+    },
     setBreadcrumb(breadcrumb: boolean) {
       this.breadcrumb = breadcrumb
     },
@@ -256,6 +291,7 @@ export const useAppStore = defineStore('app', {
         document.documentElement.classList.remove('dark')
       }
       wsCache.set(CACHE_KEY.IS_DARK, this.isDark)
+      this.setPrimaryLight()
     },
     setCurrentSize(currentSize: ElementPlusSize) {
       this.currentSize = currentSize
@@ -272,6 +308,7 @@ export const useAppStore = defineStore('app', {
       for (const key in this.theme) {
         setCssVar(`--${humpToUnderline(key)}`, this.theme[key])
       }
+      this.setPrimaryLight()
     },
     setFooter(footer: boolean) {
       this.footer = footer

+ 43 - 0
src/utils/color.ts

@@ -172,3 +172,46 @@ export const PREDEFINE_COLORS = [
   '#1f73c3',
   '#711f57'
 ]
+
+
+/**
+ * Mixes two colors.
+ *
+ * @param {string} color1 - The first color, should be a 6-digit hexadecimal color code starting with `#`.
+ * @param {string} color2 - The second color, should be a 6-digit hexadecimal color code starting with `#`.
+ * @param {number} [weight=0.5] - The weight of color1 in the mix, should be a number between 0 and 1, where 0 represents 100% of color2, and 1 represents 100% of color1.
+ * @returns {string} The mixed color, a 6-digit hexadecimal color code starting with `#`.
+ */
+export const mix = (color1: string, color2: string, weight: number = 0.5): string => {
+  let color = '#'
+  for (let i = 0; i <= 2; i++) {
+    const c1 = parseInt(color1.substring(1 + i * 2, 3 + i * 2), 16)
+    const c2 = parseInt(color2.substring(1 + i * 2, 3 + i * 2), 16)
+    const c = Math.round(c1 * weight + c2 * (1 - weight))
+    color += c.toString(16).padStart(2, '0')
+  }
+  return color
+}
+
+/**
+ * getCssColorVariable
+ * @description 获取css变量的颜色值
+ * @param colorVariable css变量名
+ * @param opacity 透明度
+ * @returns {string} 颜色值
+ * @example getCssColorVariable('--el-color-primary', 0.5)
+ * @example getCssColorVariable('--el-color-primary')
+ * @example getCssColorVariable()
+ */
+export const getCssColorVariable = (
+  colorVariable: string = '--el-color-primary',
+  opacity?: number
+) => {
+  const colorValue = getComputedStyle(document.documentElement)
+    .getPropertyValue(colorVariable)
+    .trim()
+  if (colorValue) {
+    return opacity ? hexToRGB(colorValue, opacity) : colorValue
+  }
+  return ''
+}

+ 15 - 1
src/views/infra/codegen/components/ColumInfoForm.vue

@@ -95,7 +95,20 @@
     </el-table-column>
     <el-table-column label="字典类型" min-width="12%">
       <template #default="scope">
-        <el-select v-model="scope.row.dictType" clearable filterable placeholder="请选择">
+        <el-select v-model="scope.row.dictType" :value-on-clear="''" clearable filterable placeholder="请选择">
+          <template #header>
+            <div class="flex justify-end">
+              <el-popover
+                class="box-item"
+                content="加载最新字典"
+                placement="top-start"
+              >
+                <template #reference>
+                  <el-button :icon="Refresh" size="small" circle @click="getDictOptions" class=""/>
+                </template>
+              </el-popover>
+            </div>
+          </template>
           <el-option
             v-for="dict in dictOptions"
             :key="dict.id"
@@ -114,6 +127,7 @@
 </template>
 <script lang="ts" setup>
 import { PropType } from 'vue'
+import { Refresh } from '@element-plus/icons-vue'
 import * as CodegenApi from '@/api/infra/codegen'
 import * as DictDataApi from '@/api/system/dict/dict.type'
 

+ 18 - 2
src/views/system/user/DeptTree.vue

@@ -46,8 +46,24 @@ const filterNode = (name: string, data: Tree) => {
 }
 
 /** 处理部门被点击 */
-const handleNodeClick = async (row: { [key: string]: any }) => {
-  emits('node-click', row)
+let currentNode: any = {}
+const handleNodeClick = async (row: { [key: string]: any }, treeNode: any) => {
+  // 判断选中状态
+  if (currentNode && currentNode.name === row.name) {
+    treeNode.checked = !treeNode.checked
+  } else {
+    treeNode.checked = true
+  }
+  if (treeNode.checked) {
+    // 选中
+    currentNode = row
+    emits('node-click', row)
+  } else {
+    // 取消选中
+    treeRef.value!.setCurrentKey(undefined)
+    emits('node-click', undefined)
+    currentNode = null
+  }
 }
 const emits = defineEmits(['node-click'])
 

+ 8 - 3
src/views/system/user/index.vue

@@ -265,9 +265,14 @@ const resetQuery = () => {
 }
 
 /** 处理部门被点击 */
-const handleDeptNodeClick = async (row) => {
-  queryParams.deptId = row.id
-  await getList()
+const handleDeptNodeClick = async (row: any) => {
+  if (row === undefined) {
+    queryParams.deptId = undefined
+    await getList()
+  } else {
+    queryParams.deptId = row.id
+    await getList()
+  }
 }
 
 /** 添加/修改操作 */