#
zhou zhou
昨天 4259deb19122a4807d50c99ed4a95405ebe4a47c
#
4个文件已删除
1个文件已添加
25个文件已修改
2084 ■■■■■ 已修改文件
rsf-design/.env.development 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/api/flow-step-instance.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/base/art-copyright/index.vue 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/base/art-logo/index.vue 40 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/base/art-svg-icon/index.vue 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-fireworks-effect/index.vue 427 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-page-content/index.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/text-effect/art-festival-text-scroll/index.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/config/modules/component.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/config/modules/festival.js 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/config/setting.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/hooks/core/useCeremony.js 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/hooks/index.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/locales/langs/en.json 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/locales/langs/zh.json 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/core/RouteRegistry.js 57 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/guards/afterEach.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/guards/beforeEach.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/store/modules/setting.js 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/sys/public-project-config.js 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/ui/iconify-loader.js 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/manager/task/index.vue 151 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/manager/task/modules/task-detail-drawer.vue 199 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/manager/task/modules/task-expand-panel.vue 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/manager/task/modules/task-flow-step-dialog.vue 337 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/manager/task/taskPage.helpers.js 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/pom.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/config/MybatisPlusConfig.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/.env.development
@@ -13,4 +13,4 @@
VITE_DROP_CONSOLE = false
# 是否开启 Vue DevTools / Inspector(开启后会增加本地模块转换耗时)
VITE_ENABLE_VUE_DEVTOOLS = true
VITE_ENABLE_VUE_DEVTOOLS = false
rsf-design/src/api/flow-step-instance.js
@@ -57,6 +57,32 @@
  })
}
export function fetchSaveFlowStepInstance(params = {}) {
  return request.post({
    url: '/flowStepInstance/save',
    params
  })
}
export function fetchUpdateFlowStepInstance(params = {}) {
  return request.post({
    url: '/flowStepInstance/update',
    params
  })
}
export function fetchRemoveFlowStepInstance(id) {
  return request.post({
    url: `/flowStepInstance/remove/${normalizeIds(id)}`
  })
}
export function fetchJumpCurrentFlowStepInstance(id) {
  return request.post({
    url: `/flowStepInstance/jumpCurrent/${id}`
  })
}
export async function fetchExportFlowStepInstanceReport(payload = {}, options = {}) {
  return fetch(`${import.meta.env.VITE_API_URL}/flowStepInstance/export`, {
    method: 'POST',
rsf-design/src/components/core/base/art-copyright/index.vue
@@ -3,50 +3,30 @@
</template>
<script setup>
  import AppConfig from '@/config'
  import { fetchPublicProjectCopyrightConfig } from '@/api/system-manage'
  import {
    getDefaultProjectCopyright,
    loadPublicProjectCopyright,
    setPublicProjectCopyright
  } from '@/utils/sys/public-project-config'
  const PROJECT_COPYRIGHT_UPDATED_EVENT = 'project-copyright-updated'
  let cachedCopyrightText = ''
  let copyrightRequest = null
  defineOptions({ name: 'ArtCopyright' })
  const copyrightText = ref(cachedCopyrightText || getDefaultCopyright())
  function getDefaultCopyright() {
    return `版权所有 © ${AppConfig.systemInfo.name}`
  }
  const copyrightText = ref(getDefaultProjectCopyright())
  function normalizeCopyright(value) {
    const normalized = String(value || '').trim()
    return normalized || getDefaultCopyright()
    return normalized || getDefaultProjectCopyright()
  }
  async function loadProjectCopyright(force = false) {
    if (cachedCopyrightText && !force) {
      copyrightText.value = cachedCopyrightText
      return
    }
    if (!copyrightRequest || force) {
      copyrightRequest = fetchPublicProjectCopyrightConfig()
        .then((response) => normalizeCopyright(response?.val))
        .catch(() => getDefaultCopyright())
        .then((resolvedCopyright) => {
          cachedCopyrightText = resolvedCopyright
          return resolvedCopyright
        })
    }
    copyrightText.value = await copyrightRequest
    copyrightText.value = await loadPublicProjectCopyright(force)
  }
  function handleProjectCopyrightUpdated(event) {
    const nextCopyright = normalizeCopyright(event?.detail?.text)
    cachedCopyrightText = nextCopyright
    copyrightRequest = Promise.resolve(nextCopyright)
    copyrightText.value = nextCopyright
    copyrightText.value = setPublicProjectCopyright(nextCopyright)
  }
  onMounted(() => {
rsf-design/src/components/core/base/art-logo/index.vue
@@ -12,19 +12,20 @@
</template>
<script setup>
  import defaultLogo from '@imgs/common/logo.webp'
  import { fetchPublicProjectLogoConfig } from '@/api/system-manage'
  import {
    getDefaultProjectLogo,
    loadPublicProjectLogo,
    setPublicProjectLogo
  } from '@/utils/sys/public-project-config'
  const PROJECT_LOGO_UPDATED_EVENT = 'project-logo-updated'
  let cachedLogoSrc = ''
  let logoRequest = null
  defineOptions({ name: 'ArtLogo' })
  const props = defineProps({
    size: { required: false, default: 36 },
    fill: { type: Boolean, default: false }
  })
  const logoSrc = ref(cachedLogoSrc || defaultLogo)
  const logoSrc = ref(getDefaultProjectLogo())
  const wrapperStyle = computed(() => (props.fill ? { width: '100%', height: '100%' } : {}))
  const logoStyle = computed(() => {
    if (props.fill) {
@@ -42,43 +43,24 @@
  function normalizeLogoSrc(value) {
    const normalized = String(value || '').trim()
    return normalized || defaultLogo
    return normalized || getDefaultProjectLogo()
  }
  function applyDefaultLogo() {
    cachedLogoSrc = defaultLogo
    logoRequest = Promise.resolve(defaultLogo)
    logoSrc.value = defaultLogo
    logoSrc.value = setPublicProjectLogo(getDefaultProjectLogo())
  }
  async function loadProjectLogo(force = false) {
    if (cachedLogoSrc && !force) {
      logoSrc.value = cachedLogoSrc
      return
    }
    if (!logoRequest || force) {
      logoRequest = fetchPublicProjectLogoConfig()
        .then((response) => normalizeLogoSrc(response?.val))
        .catch(() => defaultLogo)
        .then((resolvedLogo) => {
          cachedLogoSrc = resolvedLogo
          return resolvedLogo
        })
    }
    logoSrc.value = await logoRequest
    logoSrc.value = await loadPublicProjectLogo(force)
  }
  function handleProjectLogoUpdated(event) {
    const nextLogoSrc = normalizeLogoSrc(event?.detail?.url)
    cachedLogoSrc = nextLogoSrc
    logoRequest = Promise.resolve(nextLogoSrc)
    logoSrc.value = nextLogoSrc
    logoSrc.value = setPublicProjectLogo(nextLogoSrc)
  }
  function handleLogoError() {
    if (logoSrc.value === defaultLogo) {
    if (logoSrc.value === getDefaultProjectLogo()) {
      return
    }
    applyDefaultLogo()
rsf-design/src/components/core/base/art-svg-icon/index.vue
@@ -1,20 +1,65 @@
<!-- 图标组件 -->
<template>
  <span v-if="icon" v-bind="containerAttrs">
    <Icon :icon="icon" />
  <span v-if="resolvedIcon" v-bind="containerAttrs">
    <Icon :key="renderKey" :icon="resolvedIcon" />
  </span>
</template>
<script setup>
  import { Icon } from '@iconify/vue/offline'
  import { ensureIconRegistered } from '@/utils/ui/iconify-loader'
  defineOptions({ name: 'ArtSvgIcon', inheritAttrs: false })
  defineProps({
  const props = defineProps({
    icon: { required: false }
  })
  const attrs = useAttrs()
  const resolvedIcon = ref('')
  const renderKey = ref(0)
  const containerAttrs = computed(() => ({
    ...attrs,
    class: ['art-svg-icon inline-flex shrink-0', attrs.class].filter(Boolean).join(' '),
    style: attrs.style
  }))
  let latestTaskId = 0
  watch(
    () => props.icon,
    async (icon) => {
      const taskId = ++latestTaskId
      if (!icon) {
        resolvedIcon.value = ''
        renderKey.value += 1
        return
      }
      if (typeof icon !== 'string') {
        resolvedIcon.value = icon
        renderKey.value += 1
        return
      }
      const normalizedIcon = icon.trim()
      if (!normalizedIcon) {
        resolvedIcon.value = ''
        renderKey.value += 1
        return
      }
      try {
        await ensureIconRegistered(normalizedIcon)
      } catch (error) {
        console.warn(`[ArtSvgIcon] Failed to register icon "${normalizedIcon}"`, error)
      }
      if (taskId !== latestTaskId) {
        return
      }
      resolvedIcon.value = normalizedIcon
      renderKey.value += 1
    },
    { immediate: true }
  )
</script>
rsf-design/src/components/core/layouts/art-fireworks-effect/index.vue
File was deleted
rsf-design/src/components/core/layouts/art-page-content/index.vue
@@ -2,9 +2,6 @@
<template>
  <div class="layout-content" :class="{ 'overflow-auto': isFullPage }" :style="containerStyle">
    <div id="app-content-header">
      <!-- 节日滚动 -->
      <ArtFestivalTextScroll v-if="!isFullPage" />
      <!-- 路由信息调试 -->
      <div
        v-if="isOpenRouteInfo === 'true'"
@@ -27,7 +24,12 @@
    <RouterView v-else-if="isRefresh" v-slot="{ Component, route }">
      <!-- 缓存路由动画 -->
      <Transition :name="showTransitionMask ? '' : actualTransition" mode="out-in" appear>
        <div v-if="route.meta.keepAlive" :key="route.path" class="art-page-view" :style="contentStyle">
        <div
          v-if="route.meta.keepAlive"
          :key="route.path"
          class="art-page-view"
          :style="contentStyle"
        >
          <KeepAlive :max="10" :exclude="keepAliveExclude">
            <component :is="Component" :key="route.path" />
          </KeepAlive>
@@ -36,7 +38,12 @@
      <!-- 非缓存路由动画 -->
      <Transition :name="showTransitionMask ? '' : actualTransition" mode="out-in" appear>
        <div v-if="!route.meta.keepAlive" :key="route.path" class="art-page-view" :style="contentStyle">
        <div
          v-if="!route.meta.keepAlive"
          :key="route.path"
          class="art-page-view"
          :style="contentStyle"
        >
          <component :is="Component" :key="route.path" />
        </div>
      </Transition>
@@ -163,5 +170,4 @@
    line-height: 1.7;
    word-break: break-word;
  }
</style>
rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.js
@@ -7,13 +7,11 @@
import { mittBus } from '@/utils/sys'
import { StorageConfig } from '@/utils'
import { useTheme } from '@/hooks/core/useTheme'
import { useCeremony } from '@/hooks/core/useCeremony'
import { useSettingsState } from './useSettingsState'
import { useSettingsHandlers } from './useSettingsHandlers'
function useSettingsPanel() {
  const settingStore = useSettingStore()
  const { systemThemeType, systemThemeMode, menuType } = storeToRefs(settingStore)
  const { openFestival, cleanup } = useCeremony()
  const { setSystemTheme, setSystemAutoTheme } = useTheme()
  const { initColorWeak } = useSettingsState()
  const { domOperations } = useSettingsHandlers()
@@ -144,12 +142,10 @@
      const boxMode = settingStore.boxBorderMode ? 'border-mode' : 'shadow-mode'
      domOperations.setRootAttribute('data-box-mode', boxMode)
      themeHandlers.initSystemTheme()
      openFestival()
    }
    const cleanupSettings = () => {
      stopWatch()
      themeCleanup?.()
      cleanup()
    }
    return {
      initializeSettings,
rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue
@@ -64,19 +64,16 @@
    { comment: '是否显示语言切换', key: 'showLanguage' },
    { comment: '是否显示进度条', key: 'showNprogress' },
    { comment: '是否显示设置引导', key: 'showSettingGuide' },
    { comment: '是否显示节日文本', key: 'showFestivalText' },
    { comment: '是否显示水印', key: 'watermarkVisible' },
    { comment: '是否自动关闭', key: 'autoClose' },
    { comment: '是否唯一展开', key: 'uniqueOpened' },
    { comment: '是否色弱模式', key: 'colorWeak' },
    { comment: '是否刷新', key: 'refresh' },
    { comment: '是否加载节日烟花', key: 'holidayFireworksLoaded' },
    { comment: '边框模式', key: 'boxBorderMode' },
    { comment: '页面过渡效果', key: 'pageTransition' },
    { comment: '标签页样式', key: 'tabStyle' },
    { comment: '自定义圆角', key: 'customRadius' },
    { comment: '容器宽度', key: 'containerWidth', enumMap: ENUM_MAPS.containerWidth },
    { comment: '节日日期', key: 'festivalDate', forceValue: '' }
    { comment: '容器宽度', key: 'containerWidth', enumMap: ENUM_MAPS.containerWidth }
  ]
  const valueToCode = (value, enumMap) => {
    if (value === null) return 'null'
@@ -147,7 +144,6 @@
        settingStore.setNprogress()
      )
      settingStore.setWorkTab(config.showWorkTab)
      settingStore.setShowFestivalText(config.showFestivalText)
      settingStore.setWatermarkVisible(config.watermarkVisible)
      toggleIfDifferent(settingStore.autoClose, config.autoClose, () => settingStore.setAutoClose())
      toggleIfDifferent(settingStore.uniqueOpened, config.uniqueOpened, () =>
@@ -161,8 +157,6 @@
      settingStore.setTabStyle(config.tabStyle)
      settingStore.setCustomRadius(config.customRadius)
      settingStore.setContainerWidth(config.containerWidth)
      settingStore.setFestivalDate(config.festivalDate)
      settingStore.setholidayFireworksLoaded(config.holidayFireworksLoaded)
      location.reload()
    } catch (error) {
      console.error('重置配置失败:', error)
rsf-design/src/components/core/text-effect/art-festival-text-scroll/index.vue
File was deleted
rsf-design/src/config/modules/component.js
@@ -33,14 +33,6 @@
    enabled: true
  },
  {
    name: '礼花效果',
    key: 'fireworks-effect',
    component: defineAsyncComponent(
      () => import('@/components/core/layouts/art-fireworks-effect/index.vue')
    ),
    enabled: true
  },
  {
    name: '水印效果',
    key: 'watermark',
    component: defineAsyncComponent(
rsf-design/src/config/modules/festival.js
File was deleted
rsf-design/src/config/setting.js
@@ -33,8 +33,6 @@
  showNprogress: false,
  /** 是否显示设置引导 */
  showSettingGuide: true,
  /** 是否显示节日文本 */
  showFestivalText: false,
  /** 是否显示水印 */
  watermarkVisible: false,
  /** 是否自动关闭 */
@@ -45,8 +43,6 @@
  colorWeak: false,
  /** 是否刷新 */
  refresh: false,
  /** 是否加载节日烟花 */
  holidayFireworksLoaded: false,
  /** 边框模式 */
  boxBorderMode: true,
  /** 页面过渡效果 */
@@ -56,9 +52,7 @@
  /** 自定义圆角 */
  customRadius: '0.75',
  /** 容器宽度 */
  containerWidth: ContainerWidthEnum.FULL,
  /** 节日日期 */
  festivalDate: ''
  containerWidth: ContainerWidthEnum.FULL
}
function getSettingDefaults() {
  return { ...SETTING_DEFAULT_CONFIG }
rsf-design/src/hooks/core/useCeremony.js
File was deleted
rsf-design/src/hooks/index.js
@@ -4,7 +4,6 @@
import { useTable } from './core/useTable'
import { useTableColumns } from './core/useTableColumns'
import { useTheme } from './core/useTheme'
import { useCeremony } from './core/useCeremony'
import { useFastEnter } from './core/useFastEnter'
import { useHeaderBar } from './core/useHeaderBar'
import { useChart, useChartComponent, useChartOps } from './core/useChart'
@@ -13,7 +12,6 @@
  useAppMode,
  useAuth,
  useAutoLayoutHeight,
  useCeremony,
  useChart,
  useChartComponent,
  useChartOps,
rsf-design/src/locales/langs/en.json
@@ -2123,8 +2123,10 @@
      "detail": {
        "title": "Task Detail",
        "taskCode": "Task No.",
        "taskId": "Task ID",
        "baseInfo": "Basic Information",
        "pathInfo": "Execution Path",
        "executionInfo": "Execution Information",
        "items": "Task Items",
        "itemsHint": "View related orders, materials, and execution records of the current task",
        "flowStep": "Flow Steps",
@@ -2133,8 +2135,15 @@
        "warehType": "Device Type",
        "priority": "Priority",
        "status": "Status",
        "exceStatus": "Execution Status",
        "expDesc": "Exception Description",
        "expCode": "Exception Code",
        "startTime": "Start Time",
        "endTime": "End Time",
        "robotCode": "Robot Code",
        "createBy": "Created By",
        "createTime": "Created At",
        "updateBy": "Updated By",
        "updateTime": "Updated At",
        "memo": "Remark",
        "orgLoc": "Source Location",
@@ -2148,12 +2157,22 @@
        "empty": "No task items",
        "orderType": "Order Type",
        "wkType": "Business Type",
        "platOrderCode": "Customer Order No.",
        "platWorkCode": "Work Order No.",
        "platItemId": "Line No.",
        "anfme": "Quantity"
        "projectCode": "Project Code",
        "anfme": "Quantity",
        "workQty": "Executed Qty",
        "qty": "Completed Qty",
        "spec": "Specification",
        "model": "Model"
      },
      "flowStepDialog": {
        "title": "Flow Steps",
        "create": "Add Step",
        "createTitle": "Create Flow Step",
        "editTitle": "Edit Flow Step",
        "jumpCurrent": "Jump Current",
        "currentTask": "Current Task",
        "flowInstanceNo": "Flow Instance No.",
        "stepCode": "Step Code",
@@ -2162,9 +2181,46 @@
        "executeResult": "Execution Result",
        "startTime": "Start Time",
        "endTime": "End Time",
        "timeout": "Flow steps timed out and waiting has stopped"
        "timeout": "Flow steps timed out and waiting has stopped",
        "form": {
          "taskNo": "Task No.",
          "stepOrder": "Step Order",
          "stepCode": "Step Code",
          "stepName": "Step Name",
          "stepType": "Step Type",
          "status": "Status",
          "executeResult": "Execution Result",
          "executeResultPlaceholder": "Enter execution result"
        },
        "statusOptions": {
          "0": "Queued",
          "1": "Pending",
          "2": "Running",
          "3": "Succeeded",
          "4": "Failed",
          "5": "Skipped",
          "6": "Canceled"
        },
        "validation": {
          "stepOrder": "Enter step order",
          "stepCode": "Enter step code",
          "stepName": "Enter step name",
          "stepType": "Enter step type",
          "status": "Select status"
        },
        "messages": {
          "createSuccess": "Flow step created successfully",
          "updateSuccess": "Flow step updated successfully",
          "submitFailed": "Failed to submit flow step",
          "deleteConfirm": "Are you sure you want to delete flow step {code}?",
          "deleteSuccess": "Flow step deleted successfully",
          "deleteFailed": "Failed to delete flow step",
          "jumpSuccess": "Jumped to current step successfully",
          "jumpFailed": "Failed to jump to current step"
        }
      },
      "messages": {
        "detailTimeout": "Task detail timed out and waiting has stopped",
        "completeConfirm": "Are you sure you want to complete task {code}?",
        "completeSuccess": "Task completed successfully",
        "removeConfirm": "Are you sure you want to cancel task {code}?",
rsf-design/src/locales/langs/zh.json
@@ -2131,8 +2131,10 @@
      "detail": {
        "title": "任务详情",
        "taskCode": "任务号",
        "taskId": "任务ID",
        "baseInfo": "任务基础信息",
        "pathInfo": "执行路径",
        "executionInfo": "执行信息",
        "items": "任务明细",
        "itemsHint": "查看当前任务关联的业务单据、物料和执行记录",
        "flowStep": "流程步骤",
@@ -2141,8 +2143,15 @@
        "warehType": "设备类型",
        "priority": "优先级",
        "status": "状态",
        "exceStatus": "执行状态",
        "expDesc": "异常描述",
        "expCode": "异常编码",
        "startTime": "开始时间",
        "endTime": "结束时间",
        "robotCode": "机器人编码",
        "createBy": "创建人",
        "createTime": "创建时间",
        "updateBy": "更新人",
        "updateTime": "更新时间",
        "memo": "备注",
        "orgLoc": "源库位",
@@ -2156,12 +2165,22 @@
        "empty": "暂无任务明细",
        "orderType": "单据类型",
        "wkType": "业务类型",
        "platOrderCode": "客户订单号",
        "platWorkCode": "工单号",
        "platItemId": "行号",
        "anfme": "数量"
        "projectCode": "项目号",
        "anfme": "数量",
        "workQty": "执行数量",
        "qty": "完成数量",
        "spec": "规格",
        "model": "型号"
      },
      "flowStepDialog": {
        "title": "流程步骤",
        "create": "新增步骤",
        "createTitle": "新增流程步骤",
        "editTitle": "编辑流程步骤",
        "jumpCurrent": "跳转当前",
        "currentTask": "当前任务",
        "flowInstanceNo": "流程实例号",
        "stepCode": "步骤编码",
@@ -2170,9 +2189,46 @@
        "executeResult": "执行结果",
        "startTime": "开始时间",
        "endTime": "结束时间",
        "timeout": "流程步骤加载超时,已停止等待"
        "timeout": "流程步骤加载超时,已停止等待",
        "form": {
          "taskNo": "任务号",
          "stepOrder": "步骤顺序",
          "stepCode": "步骤编码",
          "stepName": "步骤名称",
          "stepType": "步骤类型",
          "status": "状态",
          "executeResult": "执行结果",
          "executeResultPlaceholder": "请输入执行结果"
        },
        "statusOptions": {
          "0": "排队中",
          "1": "待执行",
          "2": "执行中",
          "3": "执行成功",
          "4": "执行失败",
          "5": "已跳过",
          "6": "已取消"
        },
        "validation": {
          "stepOrder": "请输入步骤顺序",
          "stepCode": "请输入步骤编码",
          "stepName": "请输入步骤名称",
          "stepType": "请输入步骤类型",
          "status": "请选择状态"
        },
        "messages": {
          "createSuccess": "流程步骤新增成功",
          "updateSuccess": "流程步骤修改成功",
          "submitFailed": "流程步骤提交失败",
          "deleteConfirm": "确定删除流程步骤 {code} 吗?",
          "deleteSuccess": "流程步骤删除成功",
          "deleteFailed": "流程步骤删除失败",
          "jumpSuccess": "已跳转到当前步骤",
          "jumpFailed": "跳转当前步骤失败"
        }
      },
      "messages": {
        "detailTimeout": "任务详情加载超时,已停止等待",
        "completeConfirm": "确定完成任务 {code} 吗?",
        "completeSuccess": "任务完成成功",
        "removeConfirm": "确定取消任务 {code} 吗?",
rsf-design/src/router/core/RouteRegistry.js
@@ -2,6 +2,9 @@
import { RouteValidator } from './RouteValidator.js'
import { RouteTransformer } from './RouteTransformer.js'
const DEFAULT_WARMUP_LIMIT = 12
const DEFAULT_WARMUP_BATCH_SIZE = 6
const DEFAULT_WARMUP_BATCH_INTERVAL = 160
const DEFAULT_WARMUP_IDLE_TIMEOUT = 1200
const HOME_COMPONENT_PATH = '/dashboard/console'
class RouteRegistry {
  constructor(router, options = {}) {
@@ -86,15 +89,21 @@
   */
  warm(menuList, options = {}) {
    const limit = Number.isFinite(options.limit) ? options.limit : DEFAULT_WARMUP_LIMIT
    const batchSize = Number.isFinite(options.batchSize)
      ? Math.max(1, Math.floor(options.batchSize))
      : DEFAULT_WARMUP_BATCH_SIZE
    const batchInterval = Number.isFinite(options.batchInterval)
      ? Math.max(0, options.batchInterval)
      : DEFAULT_WARMUP_BATCH_INTERVAL
    const paths = collectWarmupPaths(menuList, limit)
    if (paths.length === 0) {
      return
    }
    const schedule = globalThis.requestIdleCallback
      ? (task) => globalThis.requestIdleCallback(task, { timeout: 1200 })
      : (task) => setTimeout(task, 80)
    schedule(() => {
      void warmSequentially(paths, this.componentLoader)
    scheduleWarmupTask(() => {
      void warmInBatches(paths, this.componentLoader, {
        batchSize,
        batchInterval
      })
    })
  }
}
@@ -128,9 +137,41 @@
  walk(menuList)
  return paths
}
async function warmSequentially(paths, componentLoader) {
  for (const componentPath of paths) {
    await componentLoader.warm(componentPath)
function scheduleWarmupTask(task, delay = 0) {
  const invoke = () => {
    if (globalThis.requestIdleCallback) {
      globalThis.requestIdleCallback(task, { timeout: DEFAULT_WARMUP_IDLE_TIMEOUT })
      return
    }
    setTimeout(task, 0)
  }
  if (delay > 0) {
    setTimeout(invoke, delay)
    return
  }
  invoke()
}
function waitForNextWarmupBatch(delay = 0) {
  return new Promise((resolve) => {
    scheduleWarmupTask(resolve, delay)
  })
}
async function warmInBatches(paths, componentLoader, options = {}) {
  const batchSize = Number.isFinite(options.batchSize)
    ? Math.max(1, Math.floor(options.batchSize))
    : DEFAULT_WARMUP_BATCH_SIZE
  const batchInterval = Number.isFinite(options.batchInterval)
    ? Math.max(0, options.batchInterval)
    : DEFAULT_WARMUP_BATCH_INTERVAL
  for (let start = 0; start < paths.length; start += batchSize) {
    const currentBatch = paths.slice(start, start + batchSize)
    await Promise.allSettled(
      currentBatch.map((componentPath) => componentLoader.warm(componentPath))
    )
    if (start + batchSize < paths.length) {
      await waitForNextWarmupBatch(batchInterval)
    }
  }
}
export { RouteRegistry }
rsf-design/src/router/guards/afterEach.js
@@ -3,10 +3,10 @@
import NProgress from 'nprogress'
import { useCommon } from '@/hooks/core/useCommon'
import { loadingService } from '@/utils/ui'
import { getPendingLoading, resetPendingLoading } from './beforeEach'
import { getPendingLoading, resetPendingLoading, triggerHomeRouteWarmup } from './beforeEach'
function setupAfterEachGuard(router) {
  const { scrollToTop } = useCommon()
  router.afterEach(() => {
  const { scrollToTop, homePath } = useCommon()
  router.afterEach((to) => {
    scrollToTop()
    const settingStore = useSettingStore()
    if (settingStore.showNprogress) {
@@ -21,6 +21,9 @@
        resetPendingLoading()
      })
    }
    nextTick(() => {
      triggerHomeRouteWarmup(to.path, homePath.value || '/')
    })
  })
}
export { setupAfterEachGuard }
rsf-design/src/router/guards/beforeEach.js
@@ -8,6 +8,7 @@
import { RoutesAlias } from '../routesAlias'
import { staticRoutes } from '../routes/staticRoutes'
import { loadingService } from '@/utils/ui'
import { warmMenuIcons } from '@/utils/ui/iconify-loader'
import { useCommon } from '@/hooks/core/useCommon'
import { useWorktabStore } from '@/store/modules/worktab'
import { fetchGetUserInfo, normalizeUserInfo } from '@/api/auth'
@@ -20,6 +21,8 @@
let routeInitFailed = false
let routeInitInProgress = false
let pendingRouteLocation = null
let pendingWarmupMenuList = null
let homeWarmupTriggered = false
function getPendingLoading() {
  return pendingLoading
}
@@ -33,6 +36,8 @@
  routeInitFailed = false
  routeInitInProgress = false
  pendingRouteLocation = null
  pendingWarmupMenuList = null
  homeWarmupTriggered = false
}
function createRouteLocation(route) {
  return {
@@ -196,10 +201,12 @@
      throw new Error('获取菜单列表失败,请重新登录')
    }
    routeRegistry?.register(menuList)
    routeRegistry?.warm(menuList)
    pendingWarmupMenuList = menuList
    homeWarmupTriggered = false
    const menuStore = useMenuStore()
    menuStore.setMenuList(menuList)
    menuStore.addRemoveRouteFns(routeRegistry?.getRemoveRouteFns() || [])
    warmMenuIcons(menuList)
    IframeRouteManager.getInstance().save()
    useWorktabStore().validateWorktabs(router)
    const initialTargetLocation = createRouteLocation(to)
@@ -295,11 +302,29 @@
function isUnauthorizedError(error) {
  return isHttpError(error) && error.code === ApiStatus.unauthorized
}
function triggerHomeRouteWarmup(currentPath, homePath) {
  if (homeWarmupTriggered || !routeRegistry || !pendingWarmupMenuList?.length) {
    return
  }
  if (!currentPath || !homePath || currentPath !== homePath) {
    return
  }
  homeWarmupTriggered = true
  const schedule = globalThis.requestAnimationFrame
    ? (task) => globalThis.requestAnimationFrame(() => globalThis.requestAnimationFrame(task))
    : (task) => setTimeout(task, 120)
  schedule(() => {
    routeRegistry?.warm(pendingWarmupMenuList, {
      limit: Number.MAX_SAFE_INTEGER
    })
  })
}
export {
  getPendingLoading,
  getRouteInitFailed,
  resetPendingLoading,
  resetRouteInitState,
  resetRouterState,
  setupBeforeEachGuard
  setupBeforeEachGuard,
  triggerHomeRouteWarmup
}
rsf-design/src/store/modules/setting.js
@@ -3,7 +3,6 @@
import AppConfig from '@/config'
import { SystemThemeEnum } from '@/enums/appEnum'
import { setElementThemeColor } from '@/utils/ui'
import { useCeremony } from '@/hooks/core/useCeremony'
import { StorageConfig } from '@/utils'
import { SETTING_DEFAULT_CONFIG } from '@/config/setting'
const useSettingStore = defineStore(
@@ -25,19 +24,16 @@
    const showLanguage = ref(SETTING_DEFAULT_CONFIG.showLanguage)
    const showNprogress = ref(SETTING_DEFAULT_CONFIG.showNprogress)
    const showSettingGuide = ref(SETTING_DEFAULT_CONFIG.showSettingGuide)
    const showFestivalText = ref(SETTING_DEFAULT_CONFIG.showFestivalText)
    const watermarkVisible = ref(SETTING_DEFAULT_CONFIG.watermarkVisible)
    const autoClose = ref(SETTING_DEFAULT_CONFIG.autoClose)
    const uniqueOpened = ref(SETTING_DEFAULT_CONFIG.uniqueOpened)
    const colorWeak = ref(SETTING_DEFAULT_CONFIG.colorWeak)
    const refresh = ref(SETTING_DEFAULT_CONFIG.refresh)
    const holidayFireworksLoaded = ref(SETTING_DEFAULT_CONFIG.holidayFireworksLoaded)
    const boxBorderMode = ref(SETTING_DEFAULT_CONFIG.boxBorderMode)
    const pageTransition = ref(SETTING_DEFAULT_CONFIG.pageTransition)
    const tabStyle = ref(SETTING_DEFAULT_CONFIG.tabStyle)
    const customRadius = ref(SETTING_DEFAULT_CONFIG.customRadius)
    const containerWidth = ref(SETTING_DEFAULT_CONFIG.containerWidth)
    const festivalDate = ref('')
    const getMenuTheme = computed(() => {
      const list = AppConfig.themeList.filter((item) => item.theme === menuThemeType.value)
      if (isDark.value) {
@@ -54,9 +50,6 @@
    })
    const getCustomRadius = computed(() => {
      return customRadius.value + 'rem' || SETTING_DEFAULT_CONFIG.customRadius + 'rem'
    })
    const isShowFireworks = computed(() => {
      return festivalDate.value === useCeremony().currentFestivalData.value?.date ? false : true
    })
    const switchMenuLayouts = (type) => {
      menuType.value = type
@@ -137,15 +130,6 @@
      customRadius.value = radius
      document.documentElement.style.setProperty('--custom-radius', `${radius}rem`)
    }
    const setholidayFireworksLoaded = (isLoad) => {
      holidayFireworksLoaded.value = isLoad
    }
    const setShowFestivalText = (show) => {
      showFestivalText.value = show
    }
    const setFestivalDate = (date) => {
      festivalDate.value = date
    }
    const setDualMenuShowText = (show) => {
      dualMenuShowText.value = show
    }
@@ -174,16 +158,12 @@
      refresh,
      watermarkVisible,
      customRadius,
      holidayFireworksLoaded,
      showFestivalText,
      festivalDate,
      dualMenuShowText,
      containerWidth,
      getMenuTheme,
      isDark,
      getMenuOpenWidth,
      getCustomRadius,
      isShowFireworks,
      switchMenuLayouts,
      setMenuOpenWidth,
      setGlopTheme,
@@ -209,9 +189,6 @@
      reload,
      setWatermarkVisible,
      setCustomRadius,
      setholidayFireworksLoaded,
      setShowFestivalText,
      setFestivalDate,
      setDualMenuShowText
    }
  },
rsf-design/src/utils/sys/public-project-config.js
New file
@@ -0,0 +1,90 @@
import AppConfig from '@/config'
import defaultLogo from '@imgs/common/logo.webp'
import {
  fetchPublicProjectCopyrightConfig,
  fetchPublicProjectLogoConfig
} from '@/api/system-manage'
const publicProjectState = {
  logoSrc: '',
  logoRequest: null,
  copyrightText: '',
  copyrightRequest: null
}
function getDefaultProjectLogo() {
  return defaultLogo
}
function getDefaultProjectCopyright() {
  return `版权所有 © ${AppConfig.systemInfo.name}`
}
function normalizeProjectLogo(value) {
  const normalized = String(value || '').trim()
  return normalized || getDefaultProjectLogo()
}
function normalizeProjectCopyright(value) {
  const normalized = String(value || '').trim()
  return normalized || getDefaultProjectCopyright()
}
function setPublicProjectLogo(value) {
  const nextLogoSrc = normalizeProjectLogo(value)
  publicProjectState.logoSrc = nextLogoSrc
  publicProjectState.logoRequest = Promise.resolve(nextLogoSrc)
  return nextLogoSrc
}
function setPublicProjectCopyright(value) {
  const nextCopyright = normalizeProjectCopyright(value)
  publicProjectState.copyrightText = nextCopyright
  publicProjectState.copyrightRequest = Promise.resolve(nextCopyright)
  return nextCopyright
}
function loadPublicProjectLogo(force = false) {
  if (publicProjectState.logoSrc && !force) {
    return Promise.resolve(publicProjectState.logoSrc)
  }
  if (!publicProjectState.logoRequest || force) {
    publicProjectState.logoRequest = fetchPublicProjectLogoConfig()
      .then((response) => normalizeProjectLogo(response?.val))
      .catch(() => getDefaultProjectLogo())
      .then((resolvedLogo) => {
        publicProjectState.logoSrc = resolvedLogo
        return resolvedLogo
      })
  }
  return publicProjectState.logoRequest
}
function loadPublicProjectCopyright(force = false) {
  if (publicProjectState.copyrightText && !force) {
    return Promise.resolve(publicProjectState.copyrightText)
  }
  if (!publicProjectState.copyrightRequest || force) {
    publicProjectState.copyrightRequest = fetchPublicProjectCopyrightConfig()
      .then((response) => normalizeProjectCopyright(response?.val))
      .catch(() => getDefaultProjectCopyright())
      .then((resolvedCopyright) => {
        publicProjectState.copyrightText = resolvedCopyright
        return resolvedCopyright
      })
  }
  return publicProjectState.copyrightRequest
}
export {
  getDefaultProjectCopyright,
  getDefaultProjectLogo,
  loadPublicProjectCopyright,
  loadPublicProjectLogo,
  setPublicProjectCopyright,
  setPublicProjectLogo
}
rsf-design/src/utils/ui/iconify-loader.js
@@ -0,0 +1,163 @@
import { addCollection } from '@iconify/vue/offline'
import { LOCAL_ICON_COLLECTIONS } from '../../plugins/iconify.collections.js'
const FULL_ICON_COLLECTION_LOADERS = Object.freeze({
  fluent: () => import('@iconify-json/fluent').then((module) => module.icons),
  'icon-park-outline': () =>
    import('@iconify-json/icon-park-outline').then((module) => module.icons),
  iconamoon: () => import('@iconify-json/iconamoon').then((module) => module.icons),
  ix: () => import('@iconify-json/ix').then((module) => module.icons),
  'line-md': () => import('@iconify-json/line-md').then((module) => module.icons),
  ri: () => import('@iconify-json/ri').then((module) => module.icons),
  solar: () => import('@iconify-json/solar').then((module) => module.icons),
  'svg-spinners': () => import('@iconify-json/svg-spinners').then((module) => module.icons),
  'system-uicons': () => import('@iconify-json/system-uicons').then((module) => module.icons),
  vaadin: () => import('@iconify-json/vaadin').then((module) => module.icons)
})
const fullyRegisteredPrefixes = new Set()
const pendingPrefixLoads = new Map()
function parseIconName(icon) {
  if (typeof icon !== 'string') {
    return null
  }
  const normalizedIcon = icon.trim()
  if (!normalizedIcon) {
    return null
  }
  const separatorIndex = normalizedIcon.indexOf(':')
  if (separatorIndex <= 0 || separatorIndex >= normalizedIcon.length - 1) {
    return null
  }
  return {
    prefix: normalizedIcon.slice(0, separatorIndex),
    name: normalizedIcon.slice(separatorIndex + 1)
  }
}
function hasBundledIcon(icon) {
  const parsedIcon = parseIconName(icon)
  if (!parsedIcon) {
    return false
  }
  const collection = LOCAL_ICON_COLLECTIONS[parsedIcon.prefix]
  if (!collection) {
    return false
  }
  return Boolean(collection.icons?.[parsedIcon.name] || collection.aliases?.[parsedIcon.name])
}
function loadFullIconCollection(prefix) {
  if (fullyRegisteredPrefixes.has(prefix)) {
    return Promise.resolve(true)
  }
  const currentTask = pendingPrefixLoads.get(prefix)
  if (currentTask) {
    return currentTask
  }
  const loader = FULL_ICON_COLLECTION_LOADERS[prefix]
  if (!loader) {
    return Promise.resolve(false)
  }
  const loadTask = loader()
    .then((collection) => {
      addCollection(collection)
      fullyRegisteredPrefixes.add(prefix)
      pendingPrefixLoads.delete(prefix)
      return true
    })
    .catch((error) => {
      pendingPrefixLoads.delete(prefix)
      throw error
    })
  pendingPrefixLoads.set(prefix, loadTask)
  return loadTask
}
function collectRuntimeIcons(source, iconNames = new Set()) {
  if (!Array.isArray(source)) {
    return iconNames
  }
  source.forEach((item) => {
    if (!item || typeof item !== 'object') {
      return
    }
    const icon = item.meta?.icon || item.icon
    if (typeof icon === 'string' && icon.includes(':') && !hasBundledIcon(icon)) {
      iconNames.add(icon)
    }
    if (Array.isArray(item.children) && item.children.length > 0) {
      collectRuntimeIcons(item.children, iconNames)
    }
  })
  return iconNames
}
function scheduleIdleTask(task, delay = 0) {
  const invoke = () => {
    if (globalThis.requestIdleCallback) {
      globalThis.requestIdleCallback(task, { timeout: 1000 })
      return
    }
    setTimeout(task, 0)
  }
  if (delay > 0) {
    setTimeout(invoke, delay)
    return
  }
  invoke()
}
async function ensureIconRegistered(icon) {
  const parsedIcon = parseIconName(icon)
  if (!parsedIcon) {
    return false
  }
  if (hasBundledIcon(icon) || fullyRegisteredPrefixes.has(parsedIcon.prefix)) {
    return true
  }
  return loadFullIconCollection(parsedIcon.prefix)
}
function warmRuntimeIcons(iconNames, delay = 120) {
  const icons = [...new Set(Array.isArray(iconNames) ? iconNames : [])].filter(Boolean)
  if (icons.length === 0) {
    return
  }
  scheduleIdleTask(() => {
    icons.forEach((icon) => {
      void ensureIconRegistered(icon)
    })
  }, delay)
}
function warmMenuIcons(menuList, delay = 120) {
  warmRuntimeIcons([...collectRuntimeIcons(menuList)], delay)
}
export {
  collectRuntimeIcons,
  ensureIconRegistered,
  hasBundledIcon,
  warmMenuIcons,
  warmRuntimeIcons
}
rsf-design/src/views/manager/task/index.vue
@@ -44,10 +44,7 @@
      />
    </ElCard>
    <TaskFlowStepDialog
      v-model:visible="flowStepDialogVisible"
      :task-row="activeTaskRow"
    />
    <TaskFlowStepDialog v-model:visible="flowStepDialogVisible" :task-row="activeTaskRow" />
    <TaskDetailDrawer
      v-model:visible="detailDrawerVisible"
@@ -76,6 +73,7 @@
    fetchPickTask,
    fetchRemoveTask,
    fetchTaskAutoRunFlag,
    fetchTaskDetail,
    fetchTaskItemPage,
    fetchTaskPage,
    fetchTopTask,
@@ -174,6 +172,12 @@
      showOverflowTooltip: true
    },
    {
      prop: 'platOrderCode',
      label: t('pages.task.expand.platOrderCode'),
      minWidth: 150,
      showOverflowTooltip: true
    },
    {
      prop: 'platWorkCode',
      label: t('pages.orders.transfer.detail.relatedCode'),
      minWidth: 150,
@@ -183,6 +187,12 @@
      prop: 'platItemId',
      label: t('pages.orders.delivery.table.platItemId'),
      minWidth: 100,
      showOverflowTooltip: true
    },
    {
      prop: 'projectCode',
      label: t('pages.task.expand.projectCode'),
      minWidth: 140,
      showOverflowTooltip: true
    },
    {
@@ -215,9 +225,57 @@
      align: 'right'
    },
    {
      prop: 'workQty',
      label: t('pages.task.expand.workQty'),
      width: 100,
      align: 'right'
    },
    {
      prop: 'qty',
      label: t('pages.task.expand.qty'),
      width: 100,
      align: 'right'
    },
    {
      prop: 'spec',
      label: t('pages.task.expand.spec'),
      minWidth: 140,
      showOverflowTooltip: true
    },
    {
      prop: 'model',
      label: t('pages.task.expand.model'),
      minWidth: 140,
      showOverflowTooltip: true
    },
    {
      prop: 'createByText',
      label: t('table.createBy'),
      minWidth: 120,
      showOverflowTooltip: true
    },
    {
      prop: 'createTimeText',
      label: t('table.createTime'),
      minWidth: 180,
      showOverflowTooltip: true
    },
    {
      prop: 'updateByText',
      label: t('pages.orders.delivery.detail.updateBy'),
      minWidth: 120,
      showOverflowTooltip: true
    },
    {
      prop: 'statusText',
      label: t('table.status'),
      minWidth: 120,
      showOverflowTooltip: true
    },
    {
      prop: 'memo',
      label: t('table.remark'),
      minWidth: 180,
      showOverflowTooltip: true
    },
    {
@@ -257,11 +315,15 @@
      }
      if (action.key === 'complete') {
        await confirmTaskAction(t('pages.task.messages.completeConfirm', { code: row.taskCode || '' }))
        await confirmTaskAction(
          t('pages.task.messages.completeConfirm', { code: row.taskCode || '' })
        )
        await fetchCompleteTask(row.id)
        ElMessage.success(t('pages.task.messages.completeSuccess'))
      } else if (action.key === 'remove') {
        await confirmTaskAction(t('pages.task.messages.removeConfirm', { code: row.taskCode || '' }))
        await confirmTaskAction(
          t('pages.task.messages.removeConfirm', { code: row.taskCode || '' })
        )
        await fetchRemoveTask(row.id)
        ElMessage.success(t('pages.task.messages.removeSuccess'))
      } else if (action.key === 'check') {
@@ -338,9 +400,13 @@
  async function loadAutoRunConfig() {
    autoRunLoading.value = true
    try {
      const response = await guardRequestWithMessage(fetchTaskAutoRunFlag(), { val: false }, {
        timeoutMessage: t('pages.task.messages.autoRunTimeout')
      })
      const response = await guardRequestWithMessage(
        fetchTaskAutoRunFlag(),
        { val: false },
        {
          timeoutMessage: t('pages.task.messages.autoRunTimeout')
        }
      )
      const rawValue = response?.val
      autoRunEnabled.value =
        rawValue === true || rawValue === 'true' || rawValue === 1 || rawValue === '1'
@@ -354,7 +420,11 @@
    try {
      await fetchUpdateTaskAutoRunFlag(enabled)
      autoRunEnabled.value = enabled
      ElMessage.success(enabled ? t('pages.task.messages.autoRunOnSuccess') : t('pages.task.messages.autoRunOffSuccess'))
      ElMessage.success(
        enabled
          ? t('pages.task.messages.autoRunOnSuccess')
          : t('pages.task.messages.autoRunOffSuccess')
      )
    } catch (error) {
      ElMessage.error(error?.message || t('pages.task.messages.autoRunUpdateFailed'))
    } finally {
@@ -369,28 +439,59 @@
    detailLoading.value = true
    try {
      const taskItemResponse = await guardRequestWithMessage(
        fetchTaskItemPage({
          taskId: activeTaskRow.value.id,
          current: detailPagination.current,
          pageSize: detailPagination.size
      const [taskDetailResult, taskItemResult] = await Promise.allSettled([
        guardRequestWithMessage(fetchTaskDetail(activeTaskRow.value.id), activeTaskRow.value, {
          timeoutMessage: t('pages.task.messages.detailTimeout')
        }),
        {
          records: [],
          total: 0,
          current: detailPagination.current,
          size: detailPagination.size
        },
        { timeoutMessage: t('pages.task.messages.itemsTimeout') }
      )
        guardRequestWithMessage(
          fetchTaskItemPage({
            taskId: activeTaskRow.value.id,
            current: detailPagination.current,
            pageSize: detailPagination.size
          }),
          {
            records: [],
            total: 0,
            current: detailPagination.current,
            size: detailPagination.size
          },
          { timeoutMessage: t('pages.task.messages.itemsTimeout') }
        )
      ])
      const taskDetailResponse =
        taskDetailResult.status === 'fulfilled' ? taskDetailResult.value : activeTaskRow.value
      const taskItemResponse =
        taskItemResult.status === 'fulfilled'
          ? taskItemResult.value
          : {
              records: [],
              total: 0,
              current: detailPagination.current,
              size: detailPagination.size
            }
      activeTaskRow.value = {
        ...activeTaskRow.value,
        ...taskDetailResponse
      }
      detailData.value = normalizeTaskRow(activeTaskRow.value)
      detailTableData.value = Array.isArray(taskItemResponse?.records)
      detailTableData.value = Array.isArray(taskItemResponse.records)
        ? taskItemResponse.records.map((record) => normalizeTaskItemRow(record))
        : []
      updatePaginationState(detailPagination, taskItemResponse, detailPagination.current, detailPagination.size)
      updatePaginationState(
        detailPagination,
        taskItemResponse,
        detailPagination.current,
        detailPagination.size
      )
      if (taskDetailResult.status === 'rejected' && taskItemResult.status === 'rejected') {
        throw taskDetailResult.reason || taskItemResult.reason
      }
    } catch (error) {
      detailTableData.value = []
      detailData.value = normalizeTaskRow(activeTaskRow.value)
      ElMessage.error(error?.message || t('pages.task.messages.detailLoadFailed'))
    } finally {
      detailLoading.value = false
rsf-design/src/views/manager/task/modules/task-detail-drawer.vue
@@ -5,70 +5,127 @@
    size="85%"
    @update:model-value="handleVisibleChange"
  >
    <div class="flex h-full flex-col gap-4">
      <div class="grid gap-4 xl:grid-cols-[1.45fr_1fr]">
        <ElCard shadow="never" class="border border-[var(--el-border-color-lighter)]">
          <template #header>
            <div class="flex items-center justify-between">
              <span class="font-medium text-[var(--art-text-gray-900)]">{{ t('pages.task.detail.baseInfo') }}</span>
              <ElTag size="small" effect="plain" type="primary">
                {{ detail.taskCode || '--' }}
              </ElTag>
    <ElScrollbar class="h-full">
      <div class="flex min-h-full min-w-0 flex-col gap-4 pr-2">
        <div class="grid shrink-0 gap-4 xl:grid-cols-[1.45fr_1fr]">
          <ElCard
            shadow="never"
            class="task-detail-card border border-[var(--el-border-color-lighter)]"
          >
            <template #header>
              <div class="flex items-center justify-between">
                <span class="font-medium text-[var(--art-text-gray-900)]">{{
                  t('pages.task.detail.baseInfo')
                }}</span>
                <ElTag size="small" effect="plain" type="primary">
                  {{ detail.taskCode || '--' }}
                </ElTag>
              </div>
            </template>
            <ElDescriptions :column="2" border size="small" class="compact-descriptions">
              <ElDescriptionsItem :label="t('pages.task.detail.taskId')">{{
                detail.taskId ?? '--'
              }}</ElDescriptionsItem>
              <ElDescriptionsItem :label="t('pages.task.detail.taskStatus')">{{
                detail.taskStatusLabel || '--'
              }}</ElDescriptionsItem>
              <ElDescriptionsItem :label="t('pages.task.detail.taskType')">{{
                detail.taskTypeLabel || '--'
              }}</ElDescriptionsItem>
              <ElDescriptionsItem :label="t('pages.task.detail.warehType')">{{
                detail.warehTypeLabel || '--'
              }}</ElDescriptionsItem>
              <ElDescriptionsItem :label="t('pages.task.detail.priority')">{{
                detail.sort ?? '--'
              }}</ElDescriptionsItem>
              <ElDescriptionsItem :label="t('pages.task.detail.status')">{{
                detail.statusText || '--'
              }}</ElDescriptionsItem>
              <ElDescriptionsItem :label="t('pages.task.detail.robotCode')">{{
                detail.robotCode || '--'
              }}</ElDescriptionsItem>
              <ElDescriptionsItem :label="t('pages.task.detail.createBy')">{{
                detail.createByText || '--'
              }}</ElDescriptionsItem>
              <ElDescriptionsItem :label="t('pages.task.detail.createTime')">{{
                detail.createTimeText || '--'
              }}</ElDescriptionsItem>
              <ElDescriptionsItem :label="t('pages.task.detail.updateBy')">{{
                detail.updateByText || '--'
              }}</ElDescriptionsItem>
              <ElDescriptionsItem :label="t('pages.task.detail.updateTime')">{{
                detail.updateTimeText || '--'
              }}</ElDescriptionsItem>
              <ElDescriptionsItem :label="t('pages.task.detail.memo')" :span="2">{{
                detail.memo || '--'
              }}</ElDescriptionsItem>
            </ElDescriptions>
          </ElCard>
          <ElCard
            shadow="never"
            class="task-detail-card border border-[var(--el-border-color-lighter)]"
          >
            <template #header>
              <div class="flex items-center justify-between">
                <span class="font-medium text-[var(--art-text-gray-900)]">{{
                  t('pages.task.detail.pathInfo')
                }}</span>
                <ElButton text type="primary" @click="$emit('flow-step')">{{
                  t('pages.task.detail.flowStep')
                }}</ElButton>
              </div>
            </template>
            <ElDescriptions :column="1" border size="small" class="compact-descriptions">
              <ElDescriptionsItem :label="t('pages.task.detail.orgLoc')">{{
                detail.orgLoc || '--'
              }}</ElDescriptionsItem>
              <ElDescriptionsItem :label="t('pages.task.detail.orgSite')">{{
                detail.orgSiteLabel || '--'
              }}</ElDescriptionsItem>
              <ElDescriptionsItem :label="t('pages.task.detail.targLoc')">{{
                detail.targLoc || '--'
              }}</ElDescriptionsItem>
              <ElDescriptionsItem :label="t('pages.task.detail.targSite')">{{
                detail.targSiteLabel || '--'
              }}</ElDescriptionsItem>
              <ElDescriptionsItem :label="t('pages.task.detail.barcode')">{{
                detail.barcode || '--'
              }}</ElDescriptionsItem>
            </ElDescriptions>
          </ElCard>
        </div>
        <div class="shrink-0 flex items-center justify-between">
          <div>
            <div class="text-sm font-medium text-[var(--art-text-gray-900)]">{{
              t('pages.task.detail.items')
            }}</div>
            <div class="mt-1 text-xs text-[var(--art-text-gray-500)]">
              {{ t('pages.task.detail.itemsHint') }}
            </div>
          </template>
          <ElDescriptions :column="2" border>
            <ElDescriptionsItem :label="t('pages.task.detail.taskStatus')">{{ detail.taskStatusLabel || '--' }}</ElDescriptionsItem>
            <ElDescriptionsItem :label="t('pages.task.detail.taskType')">{{ detail.taskTypeLabel || '--' }}</ElDescriptionsItem>
            <ElDescriptionsItem :label="t('pages.task.detail.warehType')">{{ detail.warehTypeLabel || '--' }}</ElDescriptionsItem>
            <ElDescriptionsItem :label="t('pages.task.detail.priority')">{{ detail.sort ?? '--' }}</ElDescriptionsItem>
            <ElDescriptionsItem :label="t('pages.task.detail.status')">{{ detail.statusText || '--' }}</ElDescriptionsItem>
            <ElDescriptionsItem :label="t('pages.task.detail.robotCode')">{{ detail.robotCode || '--' }}</ElDescriptionsItem>
            <ElDescriptionsItem :label="t('pages.task.detail.createTime')">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
            <ElDescriptionsItem :label="t('pages.task.detail.updateTime')">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
            <ElDescriptionsItem :label="t('pages.task.detail.memo')" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
          </ElDescriptions>
        </ElCard>
        <ElCard shadow="never" class="border border-[var(--el-border-color-lighter)]">
          <template #header>
            <div class="flex items-center justify-between">
              <span class="font-medium text-[var(--art-text-gray-900)]">{{ t('pages.task.detail.pathInfo') }}</span>
              <ElButton text type="primary" @click="$emit('flow-step')">{{ t('pages.task.detail.flowStep') }}</ElButton>
            </div>
          </template>
          <ElDescriptions :column="1" border>
            <ElDescriptionsItem :label="t('pages.task.detail.orgLoc')">{{ detail.orgLoc || '--' }}</ElDescriptionsItem>
            <ElDescriptionsItem :label="t('pages.task.detail.orgSite')">{{ detail.orgSiteLabel || '--' }}</ElDescriptionsItem>
            <ElDescriptionsItem :label="t('pages.task.detail.targLoc')">{{ detail.targLoc || '--' }}</ElDescriptionsItem>
            <ElDescriptionsItem :label="t('pages.task.detail.targSite')">{{ detail.targSiteLabel || '--' }}</ElDescriptionsItem>
            <ElDescriptionsItem :label="t('pages.task.detail.barcode')">{{ detail.barcode || '--' }}</ElDescriptionsItem>
          </ElDescriptions>
        </ElCard>
      </div>
      <div class="flex items-center justify-between">
        <div>
          <div class="text-sm font-medium text-[var(--art-text-gray-900)]">{{ t('pages.task.detail.items') }}</div>
          <div class="mt-1 text-xs text-[var(--art-text-gray-500)]">
            {{ t('pages.task.detail.itemsHint') }}
          </div>
          <div class="flex items-center gap-2">
            <ElButton :loading="loading" @click="$emit('refresh')">{{
              t('common.actions.refresh')
            }}</ElButton>
          </div>
        </div>
        <div class="flex items-center gap-2">
          <ElButton :loading="loading" @click="$emit('refresh')">{{ t('common.actions.refresh') }}</ElButton>
        <div class="min-w-0">
          <ArtTable
            :loading="loading"
            :data="data"
            :columns="columns"
            :pagination="pagination"
            @pagination:size-change="$emit('size-change', $event)"
            @pagination:current-change="$emit('current-change', $event)"
          />
        </div>
      </div>
      <ArtTable
        :loading="loading"
        :data="data"
        :columns="columns"
        :pagination="pagination"
        @pagination:size-change="$emit('size-change', $event)"
        @pagination:current-change="$emit('current-change', $event)"
      />
    </div>
    </ElScrollbar>
  </ElDrawer>
</template>
@@ -86,9 +143,31 @@
    pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
  })
  const emit = defineEmits(['update:visible', 'refresh', 'size-change', 'current-change', 'flow-step'])
  const emit = defineEmits([
    'update:visible',
    'refresh',
    'size-change',
    'current-change',
    'flow-step'
  ])
  function handleVisibleChange(visible) {
    emit('update:visible', visible)
  }
</script>
<style scoped>
  :deep(.task-detail-card .el-card__header) {
    padding: 12px 16px;
  }
  :deep(.task-detail-card .el-card__body) {
    padding: 12px 16px 16px;
  }
  :deep(.compact-descriptions .el-descriptions__label.el-descriptions__cell),
  :deep(.compact-descriptions .el-descriptions__content.el-descriptions__cell) {
    padding-top: 10px;
    padding-bottom: 10px;
  }
</style>
rsf-design/src/views/manager/task/modules/task-expand-panel.vue
@@ -1,8 +1,12 @@
<template>
  <div class="rounded-xl bg-[var(--el-fill-color-blank)] px-4 py-4">
    <div class="mb-3 flex items-center justify-between">
      <div class="text-sm font-medium text-[var(--art-gray-900)]">{{ t('pages.task.expand.title') }}</div>
      <ElButton text size="small" :loading="loading" @click="loadData">{{ t('common.actions.refresh') }}</ElButton>
      <div class="text-sm font-medium text-[var(--art-gray-900)]">{{
        t('pages.task.expand.title')
      }}</div>
      <ElButton text size="small" :loading="loading" @click="loadData">{{
        t('common.actions.refresh')
      }}</ElButton>
    </div>
    <ArtTable
@@ -53,6 +57,12 @@
      showOverflowTooltip: true
    },
    {
      prop: 'platOrderCode',
      label: t('pages.task.expand.platOrderCode'),
      minWidth: 150,
      showOverflowTooltip: true
    },
    {
      prop: 'platWorkCode',
      label: t('pages.task.expand.platWorkCode'),
      minWidth: 150,
@@ -62,6 +72,12 @@
      prop: 'platItemId',
      label: t('pages.task.expand.platItemId'),
      minWidth: 100,
      showOverflowTooltip: true
    },
    {
      prop: 'projectCode',
      label: t('pages.task.expand.projectCode'),
      minWidth: 140,
      showOverflowTooltip: true
    },
    {
@@ -94,12 +110,60 @@
      align: 'right'
    },
    {
      prop: 'workQty',
      label: t('pages.task.expand.workQty'),
      width: 100,
      align: 'right'
    },
    {
      prop: 'qty',
      label: t('pages.task.expand.qty'),
      width: 100,
      align: 'right'
    },
    {
      prop: 'spec',
      label: t('pages.task.expand.spec'),
      minWidth: 140,
      showOverflowTooltip: true
    },
    {
      prop: 'model',
      label: t('pages.task.expand.model'),
      minWidth: 140,
      showOverflowTooltip: true
    },
    {
      prop: 'createByText',
      label: t('table.createBy'),
      minWidth: 120,
      showOverflowTooltip: true
    },
    {
      prop: 'createTimeText',
      label: t('table.createTime'),
      minWidth: 180,
      showOverflowTooltip: true
    },
    {
      prop: 'updateByText',
      label: t('table.updateBy'),
      minWidth: 120,
      showOverflowTooltip: true
    },
    {
      prop: 'statusText',
      label: t('table.status'),
      minWidth: 120,
      showOverflowTooltip: true
    },
    {
      prop: 'memo',
      label: t('table.remark'),
      minWidth: 180,
      showOverflowTooltip: true
    },
    {
      prop: 'updateTimeText',
      label: t('table.updateTime'),
      minWidth: 180,
rsf-design/src/views/manager/task/modules/task-flow-step-dialog.vue
@@ -8,13 +8,27 @@
    @update:model-value="emit('update:visible', $event)"
  >
    <div class="flex flex-col gap-4">
      <div class="rounded-xl border border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-blank)] px-4 py-3">
        <div class="text-sm text-[var(--art-gray-500)]">{{ t('pages.task.flowStepDialog.currentTask') }}</div>
        <div class="mt-1 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-[var(--art-gray-900)]">
      <div
        class="rounded-xl border border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-blank)] px-4 py-3"
      >
        <div class="text-sm text-[var(--art-gray-500)]">{{
          t('pages.task.flowStepDialog.currentTask')
        }}</div>
        <div
          class="mt-1 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-[var(--art-gray-900)]"
        >
          <span>{{ t('pages.task.detail.taskCode') }}:{{ taskRow?.taskCode || '--' }}</span>
          <span>{{ t('pages.task.detail.taskStatus') }}:{{ taskRow?.taskStatusLabel || '--' }}</span>
          <span
            >{{ t('pages.task.detail.taskStatus') }}:{{ taskRow?.taskStatusLabel || '--' }}</span
          >
          <span>{{ t('pages.task.detail.taskType') }}:{{ taskRow?.taskTypeLabel || '--' }}</span>
        </div>
      </div>
      <div class="flex items-center justify-end">
        <ElButton v-auth="'add'" type="primary" plain @click="handleOpenCreate">
          {{ t('pages.task.flowStepDialog.create') }}
        </ElButton>
      </div>
      <ArtTable
@@ -26,14 +40,87 @@
        @pagination:current-change="handleCurrentChange"
      />
    </div>
    <ElDialog
      :model-value="editorVisible"
      :title="editorTitle"
      width="560px"
      append-to-body
      destroy-on-close
      @update:model-value="handleEditorVisibleChange"
    >
      <ElForm
        ref="formRef"
        :model="formData"
        :rules="formRules"
        label-width="96px"
        label-position="right"
      >
        <ElFormItem :label="t('pages.task.flowStepDialog.form.taskNo')" prop="taskNo">
          <ElInput v-model="formData.taskNo" disabled />
        </ElFormItem>
        <ElFormItem :label="t('pages.task.flowStepDialog.form.stepOrder')" prop="stepOrder">
          <ElInputNumber
            v-model="formData.stepOrder"
            class="w-full"
            :min="0"
            controls-position="right"
          />
        </ElFormItem>
        <ElFormItem :label="t('pages.task.flowStepDialog.form.stepCode')" prop="stepCode">
          <ElInput v-model.trim="formData.stepCode" />
        </ElFormItem>
        <ElFormItem :label="t('pages.task.flowStepDialog.form.stepName')" prop="stepName">
          <ElInput v-model.trim="formData.stepName" />
        </ElFormItem>
        <ElFormItem :label="t('pages.task.flowStepDialog.form.stepType')" prop="stepType">
          <ElInput v-model.trim="formData.stepType" />
        </ElFormItem>
        <ElFormItem :label="t('pages.task.flowStepDialog.form.status')" prop="status">
          <ElSelect v-model="formData.status" class="w-full">
            <ElOption
              v-for="item in statusOptions"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </ElSelect>
        </ElFormItem>
        <ElFormItem :label="t('pages.task.flowStepDialog.form.executeResult')" prop="executeResult">
          <ElInput
            v-model.trim="formData.executeResult"
            type="textarea"
            :rows="3"
            :placeholder="t('pages.task.flowStepDialog.form.executeResultPlaceholder')"
          />
        </ElFormItem>
      </ElForm>
      <template #footer>
        <div class="flex justify-end gap-3">
          <ElButton @click="handleEditorVisibleChange(false)">{{ t('common.cancel') }}</ElButton>
          <ElButton type="primary" :loading="submitLoading" @click="handleSubmit">
            {{ t('common.actions.save') }}
          </ElButton>
        </div>
      </template>
    </ElDialog>
  </ElDialog>
</template>
<script setup>
  import { computed, reactive, ref, watch } from 'vue'
  import { ElMessage, ElMessageBox } from 'element-plus'
  import { computed, h, reactive, ref, watch } from 'vue'
  import { useI18n } from 'vue-i18n'
  import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
  import { fetchFlowStepInstancePage } from '@/api/flow-step-instance'
  import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
  import {
    fetchFlowStepInstancePage,
    fetchJumpCurrentFlowStepInstance,
    fetchRemoveFlowStepInstance,
    fetchSaveFlowStepInstance,
    fetchUpdateFlowStepInstance
  } from '@/api/flow-step-instance'
  import { normalizeFlowStepInstanceRow } from '@/views/system/flow-step-instance/flowStepInstancePage.helpers'
  const props = defineProps({
@@ -50,13 +137,72 @@
  const emit = defineEmits(['update:visible'])
  const { t } = useI18n()
  const formRef = ref()
  const loading = ref(false)
  const rows = ref([])
  const editorVisible = ref(false)
  const submitLoading = ref(false)
  const editingId = ref(null)
  const pagination = reactive({
    current: 1,
    size: 20,
    total: 0
  })
  const formData = reactive(createFormState())
  const statusOptions = computed(() => [
    { value: 0, label: t('pages.task.flowStepDialog.statusOptions.0') },
    { value: 1, label: t('pages.task.flowStepDialog.statusOptions.1') },
    { value: 2, label: t('pages.task.flowStepDialog.statusOptions.2') },
    { value: 3, label: t('pages.task.flowStepDialog.statusOptions.3') },
    { value: 4, label: t('pages.task.flowStepDialog.statusOptions.4') },
    { value: 5, label: t('pages.task.flowStepDialog.statusOptions.5') },
    { value: 6, label: t('pages.task.flowStepDialog.statusOptions.6') }
  ])
  const formRules = computed(() => ({
    stepOrder: [
      {
        required: true,
        message: t('pages.task.flowStepDialog.validation.stepOrder'),
        trigger: 'blur'
      }
    ],
    stepCode: [
      {
        required: true,
        message: t('pages.task.flowStepDialog.validation.stepCode'),
        trigger: 'blur'
      }
    ],
    stepName: [
      {
        required: true,
        message: t('pages.task.flowStepDialog.validation.stepName'),
        trigger: 'blur'
      }
    ],
    stepType: [
      {
        required: true,
        message: t('pages.task.flowStepDialog.validation.stepType'),
        trigger: 'blur'
      }
    ],
    status: [
      {
        required: true,
        message: t('pages.task.flowStepDialog.validation.status'),
        trigger: 'change'
      }
    ]
  }))
  const editorTitle = computed(() =>
    editingId.value
      ? t('pages.task.flowStepDialog.editTitle')
      : t('pages.task.flowStepDialog.createTitle')
  )
  const columns = computed(() => [
    {
@@ -112,8 +258,61 @@
      label: t('pages.task.flowStepDialog.endTime'),
      minWidth: 180,
      showOverflowTooltip: true
    },
    {
      prop: 'operation',
      label: t('table.operation'),
      width: 120,
      align: 'center',
      fixed: 'right',
      formatter: (row) =>
        h(ArtButtonMore, {
          list: getActionList(row),
          onClick: (item) => handleActionClick(item, row)
        })
    }
  ])
  function createFormState() {
    return {
      id: '',
      taskNo: '',
      stepOrder: 0,
      stepCode: '',
      stepName: '',
      stepType: '',
      status: 0,
      executeResult: ''
    }
  }
  function resetFormState(seed = {}) {
    Object.assign(formData, createFormState(), seed)
  }
  function getActionList() {
    return [
      {
        key: 'edit',
        label: t('common.actions.edit'),
        icon: 'ri:pencil-line',
        auth: 'update'
      },
      {
        key: 'jumpCurrent',
        label: t('pages.task.flowStepDialog.jumpCurrent'),
        icon: 'ri:send-plane-line',
        auth: 'update'
      },
      {
        key: 'delete',
        label: t('common.actions.delete'),
        icon: 'ri:delete-bin-5-line',
        color: '#f56c6c',
        auth: 'delete'
      }
    ]
  }
  function updatePaginationState(response) {
    pagination.total = Number(response?.total || 0)
@@ -162,15 +361,141 @@
    void loadRows()
  }
  function handleOpenCreate() {
    editingId.value = null
    resetFormState({
      taskNo: props.taskRow?.taskCode || '',
      status: 0
    })
    editorVisible.value = true
  }
  function handleOpenEdit(row) {
    editingId.value = row.id
    resetFormState({
      ...row,
      id: row.id,
      taskNo: row.taskNo || props.taskRow?.taskCode || '',
      stepOrder: Number(row.stepOrder ?? 0),
      status: Number(row.status ?? 0)
    })
    editorVisible.value = true
  }
  function handleEditorVisibleChange(visible) {
    editorVisible.value = visible
    if (!visible) {
      editingId.value = null
      resetFormState({
        taskNo: props.taskRow?.taskCode || '',
        status: 0
      })
      formRef.value?.clearValidate()
    }
  }
  async function handleSubmit() {
    if (!formRef.value) {
      return
    }
    const valid = await formRef.value.validate().catch(() => false)
    if (!valid) {
      return
    }
    submitLoading.value = true
    try {
      const payload = {
        ...(editingId.value ? { id: editingId.value } : {}),
        taskNo: formData.taskNo,
        stepOrder: Number(formData.stepOrder ?? 0),
        stepCode: formData.stepCode,
        stepName: formData.stepName,
        stepType: formData.stepType,
        status: Number(formData.status ?? 0),
        executeResult: formData.executeResult
      }
      if (editingId.value) {
        await fetchUpdateFlowStepInstance(payload)
        ElMessage.success(t('pages.task.flowStepDialog.messages.updateSuccess'))
      } else {
        await fetchSaveFlowStepInstance(payload)
        ElMessage.success(t('pages.task.flowStepDialog.messages.createSuccess'))
      }
      handleEditorVisibleChange(false)
      await loadRows()
    } catch (error) {
      ElMessage.error(error?.message || t('pages.task.flowStepDialog.messages.submitFailed'))
    } finally {
      submitLoading.value = false
    }
  }
  async function handleDelete(row) {
    try {
      await ElMessageBox.confirm(
        t('pages.task.flowStepDialog.messages.deleteConfirm', {
          code: row.stepCode || row.id || ''
        }),
        t('crud.confirm.deleteTitle'),
        {
          type: 'warning',
          confirmButtonText: t('common.confirm'),
          cancelButtonText: t('common.cancel')
        }
      )
      await fetchRemoveFlowStepInstance(row.id)
      ElMessage.success(t('pages.task.flowStepDialog.messages.deleteSuccess'))
      await loadRows()
    } catch (error) {
      if (error === 'cancel' || error === 'close') {
        return
      }
      ElMessage.error(error?.message || t('pages.task.flowStepDialog.messages.deleteFailed'))
    }
  }
  async function handleJumpCurrent(row) {
    try {
      await fetchJumpCurrentFlowStepInstance(row.id)
      ElMessage.success(t('pages.task.flowStepDialog.messages.jumpSuccess'))
      await loadRows()
    } catch (error) {
      ElMessage.error(error?.message || t('pages.task.flowStepDialog.messages.jumpFailed'))
    }
  }
  function handleActionClick(action, row) {
    if (action.key === 'edit') {
      handleOpenEdit(row)
      return
    }
    if (action.key === 'jumpCurrent') {
      void handleJumpCurrent(row)
      return
    }
    if (action.key === 'delete') {
      void handleDelete(row)
    }
  }
  watch(
    () => [props.visible, props.taskRow?.taskCode],
    ([visible]) => {
      if (!visible) {
        rows.value = []
        pagination.current = 1
        handleEditorVisibleChange(false)
        return
      }
      pagination.current = 1
      resetFormState({
        taskNo: props.taskRow?.taskCode || '',
        status: 0
      })
      void loadRows()
    }
  )
rsf-design/src/views/manager/task/taskPage.helpers.js
@@ -42,6 +42,7 @@
export function normalizeTaskRow(record = {}) {
  return {
    ...record,
    taskId: record.id ?? '--',
    taskCode: record.taskCode || '-',
    taskStatusLabel: record['taskStatus$'] || '-',
    taskTypeLabel: record['taskType$'] || '-',
@@ -53,9 +54,17 @@
    barcode: record.barcode || '-',
    robotCode: record.robotCode || '-',
    sort: normalizeNumber(record.sort),
    exceStatusText: record['exceStatus$'] || record.exceStatus || '-',
    expDesc: record.expDesc || '-',
    expCode: record.expCode || '-',
    startTimeText: record['startTime$'] || record.startTime || '-',
    endTimeText: record['endTime$'] || record.endTime || '-',
    createByText: record['createBy$'] || record.createByText || record.createBy || '-',
    statusText: record['status$'] || '-',
    memo: record.memo || '-',
    updateTimeText: record['updateTime$'] || record.updateTime || '-',
    createTimeText: record['createTime$'] || record.createTime || '-',
    updateByText: record['updateBy$'] || record.updateByText || record.updateBy || '-',
    canComplete: record.canComplete === true,
    canCancel: record.canCancel === true
  }
@@ -66,15 +75,25 @@
    ...record,
    orderTypeLabel: record['orderType$'] || '-',
    wkTypeLabel: record['wkType$'] || '-',
    platOrderCode: record.platOrderCode || '-',
    platWorkCode: record.platWorkCode || '-',
    platItemId: record.platItemId || '-',
    projectCode: record.projectCode || '-',
    matnrCode: record.matnrCode || '-',
    maktx: record.maktx || '-',
    batch: record.batch || '-',
    unit: record.unit || '-',
    anfme: normalizeNumber(record.anfme),
    updateByText: record['updateBy$'] || '-',
    updateTimeText: record['updateTime$'] || record.updateTime || '-'
    workQty: normalizeNumber(record.workQty),
    qty: normalizeNumber(record.qty),
    spec: record.spec || '-',
    model: record.model || '-',
    createByText: record['createBy$'] || record.createByText || record.createBy || '-',
    createTimeText: record['createTime$'] || record.createTime || '-',
    updateByText: record['updateBy$'] || record.updateByText || record.updateBy || '-',
    updateTimeText: record['updateTime$'] || record.updateTime || '-',
    statusText: record['status$'] || record.status || '-',
    memo: record.memo || '-'
  }
}
rsf-server/pom.xml
@@ -53,6 +53,11 @@
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>2.9.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-openai</artifactId>
        </dependency>
rsf-server/src/main/java/com/vincent/rsf/server/common/config/MybatisPlusConfig.java
@@ -3,6 +3,8 @@
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.MybatisMapWrapperFactory;
import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal;
import com.baomidou.mybatisplus.extension.parser.cache.JdkSerialCaffeineJsqlParseCache;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
@@ -11,7 +13,6 @@
import com.vincent.rsf.server.system.entity.User;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.NullValue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
@@ -19,6 +20,10 @@
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.util.Arrays;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * MybatisPlus配置
@@ -30,8 +35,11 @@
@EnableTransactionManagement
public class MybatisPlusConfig {
    private static volatile boolean jsqlParserConfigured = false;
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        configureJsqlParser();
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加乐观锁插件
@@ -75,6 +83,41 @@
        return interceptor;
    }
    private void configureJsqlParser() {
        if (jsqlParserConfigured) {
            return;
        }
        synchronized (MybatisPlusConfig.class) {
            if (jsqlParserConfigured) {
                return;
            }
            AtomicInteger threadIndex = new AtomicInteger(1);
            int parserThreads = Math.max(4, Runtime.getRuntime().availableProcessors());
            ThreadPoolExecutor parserExecutor = new ThreadPoolExecutor(
                    parserThreads,
                    parserThreads,
                    0L,
                    TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<>(2048),
                    runnable -> {
                        Thread thread = new Thread(runnable);
                        thread.setName("jsql-parser-" + threadIndex.getAndIncrement());
                        thread.setDaemon(true);
                        return thread;
                    },
                    new ThreadPoolExecutor.CallerRunsPolicy()
            );
            JsqlParserGlobal.setExecutorService(parserExecutor, new Thread(() -> {
                if (!parserExecutor.isShutdown()) {
                    parserExecutor.shutdown();
                }
            }, "jsql-parser-shutdown"));
            JsqlParserGlobal.setJsqlParseCache(new JdkSerialCaffeineJsqlParseCache(builder ->
                    builder.maximumSize(1024).expireAfterAccess(30, TimeUnit.MINUTES)));
            jsqlParserConfigured = true;
        }
    }
    /**
     * 获取当前登录用户的租户id
     *