Lin Qilong пре 3 месеци
родитељ
комит
0021fd1808

+ 52 - 0
ruoyi-ui/src/views/dpp/task/integratioTask/argsStr.vue

@@ -0,0 +1,52 @@
+<template>
+  <el-form ref="formRef" :model="form" label-width="80px" label-position="top" style="width: 100%;">
+    <el-form-item label="请求数据">
+      <div
+          style="width: 100%;height: 100%;max-height: 35vh;background-color: #FAFAFA;overflow: auto;border-radius: 10px;">
+        <biz-data-show-config-api v-model="form.queryOptions"></biz-data-show-config-api>
+      </div>
+    </el-form-item>
+    <el-form-item label="规则管理">
+      <div
+          style="width: 100%;height: 35vh;background-color: #FAFAFA;overflow: auto;border-radius: 10px;">
+        <clean-form2 v-model="form.ruleList" :columns="fieldColumns"></clean-form2>
+      </div>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup>
+import BizDataShowConfigApi from "@/views/standardization/bizDataShowConfig/api/index.vue"
+import cleanForm2 from "./components/clean/cleanForm2.vue"
+
+const props = defineProps({
+  modelValue: String,
+});
+const emit = defineEmits(['update:modelValue'])
+const fieldColumns = ref([]);
+const form = ref({
+  queryOptions: "",
+  ruleList: ""
+});
+
+watch(() => props.modelValue, modelValue => {
+  if (modelValue) {
+    form.value = JSON.parse(modelValue)
+  }
+}, {immediate: true})
+
+watch(() => form, form => {
+  emit('update:modelValue', JSON.stringify(form.value))
+}, {deep: true})
+watch(() => form.value.queryOptions, queryOptions => {
+  if (queryOptions) {
+    const api = JSON.parse(queryOptions)
+    fieldColumns.value = api.response.map(item => {
+      return {
+        columnComment: item.value,
+        columnName: item.key
+      }
+    })
+  }
+}, {immediate: true, deep: true})
+</script>

+ 303 - 0
ruoyi-ui/src/views/dpp/task/integratioTask/components/clean/cleanForm2.vue

@@ -0,0 +1,303 @@
+<template>
+  <div style="width: 100%;height: 100%;padding: 10px;">
+    <el-button type="primary" icon="Plus" @click="openRuleSelector(undefined)" style="margin-bottom: 5px;">新增规则
+    </el-button>
+    <el-table stripe height="calc(100% - 37px)" style="width: 100%;" :data="tableFields" v-loading="loadingList"
+              ref="dragTable"
+              row-key="name">
+      <el-table-column label="序号" width="80" align="left">
+        <template #header>
+          <div class="justify-center">
+            <span>序号</span>
+            <el-tooltip effect="light" content="清洗规则按照下面配置的列表顺序,依次执行" placement="top">
+              <el-icon class="tip-icon">
+                <InfoFilled/>
+              </el-icon>
+            </el-tooltip>
+          </div>
+        </template>
+        <template #default="{ $index }">
+          <div class="allowDrag" style="cursor: move; display: flex; justify-content: center; align-items: center;">
+            <el-icon>
+              <Operation/>
+            </el-icon>
+            <span style="margin-left: 4px;">{{ $index + 1 }}</span>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="清洗名称" align="left" prop="name" :show-overflow-tooltip="{ effect: 'light' }"
+                       width="300">
+        <template #default="scope">
+          {{ scope.row.name || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="清洗字段" align="left" prop="columns" :show-overflow-tooltip="{ effect: 'light' }"
+                       width="300">
+        <template #default="scope">
+          {{ (scope.row.columns && scope.row.columns.length) ? scope.row.columns.join(', ') : '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="清洗规则" align="left" prop="ruleName" :show-overflow-tooltip="{ effect: 'light' }"
+                       width="300">
+        <template #default="scope">
+          {{ scope.row.ruleName || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="规则描述" align="left" prop="ruleDescription"
+                       :show-overflow-tooltip="{ effect: 'light' }">
+        <template #default="scope">
+          {{ scope.row.ruleDescription || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="维度" align="left" prop="parentName" :show-overflow-tooltip="{ effect: 'light' }"
+                       width="150">
+        <template #default="scope">
+          {{ scope.row.parentName || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="left" prop="status">
+        <template #default="scope">
+          {{ scope.row.status == '1' ? '上线' : '下线' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="180">
+        <template #default="scope">
+          <!-- <el-button link type="primary" icon="view"
+            @click="openRuleDialog(scope.row, scope.$index + 1, true)">查看</el-button> -->
+          <el-button link type="primary" icon="Edit"
+                     @click="openRuleDialog(scope.row, scope.$index + 1)">修改
+          </el-button>
+          <el-button link type="danger" icon="Delete" @click="handleRuleDelete(scope.$index + 1)">删除</el-button>
+
+        </template>
+      </el-table-column>
+    </el-table>
+    <RuleSelectorDialog ref="ruleSelectorDialog" @confirm="RuleSelectorconfirm" :inputFields="inputFields"/>
+  </div>
+</template>
+<script setup>
+import {defineEmits, defineProps, ref} from "vue";
+import {getNodeUniqueKey} from "@/api/dpp/task/index.js";
+import Sortable from "sortablejs";
+import useUserStore from "@/store/system/user.js";
+import {renameRuleToRuleConfig} from "@/views/dpp/utils/opBase.js";
+import RuleSelectorDialog from './rule/ruleBase.vue';
+
+const {proxy} = getCurrentInstance();
+const userStore = useUserStore();
+const {att_rule_clean_type, da_discovery_task_status, dpp_etl_task_execution_type} = proxy.useDict(
+    'att_rule_clean_type', 'da_discovery_task_status', 'dpp_etl_task_execution_type'
+);
+const props = defineProps({
+  columns: {
+    type: Array,
+    default: () => []
+  },
+  modelValue: String,
+});
+const emit = defineEmits(['update:modelValue'])
+let dragTable = ref(null);
+let sortableInstance = null;
+
+function setSort() {
+  nextTick(() => {
+    const tbody = dragTable.value?.$el.querySelector(
+        ".el-table__body-wrapper tbody"
+    );
+    if (!tbody) {
+      console.warn("tbody 找不到,拖拽初始化失败");
+      return;
+    }
+
+    if (sortableInstance) {
+      sortableInstance.destroy();
+    }
+
+    sortableInstance = Sortable.create(tbody, {
+      handle: ".allowDrag",
+      animation: 150,
+      onEnd: (evt) => {
+
+        const movedItem = tableFields.value.splice(evt.oldIndex, 1)[0];
+        tableFields.value.splice(evt.newIndex, 0, movedItem);
+        console.log("拖拽后顺序:", tableFields.value.map((f) => f.name));
+      },
+    });
+  });
+}
+
+let ruleSelectorDialog = ref()
+const openRuleSelector = (row) => {
+  ruleSelectorDialog.value.openDialog(row,);
+};
+const openRuleDialog = (row, index, falg) => {
+  ruleSelectorDialog.value.openDialog(row, index, falg);
+};
+const renameRuleToRule = () => {
+  const result = renameRuleToRuleConfig(inputFields.value);
+  let coverCount = 0;
+  let addCount = 0;
+
+  const norm = (v) => String(v ?? '').trim().toUpperCase();
+  const sameCols = (a, b) => {
+    if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
+    return [...a].map(norm).sort().join('|') === [...b].map(norm).sort().join('|');
+  };
+
+  result.forEach(newItem => {
+    // 找到是否存在相同 ruleName 且 columns 一样的旧数据
+    const existingIndex = tableFields.value.findIndex(oldItem =>
+        norm(oldItem.ruleName) === norm(newItem.ruleName) &&
+        sameCols(oldItem.columns, newItem.columns)
+    );
+
+    if (existingIndex > -1) {
+      // 覆盖
+      tableFields.value[existingIndex] = newItem;
+      coverCount++;
+    } else {
+      // 追加
+      tableFields.value.push(newItem);
+      addCount++;
+    }
+  });
+
+  proxy.$message.success(`覆盖 ${coverCount} 条,追加 ${addCount} 条`);
+};
+
+function RuleSelectorconfirm(obj, mode) {
+  console.log("🚀 ~ RuleSelectorconfirm ~ obj:", obj)
+  const index = Number(mode) - 1;
+  const list = tableFields.value;
+  const isDuplicate = list.some((item, i) => {
+    if (index >= 0) {
+      return i !== index && item.name == obj.name;
+    } else {
+      return item.name === obj.name;
+    }
+  });
+
+  if (isDuplicate) {
+    proxy.$message.warning("清洗名称不能重复!");
+    return;
+  }
+
+  if (!isNaN(index) && index >= 0 && index < list.length) {
+    list.splice(index, 1, obj);
+  } else {
+    list.push(obj);
+  }
+
+  tableFields.value = list;
+  ruleSelectorDialog.value.closeDialog();
+  setSort()
+}
+
+function handleRuleDelete(index) {
+  tableFields.value.splice(Number(index) - 1, 1);
+  setSort()
+}
+
+// 输入字段
+let inputFields = ref([]);
+let tableFields = ref([]);
+// 变量定义
+let loading = ref(false);
+let loadingList = ref(false);
+let opens = ref(false);
+let row = ref();
+let TablesByDataSource = ref([]);
+let ColumnByAssettab = ref([]);
+let dpModelRefs = ref();
+let form = ref({});
+
+function handleRule(data) {
+  row.value = {};
+  row.value = data;
+  opens.value = true;
+}
+
+const submitForm = (value) => {
+  if (row.value?.index) {
+    tableFields.value[row.value.index - 1] = {
+      ...tableFields.value[row.value.index - 1],
+      cleanRuleList: value,
+      elementId: value.map((item) => item.ruleId),
+    };
+
+    opens.value = false;
+  }
+};
+
+const off = () => {
+  proxy.resetForm("dpModelRefs");
+  // 清空表格字段数据
+  ColumnByAssettab.value = [];
+  TablesByDataSource.value = [];
+  tableFields.value = [];
+};
+// 保存数据
+const saveData = async () => {
+  try {
+    const valid = await dpModelRefs.value.validate();
+    if (!valid) return;
+
+    // 如果没有 code,就调用接口获取唯一的 code
+    if (!form.value.code) {
+      loading.value = true;
+      const response = await getNodeUniqueKey({
+        projectCode: userStore.projectCode || "133545087166112",
+        projectId: userStore.projectId,
+      });
+      loading.value = false; // 结束加载状态
+      form.value.code = response.data; // 设置唯一的 code
+    }
+    const taskParams = form.value?.taskParams;
+    taskParams.tableFields = tableFields.value;
+    taskParams.outputFields = inputFields.value;
+    emit("confirm", form.value);
+
+  } catch (error) {
+    console.error("保存数据失败:", error);
+    loading.value = false;
+  }
+};
+const closeDialog = () => {
+  off();
+  // 关闭对话框
+  emit("update", false);
+};
+
+// 监听属性变化
+function deepCopy(data) {
+  if (data === undefined || data === null) {
+    return {}; // 或者返回一个默认值
+  }
+  try {
+    return JSON.parse(JSON.stringify(data));
+  } catch (e) {
+    return {}; // 或者返回一个默认值
+  }
+}
+
+let nodeOptions = ref([]);
+
+watch(() => props.columns, columns => {
+  inputFields.value = columns
+})
+
+watch(() => props.modelValue, modelValue => {
+  if (modelValue) {
+    tableFields.value = JSON.parse(modelValue)
+  }
+}, {immediate: true})
+
+watch(() => tableFields, tableFields => {
+  emit('update:modelValue', JSON.stringify(tableFields.value))
+}, {deep: true})
+</script>
+<style scoped lang="less">
+.blue-text {
+  color: #2666fb;
+}
+</style>

+ 81 - 0
ruoyi-ui/src/views/dpp/task/integratioTask/components/clean/rule/affixEditorRule.vue

@@ -0,0 +1,81 @@
+<template>
+<!--  字段前缀/后缀统一  -->
+    <el-form ref="formRef" :model="form" label-width="130px" :disabled="falg">
+        <el-row>
+            <el-col :span="12">
+                <el-form-item label="标记值" prop="stringValue"
+                    :rules="[{ required: true, message: '请输入标记值', trigger: 'blur' }]">
+                    <el-input v-model="form.stringValue" placeholder="请输入添加值" style="width: 290px;" />
+                </el-form-item>
+            </el-col>
+            <el-col :span="12" class="hasMsg">
+                <el-form-item label="处理方式" prop="handleType"
+                    :rules="[{ required: true, message: '请选择处理方式', trigger: 'blur' }]">
+                    <el-radio-group v-model="form.handleType">
+                        <el-radio :value="'1'">加前綴</el-radio>
+                        <el-radio :value="'2'">加后綴</el-radio>
+                        <el-radio :value="'3'">去除前缀</el-radio>
+                        <el-radio :value="'4'">去除后缀</el-radio>
+                    </el-radio-group>
+                </el-form-item>
+            </el-col>
+        </el-row>
+        <el-row>
+        </el-row>
+    </el-form>
+</template>
+
+<script setup>
+import { reactive, ref, watch } from "vue";
+const props = defineProps({
+    form: Object,
+    inputFields: Array,
+    falg: Boolean,
+});
+
+const emit = defineEmits(["update:form"]);
+
+const formRef = ref(null);
+
+const form = reactive({ ...props.form });
+const boundaryExamples = computed(() => {
+    switch (form.handleType) {
+        case '3':
+            return ['示例: 如果年龄 > 150,则设置为 150。'];
+        case '2':
+            return ['示例: 如果收入 < 1000,则设置为 1000。'];
+        case '1':
+            return [
+                '示例1: 如果年龄 > 150,则设置为 150。如果收入 < 1000,则设置为 1000。',
+            ];
+        default:
+            return [];
+    }
+});
+const loading = ref(false);
+const exposedFields = [
+    "stringValue",
+    "handleType"
+];
+function validate() {
+    return new Promise((resolve) => {
+        formRef.value.validate((valid) => {
+            if (valid) {
+                const data = Object.fromEntries(exposedFields.map(key => [key, form[key]]));
+                resolve({
+                    valid: true,
+                    data,
+                });
+            } else {
+                resolve({ valid: false });
+            }
+        });
+    });
+}
+
+
+
+
+defineExpose({ validate });
+</script>
+<style scoped></style>

+ 193 - 0
ruoyi-ui/src/views/dpp/task/integratioTask/components/clean/rule/combinerFieldUniqueRule.vue

@@ -0,0 +1,193 @@
+<template>
+<!--  组合字段去重  -->
+    <el-form ref="formRef" :model="form" label-width="130px" :disabled="falg">
+        <div class="deduplication-config">
+            <div class="justify-between mb15">
+                <el-row :gutter="15" class="btn-style">
+                    <el-col :span="1.5">
+                        <el-button type="primary" plain @click="addtypecolumns">
+                            <i class="iconfont-mini icon-xinzeng mr5"></i>新增排序字段
+                        </el-button>
+                    </el-col>
+                </el-row>
+            </div>
+            <el-table :data="form.stringValue" btype stripe style="width: 100%;" row-key="sort" ref="dragTable">
+                <el-table-column label="序号" width="80" align="left">
+                    <template #default="{ $index }">
+                        <div class="allowDrag"
+                            style="cursor: move; display: flex; justify-content: center; align-items: center;">
+                            <el-icon>
+                                <Operation />
+                            </el-icon>
+                            <span style="margin-left: 4px;">{{ $index + 1 }}</span>
+                        </div>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="columns" label="字段名称" align="left">
+                    <template #default="{ row }">
+                        <el-select v-model="row.columns" placeholder="请选择清洗字段" clearable>
+                            <el-option v-for="dict in inputFields" :key="dict.columnName" :label="dict.label"
+                                :value="dict.columnName" :disabled="iscolumnsDisabled(dict.columnName, row.id)" />
+                        </el-select>
+                    </template>
+                </el-table-column>
+
+                <el-table-column prop="type" label="排序顺序" align="left">
+                    <template #default="{ row }">
+                        <el-select v-model="row.type" placeholder="请选择" size="default">
+                            <el-option label="升序" value="1"></el-option>
+                            <el-option label="降序" value="0"></el-option>
+                        </el-select>
+                    </template>
+                </el-table-column>
+                <template #empty>
+                    <div class="emptyBg">
+                        <p>无数据</p>
+                    </div>
+                </template>
+                <el-table-column label="操作" align="center" width="100">
+                    <template #default="scope">
+                        <el-button link type="danger" icon="Delete" @click="handleDeletetypecolumns(scope.$index)">
+                            删除
+                        </el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+        </div>
+        <el-form-item label="去重策略" prop="handleType" style="margin-top: 20px;">
+            <el-radio-group v-model="form.handleType" class="strategy-radio-group">
+                <el-radio label="1" class="radio-item">
+                    <span class="radio-label">保留首条记录</span>
+                </el-radio>
+                <p class="strategy-0ription">
+                    系统将根据去重条件保留满足去重规则记录中的第一条记录。
+                </p>
+                <el-radio label="2" class="radio-item">
+                    <span class="radio-label">保留最新记录</span>
+                </el-radio>
+                <p class="strategy-0ription">
+                    系统将根据去重条件保留满足去重规则记录中的最新记录。
+                </p>
+            </el-radio-group>
+        </el-form-item>
+    </el-form>
+
+
+</template>
+
+<script setup name="columnsCombiner">
+import Sortable from "sortablejs";
+const props = defineProps({
+    form: Object,
+    inputFields: Array,
+    falg: Boolean,
+});
+const form = reactive({ ...props.form });
+const exposedcolumnss = ['stringValue', 'handleType',];
+const data = Object.fromEntries(exposedcolumnss.map(key => [key, form[key]]));// 添加排序字段,默认排序顺序为降序
+
+let dragTable = ref(null);
+let sortableInstance = null;
+function setSort() {
+    nextTick(() => {
+        const tbody = dragTable.value?.$el.querySelector(
+            ".el-table__body-wrapper tbody"
+        );
+        if (!tbody) {
+            console.warn("tbody 找不到,拖拽初始化失败");
+            return;
+        }
+
+        if (sortableInstance) {
+            sortableInstance.destroy();
+        }
+
+        sortableInstance = Sortable.create(tbody, {
+            handle: ".allowDrag",
+            animation: 150,
+            onEnd: (evt) => {
+                const movedItem = form.stringValue.splice(evt.oldIndex, 1)[0];
+                form.stringValue.splice(evt.newIndex, 0, movedItem);
+                console.log("拖拽后顺序:", form.stringValue.map((f) => f.sort));
+            },
+        });
+    });
+} const addtypecolumns = () => {
+    form.stringValue.push({
+        sort: form.stringValue.length,
+        columns: '', // 字段名称
+        type: '0', // 默认降序
+    });
+    setSort()
+};
+// 删除排序字段
+const handleDeletetypecolumns = (index) => {
+    form.stringValue.splice(index, 1);
+    setSort()
+};
+// 判断字段是否已被其他行选择,禁用重复选项
+const iscolumnsDisabled = (columnsName, currentRowId) => {
+    return form.stringValue.some(
+        (item) => item.columns === columnsName && item.id !== currentRowId
+    );
+};
+const formRef = ref(null);
+function validate() {
+    return new Promise((resolve) => {
+        formRef.value.validate((valid) => {
+            if (!valid) {
+                resolve({ valid: false });
+                return;
+            }
+
+            // 如果没有添加排序字段,直接通过
+            if (!form.stringValue || form.stringValue.length === 0) {
+                resolve({
+                    valid: true,
+                    data,
+                });
+                return;
+            }
+
+            // 校验每个字段名称非空
+            for (const item of form.stringValue) {
+                if (!item.columns) {
+                    ElMessage.error('排序字段名称不能为空');
+                    resolve({ valid: false });
+                    return;
+                }
+            }
+
+            // 校验字段名称不重复
+            const columnss = form.stringValue.map(item => item.columns);
+            const hasDuplicate = new Set(columnss).size !== columnss.length;
+            if (hasDuplicate) {
+                ElMessage.error('排序字段名称不能重复');
+                resolve({ valid: false });
+                return;
+            }
+
+            // 只有数组有值才更新 sort
+            if (form.stringValue && form.stringValue.length > 0) {
+                form.stringValue.forEach((item, index) => {
+                    item.sort = index + 1;
+                });
+            }
+
+            resolve({
+                valid: true,
+                data,
+            });
+        });
+    });
+}
+
+setSort()
+defineExpose({ validate });
+</script>
+
+<style scoped lang="scss">
+.deduplication-config {
+    padding-left: 57px;
+}
+</style>

+ 19 - 0
ruoyi-ui/src/views/dpp/task/integratioTask/components/clean/rule/emptyRule.vue

@@ -0,0 +1,19 @@
+<template>
+    <div></div>
+</template>
+
+<script setup>
+defineProps({
+    form: Object,
+    falg: Boolean,
+    inputFields: Array,
+    columnList: Array
+});
+
+// 占位组件校验,始终返回通过
+const validate = async () => {
+    return { valid: true, data: {} };
+};
+
+defineExpose({ validate });
+</script>

+ 338 - 0
ruoyi-ui/src/views/dpp/task/integratioTask/components/clean/rule/enumMapRule/dataElem.vue

@@ -0,0 +1,338 @@
+<template>
+    <el-dialog :title="title" v-model="visible" class="medium-dialog max-dialogs-status0" draggable width="90%">
+        <div class="flex-row">
+            <!-- 左侧树 -->
+            <div class="left-col">
+                <DeptTree :deptOptions="deptOptions" :leftWidth="leftWidth" placeholder="请输入数据元类目"
+                    @node-click="handleNodeClick" ref="DeptTreeRef" :showFilter="false" :show-background="false"
+                    style="height: 650px;" />
+            </div>
+
+            <!-- 分隔线 -->
+            <div class="divider"></div>
+
+            <!-- 右侧表格 + 分页 -->
+            <div class="content-col" v-loading="loading">
+                <!-- 表格 -->
+                <el-table :data="dpDataElemList" stripe @row-click="handleRowClick" :highlight-current-row="true"
+                    ref="tableRef" border height="62vh">
+                    <el-table-column v-if="getColumnVisibility(0)" label="编号" align="left" prop="id" width="80" />
+                    <el-table-column v-if="getColumnVisibility(1)" label="中文名称"
+                        :show-overflow-tooltip="{ effect: 'light' }" width="80" align="left" prop="name">
+                        <template #default="scope">{{ scope.row.name || "-" }}</template>
+                    </el-table-column>
+                    <el-table-column v-if="getColumnVisibility(2)" label="英文名称"
+                        :show-overflow-tooltip="{ effect: 'light' }" width="80" align="left" prop="engName">
+                        <template #default="scope">{{ scope.row.engName || "-" }}</template>
+                    </el-table-column>
+                    <el-table-column v-if="getColumnVisibility(3)" label="类型" align="left" prop="type">
+                        <template #default="scope">{{ typeFormat(scope.row) }}</template>
+                    </el-table-column>
+                    <el-table-column v-if="getColumnVisibility(6)" width="140" label="元描述" align="left"
+                        prop="description" :show-overflow-tooltip="{ effect: 'light' }">
+                        <template #default="scope">{{ scope.row.description || "-" }}</template>
+                    </el-table-column>
+                    <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right"
+                        width="240">
+                        <template #default="scope">
+                            <el-button link type="primary" icon="view" @click="showDialog(scope.row)"
+                                v-hasPermi="['dp:dataElem:dataelem:edit']">查看
+                            </el-button>
+                        </template>
+                    </el-table-column>
+                    <template #empty>
+                        <div class="emptyBg">
+                            <img src="../../../../../../../../assets/system/images/no_data/noData.png" alt="" />
+                            <p>无数据</p>
+                        </div>
+                    </template>
+                </el-table>
+                <!-- 分页 -->
+                <div class="pagination-wrapper" style="margin-top: 10px; text-align: right;">
+                    <pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize"
+                        @pagination="getList" />
+                </div>
+            </div>
+        </div>
+
+        <!-- 底部按钮 -->
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="handleCancel">取 消</el-button>
+                <el-button type="primary" @click="handleConfirm" :disabled="!selectedRow" :loading="loading">
+                    保 存
+                </el-button>
+            </div>
+        </template>
+        <CodeValueInput ref="dialogRef" @confirm="handleConfirm" />
+    </el-dialog>
+</template>
+
+<script setup>
+import { ref, reactive } from "vue";
+const emit = defineEmits(["confirm"]);
+import DeptTree from '@/components/DeptTree/tree.vue';
+import {
+    listDpDataElem,
+} from "@/api/dp/dataElem/dataElem.js";
+import {
+    listDpDataElemCode,
+} from '@/api/dp/dataElem/dataElem.js';
+import { deptUserTree } from "@/api/system/system/user.js";
+import { listAttDataElemCat } from "@/api/att/cat/dataElemCat/dataElemCat.js";
+const { proxy } = getCurrentInstance();
+const { dp_data_elem_code_type } = proxy.useDict(
+    "dp_data_elem_code_type"
+);
+import CodeValueInput from "./dataElemDetail.vue";
+const deptOptions = ref(undefined);
+const leftWidth = ref(240); // 初始左侧宽度
+const isResizing = ref(false); // 判断是否正在拖拽
+let startX = 0; // 鼠标按下时的初始位置// 初始左侧宽度
+/** 类型字典翻译 */
+function typeFormat(row) {
+    return proxy.selectDictLabel(dp_data_elem_code_type.value, row.type);
+}
+
+const dpDataElemList = ref([]);
+const dpDataElemRuleRelList = ref([]);
+
+// 列显隐信息
+const columns = ref([
+    { key: 1, label: "中文名称", visible: true },
+    { key: 2, label: "英文名称", visible: true },
+    { key: 3, label: "类型", visible: true },
+    { key: 4, label: "数据元类目", visible: true },
+    { key: 5, label: "当前状态", visible: true },
+    { key: 6, label: "元描述", visible: true },
+]);
+const dialogRef = ref();
+function handleQuery() {
+    queryParams.value.pageNum = 1;
+    getList();
+}
+function showDialog(row) {
+    dialogRef.value.openDialog(row);
+}
+const getColumnVisibility = (key) => {
+    const column = columns.value.find((col) => col.key === key);
+    // 如果没有找到对应列配置,默认显示
+    if (!column) return true;
+    // 如果找到对应列配置,根据visible属性来控制显示
+    return column.visible;
+};
+
+const open = ref(false);
+const loading = ref(true);
+const ids = ref([]);
+const checkedDpDataElemRuleRel = ref([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref("");
+const data = reactive({
+    form: { status: "0" },
+    queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        name: null,
+        engName: null,
+        catCode: null,
+        type: 2,
+    },
+
+});
+
+const { queryParams, form, } = toRefs(data);
+const managerOptions = ref([]);
+/** 查询数据元列表 */
+function getList() {
+    loading.value = true;
+    listDpDataElem(queryParams.value).then((response) => {
+        dpDataElemList.value = response.data.rows;
+        total.value = response.data.total;
+        loading.value = false;
+    });
+    deptUserTree().then((response) => {
+        managerOptions.value = response.data;
+    });
+}
+// 树组件 传值
+function handleNodeClick(data) {
+    queryParams.value.catCode = data.code;
+    handleQuery();
+}
+
+// 表单重置
+function reset() {
+    form.value = {
+        id: null,
+        code: null,
+        name: null,
+        engName: null,
+        catCode: null,
+        type: "1",
+        personCharge: null,
+        contactNumber: null,
+        columnType: null,
+        status: "0",
+        description: null,
+        validFlag: null,
+        delFlag: null,
+        createBy: null,
+        creatorId: null,
+        createTime: null,
+        updateBy: null,
+        updaterId: null,
+        updateTime: null,
+        remark: null,
+    };
+    dpDataElemRuleRelList.value = [];
+    proxy.resetForm("dpDataElemRef");
+}
+
+
+const DeptTreeRef = ref(null);
+/** 重置按钮操作 */
+function resetQuery() {
+    if (DeptTreeRef.value?.resetTree) {
+        DeptTreeRef.value.resetTree();
+    }
+    queryParams.value.catCode = "";
+    queryParams.value.pageNum = 1;
+    selectedRow.value = null;
+    reset();
+    proxy.resetForm("queryRef");
+}
+
+function getDeptTree() {
+    listAttDataElemCat({ validFlag: true }).then((response) => {
+        deptOptions.value = proxy.handleTree(response.data, "id", "parentId");
+        deptOptions.value = [
+            {
+                name: "数据元类目",
+                value: "",
+                id: 0,
+                children: deptOptions.value,
+            },
+        ];
+    });
+}
+
+
+const visible = ref(false);
+/**
+ * 打开弹窗
+ * @param {String} dialogTitle 弹窗标题
+ */
+function openDialog(dialogTitle = "选择数据") {
+    title.value = dialogTitle;
+    visible.value = true;
+    getDeptTree();
+    getList();
+
+}
+const selectedRow = ref(null)
+const tableRef = ref(null)
+function handleRowClick(row) {
+    selectedRow.value = row
+    if (tableRef.value) {
+        tableRef.value.setCurrentRow(row)  // 高亮选中
+    }
+    console.log('选中行数据:', row)
+}
+/**
+ * 取消
+ */
+function handleCancel() {
+    visible.value = false;
+    if (tableRef.value) {
+        tableRef.value.setCurrentRow(null); // 清除表格选中行高亮
+    }
+    resetQuery()
+
+
+
+}
+async function ElemCode(id) {
+    if (id === -1) {
+        return [];
+    }
+    loading.value = true;
+    try {
+        const response = await listDpDataElemCode({
+            pageNum: 1,
+            pageSize: 999,
+            id,
+        });
+        return response.data.rows || [];
+    } catch (error) {
+        console.error('请求失败', error);
+        return [];
+    } finally {
+        loading.value = false;
+    }
+}
+
+/**
+ * 保存
+ */
+async function handleConfirm() {
+    if (!selectedRow.value) {
+        proxy.$modal.msgWarning('请选择一条记录');
+        return;
+    }
+    // const list = await ElemCode(selectedRow.value.id);
+    emit('confirm', selectedRow.value,);
+    resetQuery()
+    visible.value = false;
+}
+
+
+
+defineExpose({ openDialog });
+</script>
+<style scoped lang="scss">
+.flex-row {
+    display: flex;
+    height: 71vh;
+}
+
+.left-col {
+    width: 250px;
+    overflow-y: auto;
+}
+
+.divider {
+    width: 1px;
+    background-color: #dcdfe6;
+    margin: 0 20px;
+    height: 700px;
+}
+
+.content-col {
+    margin-top: 50px;
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    padding-right: 20px;
+}
+
+.el-table {
+    flex: none;
+    /* 不占满父容器 */
+}
+
+.pagination-wrapper {
+    margin-top: 10px;
+    text-align: right;
+}
+
+.emptyBg {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 20px;
+}
+</style>

+ 137 - 0
ruoyi-ui/src/views/dpp/task/integratioTask/components/clean/rule/enumMapRule/dataElemDetail.vue

@@ -0,0 +1,137 @@
+<template>
+    <el-dialog title="详请" v-model="visible" width="800px" draggable>
+        <el-table stripe height="65vh" v-loading="loading" :data="dpDataElemCodeList" :default-sort="defaultSort">
+            <el-table-column label="编号" align="left" prop="id" width="80" />
+            <el-table-column label="代码值" align="left" prop="codeValue" width="160">
+                <template #default="scope">
+                    {{ scope.row.codeValue || '-' }}
+                </template>
+            </el-table-column>
+            <el-table-column label="代码名称" align="left" prop="codeName" width="350">
+                <template #default="scope">
+                    {{ scope.row.codeName || '-' }}
+                </template>
+            </el-table-column>
+            <el-table-column label="创建人" align="left" prop="createBy" width="160">
+                <template #default="scope">
+                    {{ scope.row.createBy || '-' }}
+                </template>
+            </el-table-column>
+            <el-table-column label="创建时间" align="left" prop="createTime" width="220">
+                <template #default="scope">
+                    <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
+                </template>
+            </el-table-column>
+            <el-table-column label="备注" align="left" prop="remark" width="360"
+                :show-overflow-tooltip="{ effect: 'light' }">
+                <template #default="scope">
+                    {{ scope.row.remark || '-' }}
+                </template>
+            </el-table-column>
+            <template #empty>
+                <div class="emptyBg">
+                    <img src="../../../../../../../../assets/system/images/no_data/noData.png" alt="" />
+                    <p>无数据</p>
+                </div>
+            </template>
+        </el-table>
+
+        <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
+            v-model:limit="queryParams.pageSize" @pagination="getList" />
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button size="mini" @click="handleClose">关 闭</el-button>
+            </div>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup name="ComponentOne">
+import {
+    listDpDataElemCode,
+    validateCodeValue
+} from '@/api/dp/dataElem/dataElem.js';
+const route = useRoute();
+const { proxy } = getCurrentInstance();
+
+const dpDataElemCodeList = ref([]);
+
+const open = ref(false);
+const openDetail = ref(false);
+const loading = ref(true);
+const total = ref(0);
+const defaultSort = ref({ prop: 'createTime', order: 'desc' });
+
+const data = reactive({
+    form: {},
+    queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        dataElemId: null,
+        codeValue: null,
+        codeName: null,
+        createTime: null
+    },
+
+});
+
+let id = route.query.id;
+
+const { queryParams, form } = toRefs(data);
+
+
+
+/** 查询数据元代码列表 */
+function getList(id) {
+    if (id == -1) {
+        return;
+    }
+    loading.value = true;
+    queryParams.value.dataElemId = id;
+    listDpDataElemCode(queryParams.value).then((response) => {
+        dpDataElemCodeList.value = response.data.rows;
+        total.value = response.data.total;
+        loading.value = false;
+    });
+}
+
+// 取消按钮
+function cancel() {
+    open.value = false;
+    openDetail.value = false;
+    reset();
+}
+
+// 表单重置
+function reset() {
+    form.value = {
+        id: null,
+        dataElemId: null,
+        codeValue: null,
+        codeName: null,
+        validFlag: null,
+        delFlag: null,
+        createBy: null,
+        creatorId: null,
+        createTime: null,
+        updateBy: null,
+        updaterId: null,
+        updateTime: null,
+        remark: null
+    };
+    proxy.resetForm('dpDataElemCodeRef');
+}
+
+const visible = ref(false);
+
+function openDialog(row) {
+    visible.value = true;
+    getList(row.id);
+
+}
+function handleClose() {
+    visible.value = false;
+    reset();
+}
+defineExpose({ openDialog });
+</script>

+ 203 - 0
ruoyi-ui/src/views/dpp/task/integratioTask/components/clean/rule/enumMapRule/index.vue

@@ -0,0 +1,203 @@
+<template>
+    <el-form ref="formRef" :model="form" label-width="130px" :disabled="falg">
+        <div v-loading="loadingList">
+            <div class="justify-between mb15">
+                <el-row :gutter="15" class="btn-style">
+                    <el-col :span="1.5">
+                        <el-button type="primary" icon="Plus" @click="opencodeDialog(undefined)">新增规则</el-button>
+                    </el-col>
+                    <el-col :span="1.5">
+                        <el-button type="primary" icon="Plus" @click="showDialog(undefined)">导入规则</el-button>
+                    </el-col>
+                </el-row>
+            </div>
+            <el-table stripe :data="form.stringValue" v-loading="loading">
+                <el-table-column label="原值" align="left" prop="value">
+                    <template #default="scope">
+                        <el-input v-model="scope.row.value" style="width: 100%" placeholder="请输入原值" />
+                    </template>
+                </el-table-column>
+
+                <el-table-column label="标准值" align="left" prop="name">
+                    <template #default="scope">
+                        <el-input v-model="scope.row.name" style="width: 100%" placeholder="请输入标准值" />
+                    </template>
+                </el-table-column>
+
+                <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right"
+                    width="150">
+                    <template #default="scope">
+                        <el-button link type="danger" icon="Delete"
+                            @click="handleDelete(scope.$index + 1)">删除</el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+        </div>
+        <el-row>
+        </el-row>
+        <singleSelectTableDialog ref="dialogRef" @confirm="handleConfirm" />
+    </el-form>
+
+</template>
+
+<script setup>
+import { reactive, ref, watch } from "vue";
+import {
+    listDpDataElem,
+    listDpDataElemCode,
+} from "@/api/dp/dataElem/dataElem.js";
+import singleSelectTableDialog from "./dataElem.vue";
+const props = defineProps({
+    form: Object,
+    inputFields: Array,
+    falg: Boolean,
+});
+let loadingList = ref(false);
+const emit = defineEmits(["update:form"]);
+let loading = ref(false);
+const formRef = ref(null);
+const { proxy } = getCurrentInstance();
+const form = reactive({ ...props.form, });
+let dpDataElemstringValue = ref([])
+let dpDataElemList = ref([])
+
+const dialogRef = ref();
+
+function showDialog() {
+    dialogRef.value.openDialog("选择数据元");
+}
+
+function handleConfirm(row, list) {
+    console.log("选中行:", row);
+    dpDataElemstringValue.value = []
+
+    loadCodeItemsByTableId(row.id)
+}
+function loadCodeItemsByTableId(id) {
+    if (!id || id == -1) return;
+    loading.value = true;
+    listDpDataElemCode({
+        pageNum: 1,
+        pageSize: 999,
+        dataElemId: id,
+        ruleType: 2
+    }).then((res) => {
+        // dpDataElemstringValue.value = res.data.rows;
+        form.stringValue = (res?.data?.rows || []).map(({ codeValue, codeName, ...rest }) => ({
+            ...rest,
+            value: codeValue ?? '',
+            name: codeName ?? ''
+        }));
+        loading.value = false;
+    });
+}
+function handleDelete(index) {
+    form.stringValue.splice(Number(index) - 1, 1);
+}
+function opencodeDialog() {
+    const hasIncomplete = (form.stringValue || []).some(item =>
+        !item.value || !item.name
+    );
+
+    if (hasIncomplete) {
+        ElMessage.warning('请先填写完整所有项');
+        return;
+    }
+
+    if (!Array.isArray(form.stringValue)) {
+        form.stringValue = [];
+    }
+
+    // 新增一行空数据
+    form.stringValue.push({
+        value: '',
+        name: '',
+    });
+}
+
+function loadCodeTableList() {
+
+    listDpDataElem({
+        pageNum: 1,
+        pageSize: 999,
+        type: '2'
+
+    }).then((res) => {
+        dpDataElemList.value = res.data.rows;
+        loading.value = false;
+    }).catch(() => {
+        loading.value = false;
+    });
+}
+function handleUseCodeTableChange(val) {
+    if (val == '1') {
+        loadCodeTableList();
+    } else {
+        form.codeTableId = '';
+        form.stringValue = [];
+        dpDataElemList.value = [];
+    }
+}
+
+onMounted(() => {
+    if (form.useCodeTable === '1' && form.codeTableId) {
+        handleUseCodeTableChange('1', true);
+    }
+});
+function checkValueAndName(list) {
+    if (!list || list.length === 0) {
+        return { formIsValid: false, message: '至少需要添加一条规则数据!' };
+    }
+    const values = [];
+    const names = [];
+    for (const item of list) {
+        const v = item.value?.trim();
+        const n = item.name?.trim();
+        if (!v || !n) {
+            return { formIsValid: false, message: '原值和标准值不能为空!' };
+        }
+        values.push(v);
+        names.push(n);
+    }
+
+    const hasDuplicate = arr => arr.some((val, idx) => arr.indexOf(val) !== idx);
+
+    if (hasDuplicate(values)) {
+        return { formIsValid: false, message: '原值不能重复!' };
+    }
+    if (hasDuplicate(names)) {
+        return { formIsValid: false, message: '标准值不能重复!' };
+    }
+
+    return { formIsValid: true, message: '' };
+}
+
+function validate() {
+    return new Promise((resolve) => {
+        formRef.value.validate((valid) => {
+            if (!valid) {
+                resolve({ valid: false });
+                return;
+            }
+            const { formIsValid, message } = checkValueAndName(form.stringValue);
+            if (!formIsValid) {
+                proxy.$message.warning(message);
+                resolve({ valid: false });
+                return;
+            }
+            const result = {
+                stringValue: form.stringValue,
+            };
+
+            resolve({ valid: true, data: result });
+        });
+    });
+}
+
+
+
+
+
+defineExpose({ validate });
+</script>
+<style scoped></style>

+ 93 - 0
ruoyi-ui/src/views/dpp/task/integratioTask/components/clean/rule/numberBoundaryRule.vue

@@ -0,0 +1,93 @@
+<template>
+<!--  数值边界调整  -->
+    <el-form ref="formRef" :model="form" label-width="130px" :disabled="falg">
+        <el-row>
+            <el-col :span="12">
+                <el-form-item label="最小值" prop="min" :rules="[{ required: true, message: '请输入最小值', trigger: 'blur' }]">
+                    <el-input v-model="form.min" placeholder="不填写表示不限制最小值" type="number" style="width: 290px;" />
+                </el-form-item>
+            </el-col>
+            <el-col :span="12">
+                <el-form-item label="最大值" prop="max" :rules="[{ required: true, message: '请输入最大值', trigger: 'blur' }]">
+                    <el-input v-model="form.max" placeholder="不填写表示不限制最大值" type="number" style="width: 290px;" />
+                </el-form-item>
+            </el-col>
+        </el-row>
+        <el-row>
+            <el-col :span="24" class="hasMsg">
+                <el-form-item label="处理方式" prop="handleType"
+                    :rules="[{ required: true, message: '请选择处理方式', trigger: 'blur' }]">
+                    <el-radio-group v-model="form.handleType">
+                        <el-radio :value="'3'">超出最大值时调整为最大值</el-radio>
+                        <el-radio :value="'2'">超出最小值时调整为最小值</el-radio>
+                        <el-radio :value="'1'">两种情况都调整到对应的边界值</el-radio>
+                    </el-radio-group>
+                    <div class="msg">
+                        <div v-for="(msg, index) in boundaryExamples" :key="index">
+                            <el-icon>
+                                <InfoFilled />
+                            </el-icon>
+                            <span>{{ msg }}</span>
+                        </div>
+                    </div>
+                </el-form-item>
+            </el-col>
+        </el-row>
+    </el-form>
+</template>
+
+<script setup>
+import { reactive, ref, watch } from "vue";
+const props = defineProps({
+    form: Object,
+    inputFields: Array,
+    falg: Boolean,
+});
+
+const emit = defineEmits(["update:form"]);
+
+const formRef = ref(null);
+
+const form = reactive({ ...props.form });
+const boundaryExamples = computed(() => {
+    switch (form.handleType) {
+        case '3':
+            return ['示例: 如果年龄 > 150,则设置为 150。'];
+        case '2':
+            return ['示例: 如果收入 < 1000,则设置为 1000。'];
+        case '1':
+            return [
+                '示例1: 如果年龄 > 150,则设置为 150。如果收入 < 1000,则设置为 1000。',
+            ];
+        default:
+            return [];
+    }
+});
+const loading = ref(false);
+const exposedFields = [
+    "min",
+    "max",
+    "handleType"
+];
+function validate() {
+    return new Promise((resolve) => {
+        formRef.value.validate((valid) => {
+            if (valid) {
+                const data = Object.fromEntries(exposedFields.map(key => [key, form[key]]));
+                resolve({
+                    valid: true,
+                    data,
+                });
+            } else {
+                resolve({ valid: false });
+            }
+        });
+    });
+}
+
+
+
+
+defineExpose({ validate });
+</script>
+<style scoped></style>

+ 366 - 0
ruoyi-ui/src/views/dpp/task/integratioTask/components/clean/rule/ruleBase.vue

@@ -0,0 +1,366 @@
+<template>
+    <!-- 清洗规则基础页面   -->
+    <el-dialog v-model="dialogVisible" draggable class="medium-dialog"
+        :class="{ 'max-dialogs-status0': dialogStatus === 0 }" :title="dialogTitle" destroy-on-close
+        :append-to="$refs['app-container']">
+        <div class="content" v-if="dialogStatus == 0">
+            <SideMenu :dialogStatus="dialogStatus" @card-click="handleCardClick" ref="SideMenus" :type="type" />
+        </div>
+        <div class="content" style="height: 600px; padding-right: 10px;" v-show="dialogStatus == 1 || dialogStatus == 2"
+            :disabled="dialogStatus == 2">
+            <el-form ref="formRef" :model="form" label-width="130px">
+                <div class="h2-title">基础信息</div>
+                <el-row>
+                    <el-col :span="8">
+                        <el-form-item label="清洗名称" prop="name"
+                            :rules="[{ required: true, message: '请输入清洗名称名称', trigger: 'blur' }]">
+                            <el-input v-model="form.name" placeholder="请输入清洗名称" :disabled="falg" />
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="8">
+                        <el-form-item label="清洗规则编号" prop="ruleCode">
+                            <el-input v-model="form.ruleCode" placeholder="请输入清洗规则编号" disabled />
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="8">
+                        <el-form-item label="清洗规则名称" prop="ruleName">
+                            <el-input v-model="form.ruleName" placeholder="请输入清洗规则名称" disabled />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row>
+                    <el-col :span="8">
+                        <el-form-item label="状态" prop="status">
+                            <el-radio-group v-model="form.status" :disabled="falg">
+                                <el-radio :value="'1'">上线</el-radio>
+                                <el-radio :value="'0'">下线</el-radio>
+                            </el-radio-group>
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row>
+                    <el-col :span="24">
+                        <el-form-item label="规则描述" prop="ruleDesc">
+                            <el-input type="textarea" v-model="form.ruleDesc" placeholder="请输入规则描述" :disabled="falg" />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row>
+                    <el-col :span="24">
+                        <el-form-item label="Where 条件" prop="whereClause">
+                            <el-input type="textarea" v-model="form.whereClause" placeholder="请输入 Where 条件"
+                                :disabled="falg" />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <!-- 规则配置 -->
+                <div class="h2-title">规则配置</div>
+                <el-row v-if="type != 3">
+                    <el-col :span="24">
+                        <el-form-item label="清洗字段" prop="columns" :disabled="falg"
+                            :rules="[{ required: true, message: '请选择清洗字段', trigger: 'blur' }]">
+                            <el-select v-if="isMultipleSelect" v-model="form.columns" placeholder="请选择清洗字段" multiple
+                                clearable>
+                                <el-option v-for="dict in processedFields" :key="dict.columnName" :label="dict.label"
+                                    :value="dict.columnName" />
+                            </el-select>
+                            <!-- 单选 -->
+                            <el-select v-else v-model="form.columns" placeholder="请选择清洗字段" clearable>
+                                <el-option v-for="dict in processedFields" :key="dict.columnName" :label="dict.label"
+                                    :value="dict.columnName" :disabled="form?.ruleCode == '039' && !(
+                                        dict.columnType?.toUpperCase().includes('DATE') ||
+                                        dict.columnType?.toUpperCase().startsWith('TIMESTAMP') ||
+                                        dict.columnType?.toUpperCase() === 'TIME' ||
+                                        dict.columnType?.toUpperCase() === 'YEAR'
+                                    )
+                                        " />
+
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <component :is="currentRuleComponent" ref="ruleComponentRef" :form="form.ruleConfig"
+                    :inputFields="processedFields" :falg="falg" :columnList="columnList" />
+            </el-form>
+        </div>
+        <template #footer>
+            <template v-if="dialogStatus == 1"><el-button type="primary" @click="handleSave" v-if="!falg">确定</el-button>
+                <el-button @click="handleBack" v-if="!mode">返回</el-button>
+                <!-- <el-button type="warning" @click="handleSpotCheck">预览</el-button> -->
+            </template>
+            <el-button @click="closeDialog" v-else>关闭</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import SideMenu from "./ruleSelectorMenu.vue";
+// 数值边界值调整
+import NumberRangeSelector from "./numberBoundaryRule.vue";
+// 字段前缀/后缀统一
+import AffixEditor from "./affixEditorRule.vue";
+// 枚举值映射标准化
+import EnumRule from "./enumMapRule/index.vue";
+// 按组合字段去重
+import FieldCombiner from "./combinerFieldUniqueRule.vue";
+
+// 站位组件
+import EmptyRule from "./emptyRule.vue";
+import moment from "moment";
+let falg = ref(false)
+const { proxy } = getCurrentInstance();
+const { quality_warning_status, } =
+    proxy.useDict(
+        "quality_warning_status",
+    );
+const emit = defineEmits(["confirm"]);
+// 父组件传入评测对象列表
+const props = defineProps({
+    inputFields: {
+        type: Array,
+        default: () => [],
+    },
+    type: {
+        type: String,
+        default: ''
+    },
+});
+
+const { inputFields } = toRefs(props);
+const processedFields = computed(() => {
+    return inputFields.value.map(item => ({
+        ...item,
+        label: item.columnComment
+            ? `${item.columnName} / ${item.columnComment}`
+            : item.columnName
+    }))
+})
+
+const dialogVisible = ref(false);
+const dialogStatus = ref(1);
+const dialogTitle = ref("");
+const formRef = ref();
+
+let form = reactive({
+    name: '',
+    ruleName: "",//清洗规则名称:
+    ruleCode: "",//清洗规则编号:
+    status: "1",
+    // warningLevel: "2",
+    whereClause: "",
+    columns: '',
+    tableName: "",
+    ruleDesc: "",
+    type: '',
+    ruleConfig: {
+        //数值边界调整
+        max: '100',
+        min: "0",
+        handleType: "1",
+        // 去除字符串空格
+        handleType: "1",//"1-去除前后空格,2-去除所有空格"
+        // 正则表达式替换
+        pattern: "",//表达式
+        replacement: "",//replacement
+        ruleValue: [],
+        deduplicationStrategy: "1",
+        dataRangeValue: moment().format("YYYY-MM-DD"),
+        // 数据添加值
+        stringValue: "",//添加值
+
+    }
+});
+const isMultipleSelect = computed(() => {
+    return form.ruleCode == '019' || form.ruleCode == '029';
+})
+let title = ref()
+const ruleConfigMap = {
+    // 数值范围
+    "001": {
+        label: "数值边界值调整",
+        component: NumberRangeSelector,
+    },
+    //
+    "010": {
+        label: "字段前缀/后缀统一",
+        component: AffixEditor,
+    },
+    "019": {
+        label: "组合字段为空删除",
+        component: EmptyRule,
+    },
+
+    "024": {
+        label: "枚举值映射标准化",
+        component: EnumRule,
+    },
+    "029": {
+        label: "按组合字段去重",
+        component: FieldCombiner,
+    },
+
+};
+
+// 计算属性:当前规则配置
+const currentRuleConfig = computed(() => {
+    return ruleConfigMap[form.ruleCode] || null;
+});
+
+// 计算属性:当前规则组件
+const currentRuleComponent = computed(() => {
+    return currentRuleConfig.value?.component || null;
+});
+
+let loading = ref(false);
+let columnList = ref([]);
+
+let ruleComponentRef = ref()
+async function handleSave() {
+    await nextTick();
+    try {
+        await formRef?.value?.validate();
+    } catch (err) {
+        proxy.$message.warning("请完善必填项");
+        return;
+    }
+    let res = { valid: true, data: {} };
+    res = await ruleComponentRef.value?.validate();
+    if (!res.valid) return;
+    if (!isMultipleSelect.value) {
+        form.columns = [form.columns]
+
+    }
+    if (form.ruleCode == '035') {
+
+    }
+    const formCopy = JSON.parse(JSON.stringify({
+        ...form,
+        ruleConfig: JSON.stringify({
+            columns: form.columns,
+            ...res.data,
+            parentName: form.parentName
+        }),
+    }));
+
+    emit('confirm', formCopy, mode.value);
+}
+let sampleCheckMsg = ref()
+
+function handleCardClick(data) {
+    resetForm()
+    form.ruleName = data?.name;
+    form.ruleCode = data?.code;
+    form.ruleType = data?.strategyKey;
+    form.type = data?.type;
+    form.parentName = data?.parentName;
+    form.dimensionType = data?.qualityDim
+    console.log("🚀 ~ handleCardClick ~ data:", data)
+    console.log("🚀 ~ handleCardClick ~  form.dimensionType:", form.dimensionType)
+    dialogTitle.value = `新增清洗规则${data?.name ? '-' + data.name : ''}`
+    dialogStatus.value = 1;
+}
+let mode = ref();
+async function openDialog(record, index, fg) {
+    falg.value = fg;
+    mode.value = index;
+    resetForm();
+    dialogTitle.value = `${mode.value ? '修改' : '新增'}清洗规则${record?.ruleName ? `-${record.ruleName}` : ''}`;
+    if (falg?.value) {
+        dialogTitle.value = `清洗规则${record?.ruleName ? `-${record.ruleName}` : ''}`;
+    }
+    dialogStatus.value = mode.value ? 1 : 0;
+    dialogVisible.value = true;
+
+    if (index) {
+        dialogStatus.value = 1;
+        const { ruleType, ruleConfig, columns, ...rest } = record;
+        Object.assign(form, rest);
+        form.ruleType = ruleType;
+
+        try {
+            form.ruleConfig = typeof ruleConfig == 'string' ? JSON.parse(ruleConfig) : ruleConfig;
+        } catch (e) {
+            form.ruleConfig = {};
+        }
+        if (isMultipleSelect.value) {
+            form.columns = Array.isArray(columns) ? columns : [];
+        } else {
+            form.columns = Array.isArray(columns) && columns.length > 0 ? columns[0] : '';
+        }
+    } else {
+        resetForm();
+    }
+}
+
+const initialForm = () => ({
+    id: '',
+    name: '',
+    type: '',
+    ruleName: "",//清洗规则名称:
+    ruleCode: "",//清洗规则编号:
+    status: "1",
+    whereClause: "",
+    columns: isMultipleSelect.value ? [] : '',
+    tableName: "",
+    ruleDesc: "",
+    ruleConfig: {
+
+        //数值边界调整
+        max: '100',
+        min: "0",
+        handleType: "1",
+        // 去除字符串空格
+        handleType: "1",//"1-去除前后空格,2-去除所有空格"
+        // 正则表达式替换
+        pattern: "",//表达式
+        replacement: "",//replacement
+
+        ruleValue: [],
+        deduplicationStrategy: "1",
+        // 枚举值映射标准化
+        stringValue: [],
+        dataRange: "1",// 0:固定时间范围,1:具体日期
+        dataRangeType: "1",// 0:天前
+        dataRangeValue: moment().format("YYYY-MM-DD"),
+        handleType: "1",// 0:过期处理方式,1:删除记录
+        handleColumns: "",// // 标记字段     选中过期处理方式才会有
+        handleValue: "",// 标记值       选中过期处理方式才会有
+
+    }
+});
+
+function resetForm() {
+    Object.assign(form, initialForm());
+    columnList.value = [];
+    title.value = ''
+    sampleCheckMsg.value = ''
+}
+
+function closeDialog() {
+    dialogVisible.value = false;
+    resetForm();
+}
+
+function handleBack() {
+    dialogStatus.value = 0;
+    dialogTitle.value = `新增评测规则`
+    resetForm()
+}
+defineExpose({ openDialog, closeDialog })
+</script>
+
+<style scoped>
+.blue-text {
+    color: var(--el-color-primary);
+}
+
+.medium-dialog {
+    width: 800px;
+}
+</style>
+<style>
+.el-dialog.max-dialogs-status0 .el-dialog__body {
+    padding: 0 !important;
+    padding-left: 10px !important;
+}
+</style>

+ 295 - 0
ruoyi-ui/src/views/dpp/task/integratioTask/components/clean/rule/ruleSelectorMenu.vue

@@ -0,0 +1,295 @@
+<template>
+    <el-row>
+        <el-col :span="5">
+            <DeptTree :deptOptions="processedData" :leftWidth="leftWidth" :placeholder="'请输入规则类型'"
+                @node-click="handleNodeClick" ref="DeptTreeRef" :showFilter="false" />
+        </el-col>
+        <div class="divider"></div>
+        <el-col :span="18" class="content-col" v-loading="loading">
+            <div class="content" ref="contentWrapper">
+                <el-row>
+                    <div class="cards-wrapper">
+                        <template v-if="attCleanRuleList.length">
+                            <div v-for="data in attCleanRuleList" :key="data.id" class="card-item"
+                                :class="{ selected: selectedCard?.id === data.id }" @click="cardClick(data)">
+                                <el-card class="box-card boxCard" shadow="never" :body-style="{ padding: '15px' }">
+                                    <div class="card-icon" :class="{ 'is-disabled': data.validFlag == false }">
+                                        <el-icon>
+                                            <Document />
+                                        </el-icon>
+                                    </div>
+                                    <div class="card-title ellipsis">{{ data.name }}</div>
+                                    <div class="card-desc ellipsis-multi">{{ data.description }}
+                                    </div>
+                                </el-card>
+                            </div>
+                        </template>
+                        <template v-else>
+                            <div class="empty-wrapper">
+                                <div class="emptyBg">
+                                    <img src="../../../../../../../assets/system/images/no_data/noData.png" alt="" />
+                                    <p>无数据</p>
+                                </div>
+                            </div>
+                        </template>
+                    </div>
+                </el-row>
+            </div>
+        </el-col>
+    </el-row>
+</template>
+
+<script setup>
+
+import {
+    Document,
+    Menu,
+    DataLine,
+    Files,
+    Monitor,
+} from "@element-plus/icons-vue";
+import DeptTree from '@/components/DeptTree/tree.vue';
+import {
+    listAll,
+} from '@/api/att/rule/cleanRule.js';
+const { proxy } = getCurrentInstance();
+const { att_rule_clean_type } = proxy.useDict("att_rule_clean_type");
+import { listAttCleanCat, getAttCleanCat, delAttCleanCat, addAttCleanCat, updateAttCleanCat } from "@/api/att/cat/cleanCat/cleanCat.js";
+
+const contentWrapper = ref(null);
+const selectedCard = ref(null);
+const leftWidth = ref(250); // 初始左侧宽度
+const emit = defineEmits(["card-click"]);
+const props = defineProps({
+    type: {
+        type: String,
+        default: ''
+    },
+});
+let queryParams = ref(({
+    type: '',
+    // validFlag: '1'
+}))
+const processedData = ref([]);
+function handleNodeClick(data) {
+    queryParams.value.catCode = data.code;
+    fetchRulesByDimension();
+}
+
+let attCleanRuleList = ref([])
+function getDataTree() {
+    listAttCleanCat().then((response) => {
+        processedData.value = [];
+        const data = { id: '', name: '清洗规则', children: [] };
+        data.children = proxy.handleTree(response.data, 'id', 'parentId');
+        processedData.value.push(data);
+    });
+}
+let loading = ref(false)
+async function fetchRulesByDimension() {
+    loading.value = true;
+    const res = await listAll(queryParams.value);
+    const list = res.data || [];
+    console.log("🚀 ~ fetchRulesByDimension ~ list:", list)
+
+    if (props.type == '3') {
+        const disabledCodes = ['029', '039'];
+        attCleanRuleList.value = list.map(item => {
+            if (disabledCodes.includes(item.code)) {
+                return { ...item, validFlag: false };
+            }
+            return item;
+        });
+    } else {
+        attCleanRuleList.value = list;
+    }
+
+    // 排序,把 validFlag == false 的放后面
+    attCleanRuleList.value.sort((a, b) => {
+        return (b.validFlag === true) - (a.validFlag === true);
+    });
+
+    loading.value = false;
+}
+
+
+
+
+function cardClick(data) {
+    if (data.validFlag == false) {
+        return ElMessage.info('开发中')
+    }
+    selectedCard.value = data;
+    emit("card-click", data);
+}
+
+onMounted(() => {
+    fetchRulesByDimension();
+    getDataTree()
+});
+</script>
+
+<style lang="less" scoped>
+.main-layout {
+    height: 75vh;
+    overflow: hidden;
+}
+
+.left-col {
+    padding-right: 0;
+}
+
+.divider {
+    width: 1px;
+    height: 700px;
+    background-color: #dcdfe6;
+}
+
+.content-col {
+    width: 100%;
+    overflow: hidden;
+    padding-left: 0;
+}
+
+.content {
+    overflow-y: auto;
+    position: relative;
+}
+
+/* 右侧内容 */
+.content-col {
+    height: 75vh;
+    overflow: hidden;
+}
+
+.content {
+    height: 75vh;
+    overflow-y: auto;
+    padding: 20px 10px;
+}
+
+.cards-wrapper {
+    padding-left: 40px;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 20px;
+    justify-content: flex-start;
+}
+
+.card-item {
+    width: 180px;
+    box-sizing: border-box;
+    transition: transform 0.2s;
+    display: flex;
+    justify-content: center;
+    cursor: pointer;
+
+    &.selected {
+        transform: translateY(-2px);
+        border: 2px solid #409eff;
+        box-shadow: 0 0 10px rgba(64, 158, 255, 0.3);
+    }
+
+    &:hover {
+        transform: translateY(-2px);
+    }
+}
+
+.boxCard {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    min-height: 150px;
+    height: 150px;
+    width: 100%;
+    padding: 10px;
+    transition: all 0.2s;
+    box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
+
+    &:hover {
+        box-shadow: 0 0 12px rgba(0, 0, 0, 0.2);
+    }
+
+    .card-icon {
+        font-size: 45px;
+        color: #409EFF;
+        margin-bottom: 6px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+    }
+
+    .card-title {
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        max-width: 17ch;
+        /* 限制最大宽度为8个字符 */
+        margin: 0 auto;
+        cursor: pointer;
+        text-align: center;
+        width: 100%;
+    }
+
+    .ellipsis {
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+    }
+
+    .card-desc {
+        font-size: 12px;
+        color: #666;
+        text-align: center;
+        padding: 0 5px;
+        word-break: break-word;
+        display: -webkit-box;
+        -webkit-line-clamp: 3;
+        /* 显示3行 */
+        -webkit-box-orient: vertical;
+        overflow: hidden;
+        text-overflow: ellipsis;
+    }
+}
+
+.dh {
+    padding: 0;
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+}
+
+.menu-icon {
+    font-size: 20px;
+    vertical-align: middle;
+}
+
+::v-deep .el-card__body {
+    padding: 0 !important;
+}
+
+.empty-wrapper {
+    position: absolute;
+    top: 300px;
+    left: 0;
+    right: 0;
+    bottom: 0;
+
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    /* 文字样式 */
+    font-size: 16px;
+    color: #999;
+    user-select: none;
+}
+
+.card-icon {
+    cursor: pointer;
+    color: #409EFF;
+}
+
+.card-icon.is-disabled {
+    color: #ccc;
+    cursor: not-allowed;
+}
+</style>

+ 333 - 0
ruoyi-ui/src/views/dpp/task/integratioTask/components/clean/rule/ruleSelectorTree.vue

@@ -0,0 +1,333 @@
+<template>
+  <el-aside :style="{ width: `${leftWidth}px`, marginLeft: leftWidth == 0 ? '-15px' : '0px' }" class="left-pane">
+    <div class="left-tree">
+      <div class="head-container" v-if="props.showFilter">
+        <el-input class="filter-tree" size="large" v-model="deptName" :placeholder="placeholder" clearable
+          prefix-icon="Search" />
+      </div>
+      <div class="head-container">
+        <el-tree class="dept-tree" :data="deptOptions" :props="{ label: 'name', children: 'children' }"
+          :filter-node-method="filterNode" ref="deptTreeRef" node-key="id" highlight-current
+          :default-expanded-keys="expandedKeys" @node-click="handleNodeClick" :default-expand-all="defaultExpand">
+          <template #default="{ node, data }">
+            <span class="custom-tree-node">
+              <!-- 第一级 -->
+              <!-- <el-icon class="iconimg colorxz" v-if="node.expanded && node.level === 1">
+                <FolderOpened />
+              </el-icon>
+              <el-icon class="iconimg colorxz" v-if="!node.expanded && node.level === 1">
+                <Folder />
+              </el-icon> -->
+              <!-- 第二级 -->
+              <!-- <el-icon class="iconimg colorxz" v-if="node.expanded && node.childNodes.length && node.level == 2">
+                <FolderOpened />
+              </el-icon>
+              <el-icon class="iconimg colorxz" v-if="!node.expanded && node.childNodes.length && node.level == 2">
+                <Folder />
+              </el-icon> -->
+              <img class="node-icon" src="../../../../../../../assets/da/asset/folder.svg" alt=""
+                   v-if="node.expanded && node.childNodes.length" />
+              <img class="node-icon" src="../../../../../../../assets/da/asset/folder.svg" alt=""
+                   v-if="!node.expanded && node.childNodes.length" />
+              <!-- 子级 -->
+              <img class="child-icon" src="../../../../../../../assets/da/asset/file.svg" alt=""
+                   v-show="!node.isCurrent && node.childNodes.length == 0" />
+              <img class="child-icon" src="../../../../../../../assets/da/asset/file.svg" alt=""
+                   v-show="node.isCurrent && node.childNodes.length == 0" />
+              <span class="treelable" @click="getNode(node)">
+                {{ node.label }}
+              </span>
+            </span>
+          </template>
+        </el-tree>
+      </div>
+    </div>
+  </el-aside>
+
+  <!-- 拖拽栏 -->
+  <div class="resize-bar" @mousedown="startResize">
+    <div class="resize-handle-sx">
+      <span class="zjsx"></span>
+      <el-icon v-if="leftWidth == 0" @click.stop="toggleCollapse" class="collapse-icon">
+        <ArrowRight />
+      </el-icon>
+      <el-icon v-else class="collapse-icon" @click.stop="toggleCollapse">
+        <ArrowLeft />
+      </el-icon>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, defineProps, defineEmits, watch, onMounted } from "vue";
+const { proxy } = getCurrentInstance();
+const props = defineProps({
+  deptOptions: Array,
+  leftWidth: {
+    type: Number,
+    default: 300,
+  },
+  placeholder: {
+    type: String,
+    default: "请输入部门名称",
+  },
+  defaultExpand: {
+    type: Boolean,
+    default: false,
+  },
+  height: {
+    type: String,
+    default: "87vh",
+  },
+  showFilter: {
+    type: Boolean,
+    default: true,
+  },
+});
+
+const emit = defineEmits(["node-click", "update:deptName", "update:leftWidth"]);
+
+const deptName = ref("");
+const deptTreeRef = ref(null);
+const leftWidth = ref(props.leftWidth);
+const expandedKeys = ref([]);
+
+// 等 deptOptions 加载后设置一级节点展开
+watch(
+  () => props.deptOptions,
+  (val) => {
+    if (Array.isArray(val) && val.length > 0) {
+      console.log(val);
+      expandedKeys.value = val.map((item) => item.id); // 只展开第一层
+    }
+  },
+  { immediate: true }
+);
+
+// 过滤节点
+const filterNode = (value, data) => {
+  if (!value) return true;
+  return data.name.indexOf(value) !== -1;
+};
+
+watch(deptName, (val) => {
+  if (deptTreeRef.value) {
+    deptTreeRef.value.filter(val);
+  }
+});
+
+watch(
+  () => props.leftWidth,
+  (val) => {
+    leftWidth.value = val;
+  }
+);
+
+// 拖拽逻辑
+const isResizing = ref(false);
+let startX = 0;
+const startResize = (event) => {
+  isResizing.value = true;
+  startX = event.clientX;
+  document.addEventListener("mousemove", updateResize);
+  document.addEventListener("mouseup", stopResize);
+};
+const stopResize = () => {
+  isResizing.value = false;
+  document.removeEventListener("mousemove", updateResize);
+  document.removeEventListener("mouseup", stopResize);
+};
+const updateResize = (event) => {
+  if (isResizing.value) {
+    const delta = event.clientX - startX; // 计算鼠标移动距离
+    leftWidth.value += delta; // 修改左侧宽度
+    startX = event.clientX; // 更新起始位置
+    // 使用 requestAnimationFrame 来减少页面重绘频率
+    requestAnimationFrame(() => { });
+  }
+};
+
+// 折叠展开
+const toggleCollapse = () => {
+  if (leftWidth.value === 0) {
+    leftWidth.value = 300;
+  } else {
+    leftWidth.value = 0;
+  }
+  emit("update:leftWidth", leftWidth.value);
+};
+
+function handleNodeClick(data) {
+  emit("node-click", data);
+}
+
+const getNode = (node) => {
+  console.log(node);
+};
+
+const resetTree = () => {
+  if (deptTreeRef.value) {
+    proxy.$refs.deptTreeRef.setCurrentKey(null);
+  }
+};
+
+defineExpose({ resetTree });
+</script>
+
+<style scoped lang="scss">
+.left-wrapper {
+  display: flex;
+  height: 100%;
+}
+
+.left-pane {
+  background-color: #ffffff;
+  overflow: hidden;
+}
+
+.left-tree {
+  height: 72vh;
+  padding: 15px 15px 15px 15px;
+  scrollbar-width: none;
+  -ms-overflow-style: none;
+  box-shadow: none !important;
+
+}
+
+.el-aside {
+  padding: 2px 0px;
+  margin-bottom: 0px;
+  background-color: none;
+
+
+}
+
+.custom-tree-node {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  padding: 0 36px 0 12px;
+
+  .node-icon {
+    width: 16px;
+    height: 16px;
+  }
+
+  .child-icon {
+    width: 16px;
+    height: 16px;
+  }
+
+  .treelable {
+    margin-left: 10px;
+    flex: 1;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    font-family: PingFang SC;
+    font-weight: 400;
+    font-size: 14px;
+    color: rgba(0, 0, 0, 0.85);
+  }
+}
+
+.zjiconimg {
+  font-size: 12px;
+}
+
+.colorxz {
+  color: #358cf3;
+}
+
+.colorwxz {
+  color: var(--el-color-primary);
+}
+
+.iconimg {
+  font-size: 15px;
+}
+
+.resize-bar {
+  cursor: ew-resize;
+  background-color: #f0f2f5;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 72vh;
+}
+
+.resize-handle-sx {
+  width: 15px;
+  text-align: center;
+  position: relative;
+  /* 必须加,用来定位 collapse-icon */
+}
+
+.zjsx {
+  display: none;
+  width: 5px;
+  height: 50px;
+  border-left: 1px solid #ccc;
+  border-right: 1px solid #ccc;
+}
+
+.collapse-icon {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  /* 真正的居中 */
+  font-size: 28px;
+  color: #aaa;
+  cursor: pointer;
+  z-index: 10;
+  padding: 5px;
+}
+
+:deep(.filter-tree) {
+  margin-bottom: 16px;
+
+  .el-input__wrapper {
+    border: 1px solid var(--el-color-primary);
+  }
+
+  .el-input__prefix {
+    color: var(--el-color-primary);
+  }
+}
+
+:deep(.dept-tree) {
+
+  //组织树 背景颜色 及右边线颜色
+  &.el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content {
+    background: rgba(51, 103, 252, 0.06) !important;
+    border: none;
+
+    .custom-tree-node {
+      .treelable {
+        color: var(--el-color-primary);
+      }
+    }
+  }
+
+  .el-tree-node__content {
+    position: relative;
+
+    .el-tree-node__expand-icon {
+      position: absolute;
+      right: 10px;
+      color: transparent;
+      font-size: 11px;
+      width: 11px;
+      height: 11px;
+
+      &>svg {
+        background: url("@/assets/da/asset/arrow.svg") no-repeat;
+        background-size: 100% 100%;
+        transform: rotate(-90deg);
+
+      }
+    }
+  }
+}
+</style>

+ 820 - 0
ruoyi-ui/src/views/dpp/task/integratioTask/index2.vue

@@ -0,0 +1,820 @@
+<template>
+  <div class="app-container">
+    <el-row>
+      <el-col :span="16">
+        <el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
+          <el-form-item label="任务名称">
+            <el-input v-model="queryParams.jobName" style="width: 150px" placeholder="请输入任务名称"/>
+          </el-form-item>
+          <el-form-item label="任务状态">
+            <el-select v-model="queryParams.jobStatus" class="noBorSel" placeholder="请选择任务状态"
+                       style="width: 120px;">
+              <el-option label="启用" :value="0"/>
+              <el-option label="禁用" :value="1"/>
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" icon="Search" @click="getTable">查询</el-button>
+          </el-form-item>
+        </el-form>
+      </el-col>
+      <el-col :span="8" style="display: flex;justify-content: right;">
+        <el-button plain type="primary" @click="showAdd" style="margin-left:auto;" :icon="Plus">新增
+        </el-button>
+        <el-button type="danger" @click="delAll" plain style="margin-left:1%;" :icon="Delete">删除
+        </el-button>
+      </el-col>
+    </el-row>
+    <el-table
+        :data="tableData"
+        :cell-style="{ paddingTop:'3px',paddingBottom:'3px' }"
+        :header-cell-style="{height: heightAll*0.01+'px',}"
+        @selection-change="handleSelectionChange"
+        :row-style="{ fontSize: '16px',textAlign:'center'}"
+        border
+        :height="tableheight">
+      <el-table-column type="selection" width="55"/>
+      <el-table-column type="index" label="序号" width="80"></el-table-column>
+      <el-table-column prop="jobName" label="任务名称" width="160" v-if="showTablepane.rw">
+        <template #default="scope">
+          <el-button type="primary" @click="showDe(scope.row)" text style="margin-left: 1%;">
+            {{ scope.row.jobName }}
+          </el-button>
+        </template>
+      </el-table-column>
+      <el-table-column prop="groupName" label="组名称" width="160" v-if="showTablepane.zm"/>
+      <el-table-column prop="executorInfo" label="执行器名称" width="160" v-if="showTablepane.zx"/>
+      <el-table-column prop="ownerName" label="负责人" width="160" v-if="showTablepane.fz"/>
+      <el-table-column prop="nextTriggerAt" label="触发时间" width="180" v-if="showTablepane.cf">
+      </el-table-column>
+      <el-table-column prop="jobStatus" label="状态" width="170" v-if="showTablepane.zt">
+        <template #default="scope">
+          <el-switch @change="changejobStatus(scope.row)" v-model="scope.row.jobStatus"/>
+        </template>
+      </el-table-column>
+      <el-table-column prop="taskType" label="任务类型" width="170" v-if="showTablepane.rwl">
+        <template #default="scope">
+          <el-tag v-if="scope.row.taskType==1" type="success">集群</el-tag>
+          <el-tag v-if="scope.row.taskType==2" type="info">广播</el-tag>
+          <el-tag v-if="scope.row.taskType==3" type="warning">Sharding</el-tag>
+          <el-tag v-if="scope.row.taskType==4" type="danger">Map</el-tag>
+          <el-tag v-if="scope.row.taskType==5">MapReduce</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="triggerType" label="触发类型" width="170" v-if="showTablepane.cfl">
+        <template #default="scope">
+          <el-tag v-if="scope.row.triggerType==2">固定时间</el-tag>
+          <el-tag v-if="scope.row.triggerType==3">CRON 表达式</el-tag>
+          <el-tag v-if="scope.row.triggerType==99">工作流</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="retryInterval" label="间隔时长" width="170" v-if="showTablepane.jg"/>
+      <el-table-column prop="blockStrategy" label="阻塞策略" width="170" v-if="showTablepane.zs">
+        <template #default="scope">
+          <el-tag v-if="scope.row.blockStrategy==1">丢弃</el-tag>
+          <el-tag v-if="scope.row.blockStrategy==2">覆盖</el-tag>
+          <el-tag v-if="scope.row.blockStrategy==3">并行</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="executorTimeout" label="超时时间(秒)" width="170" v-if="showTablepane.cs"/>
+      <el-table-column prop="updateDt" label="更新时间" width="170" v-if="showTablepane.gx"/>
+      <el-table-column prop="address" label="操作" width="222" fixed="right">
+        <template #default="scope">
+          <div style="display: flex;justify-content: space-between;width: 100%;">
+            <el-button type="primary" @click="showEdit(scope.row)" text style="margin-left: 1%;">编辑
+            </el-button>
+            <el-button @click="snaliTrigger(scope.row)" type="primary" text style="margin-left: 1%;">
+              执行
+            </el-button>
+            <el-button @click="delRow(scope.row)" type="danger" text style="margin-left: 1%;">删除
+            </el-button>
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+
+  <el-dialog :title="title" @close="clearForm" v-model="dialogVisible" title="" width="60%" destroy-on-close>
+    <el-form style="margin-top: 0;width: 98%;" :model="formJi" label-position="right" ref="formRefJi"
+             label-width="120px" :rules="rulesJi">
+      <el-row :gutter="24">
+        <el-col :span="8">
+          <el-form-item label="任务名称:" prop="jobName" style="">
+            <div style="display: flex;width: 100%;justify-content: space-between;">
+              <el-input v-model="formJi.jobName" style="width: 100%;"/>
+            </div>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="负责人:" prop="" style="">
+            <div style="display: flex;width: 100%;justify-content: space-between;">
+              <el-input v-model="formJi.ownerName" style="width: 100%;"/>
+            </div>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="48">
+        <el-col :span="15">
+          <!-- <el-form-item label="标签:" prop="" style="">
+               <div
+                  v-if="parTag.length==0"
+                  @click='addTag'
+                  style="
+                  cursor: pointer;
+                  display: flex;
+                  justify-content: center;
+                  align-items: center;
+                  width: 200px;
+                  height: 30px;
+                  border: 0.5px dashed #c8c9cc;
+                  border-radius: 8px;
+                  position: relative;
+                  "
+              >
+              +  &nbsp;添加
+              </div>
+              <div v-if="parTag.length!=0" v-for="(item,index) in parTag">
+                  <div style="display: flex;align-items: center;margin-top: 3%;" :class="{ 'no-margin-top': index === 0 }">
+                      <el-input  v-model="item.key" style="width: 50%;" resize="none" placeholder="key"/>
+                      <div style="margin-left: 1%;">:</div>
+                      <el-input  v-model="item.value" style="width: 50%;margin-left: 1%;" resize="none" placeholder="value"/>
+                      <el-icon @click="delTag(index)" style="margin-left: 3%;color: red;cursor: pointer;"><Minus /></el-icon>
+                      <el-icon @click="addTag" style="margin-left: 3%;color: #337ecc;cursor: pointer;"><Plus /></el-icon>
+                  </div>
+              </div>
+          </el-form-item> -->
+        </el-col>
+      </el-row>
+      <el-row :gutter="48">
+        <el-col :span="8">
+          <el-form-item label="状态:" prop="jobStatus">
+            <el-radio-group :disabled="!isAddTa" v-model="formJi.jobStatus" class="custom-radio-group"
+                            style="width: 100%;margin-top: -1%;">
+              <el-radio :label="1">启用</el-radio>
+              <el-radio :label="0">禁用</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+
+      </el-row>
+      <el-row :gutter="48">
+        <el-col :span="8">
+          <el-form-item label="任务类型:" prop="taskType">
+            <el-select
+                v-model="formJi.taskType"
+                class="noBorSel"
+                placeholder=""
+                style="width: 100%;margin-left: 0;"
+            >
+              <el-option
+                  v-for="item in optionsTaskType"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <div style="margin-top: 0;">
+        <el-form-item label="任务配置:">
+          <args-str v-model="formJi.argsStr"></args-str>
+        </el-form-item>
+        <el-row :gutter="48">
+          <el-col :span="8">
+            <el-form-item label="路由策略:" prop="routeKey">
+              <el-select
+                  v-model="formJi.routeKey"
+                  class="noBorSel"
+                  placeholder=""
+                  style="width: 100%;margin-left: 0;"
+              >
+                <el-option label="Hash" :value="1"/>
+                <el-option label="随机" :value="2"/>
+                <el-option label="LRU" :value="3"/>
+                <el-option label="轮询" :value="4"/>
+                <el-option label="匹配第一个" :value="5"/>
+                <el-option label="匹配最后一个" :value="6"/>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="阻塞策略:" prop="blockStrategy">
+              <el-select
+                  v-model="formJi.blockStrategy"
+                  class="noBorSel"
+                  placeholder=""
+                  style="width: 100%;margin-left: 0;"
+              >
+                <el-option label="丢弃" :value="1"/>
+                <el-option label="覆盖" :value="2"/>
+                <el-option label="并行" :value="3"/>
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="48">
+          <el-col :span="8">
+            <el-form-item label="触发类型:" prop="triggerType">
+              <el-select
+                  v-model="formJi.triggerType"
+                  class="noBorSel"
+                  placeholder=""
+                  style="width: 100%;margin-left: 0;"
+              >
+                <el-option label="固定时间" :value="2"/>
+                <el-option label="CRON 表达式" :value="3"/>
+                <el-option label="工作流" :value="99"/>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="间隔时长:" prop="triggerInterval">
+              <el-input v-model="formJi.triggerInterval" placeholder="请选择调度周期">
+                <template #append>
+                  <el-button type="primary" @click="handleShowCron" style="background-color: #2666fb; color: #fff">
+                    配置
+                    <i class="el-icon-time el-icon--right"></i>
+                  </el-button>
+                </template>
+              </el-input>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="48">
+          <el-col :span="8">
+            <el-form-item label="超时时间(秒):" prop="executorTimeout">
+              <el-input-number
+                  v-model="formJi.executorTimeout"
+                  style="width: 100%;"
+                  class="mx-4"
+                  :min="1"
+                  :max="10"
+                  controls-position="right"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="最大重试次数:" prop="maxRetryTimes">
+              <el-input-number
+                  v-model="formJi.maxRetryTimes"
+                  style="width: 100%;"
+                  class="mx-4"
+                  :min="1"
+                  :max="10"
+                  controls-position="right"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="48">
+          <el-col :span="8">
+            <el-form-item label="重试间隔:" prop="retryInterval">
+              <el-input-number
+                  v-model="formJi.retryInterval"
+                  style="width: 100%;"
+                  class="mx-4"
+                  :min="1"
+                  :max="10"
+                  controls-position="right"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="描述:">
+          <el-input v-model="formJi.description" style="width: 75%;" placeholder="" type="textarea" :row="2"
+                    resize="none"/>
+        </el-form-item>
+      </div>
+    </el-form>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button v-if="isAddTa" type="primary" @click="addTa">
+          提交
+        </el-button>
+        <el-button v-if="!isAddTa" type="primary" @click="saveEditTa">
+          提交
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
+  <el-dialog @close="clearFromTree" v-model="dialogVisibleTree" title="" width="55%" destroy-on-close>
+    <el-descriptions
+        class="margin-top"
+        :title="titleDe"
+        style="padding-top: 2%;"
+        :column="3"
+        :size="size"
+        border
+    >
+      <el-descriptions-item>
+        <template #label>
+          <div class="cell-item">
+            任务名称
+          </div>
+        </template>
+        {{ formJi.jobName }}
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label>
+          <div class="cell-item">
+            组名称
+          </div>
+        </template>
+        {{ formJi.groupName }}
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label>
+          <div class="cell-item">
+            负责人
+          </div>
+        </template>
+        {{ formJi.ownerName }}
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label>
+          <div class="cell-item">
+            状态
+          </div>
+        </template>
+        <el-tag>{{ formJi.jobStatus }}</el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label>
+          <div class="cell-item">
+            任务类型
+          </div>
+        </template>
+        {{ formJi.taskType }}
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label>
+          <div class="cell-item">
+            执行器名称
+          </div>
+        </template>
+        {{ formJi.executorInfo }}
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label>
+          <div class="cell-item">
+            方法参数
+          </div>
+        </template>
+        {{ formJi.argsStr }}
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label>
+          <div class="cell-item">
+            路由策略
+          </div>
+        </template>
+        {{ formJi.routeKey }}
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label>
+          <div class="cell-item">
+            阻塞策略
+          </div>
+        </template>
+        {{ formJi.blockStrategy }}
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label>
+          <div class="cell-item">
+            触发类型
+          </div>
+        </template>
+        {{ formJi.triggerType }}
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label>
+          <div class="cell-item">
+            间隔时长
+          </div>
+        </template>
+        {{ formJi.triggerInterval }}
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label>
+          <div class="cell-item">
+            超时时间(秒)
+          </div>
+        </template>
+        {{ formJi.executorTimeout }}
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label>
+          <div class="cell-item">
+            最大重试次数
+          </div>
+        </template>
+        {{ formJi.maxRetryTimes }}
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label>
+          <div class="cell-item">
+            重试间隔
+          </div>
+        </template>
+        {{ formJi.retryInterval }}
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label>
+          <div class="cell-item">
+            描述:
+          </div>
+        </template>
+        {{ formJi.description }}
+      </el-descriptions-item>
+    </el-descriptions>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button type="primary" @click="dialogVisibleTree = false">确定</el-button>
+      </span>
+    </template>
+  </el-dialog>
+  <el-dialog title="Cron表达式生成器" v-model="openCron" class="dialog" append-to-body destroy-on-close>
+    <crontab ref="crontabRef" @hide="openCron = false" @fill="crontabFill" :expression="expression"></crontab>
+  </el-dialog>
+</template>
+<script setup>
+import {Delete, Plus} from '@element-plus/icons-vue'
+import {onMounted, reactive, ref} from 'vue'
+import {addSnail, delSnamil, snailDe, snailEdit, snailList, snailSta, snailTri} from "@/api/service/timing";
+import ArgsStr from "@/views/dpp/task/integratioTask/argsStr.vue";
+import Crontab from "@/components/Crontab/index.vue";
+import {copyObj} from "codemirror/src/util/misc.js";
+
+const queryParams = ref({
+  jobName: '',
+  jobStatus: null,
+})
+const {proxy} = getCurrentInstance();
+const title = ref([])
+const total = ref(1)
+const tableData = ref([])
+const tableheight = window.innerHeight * 0.8
+const heightAll = window.innerHeight
+const dialogVisible = ref(false)
+const dialogVisibleTree = ref(false)
+const isAddTa = ref(false)
+const titleDe = ref('')
+const optionsTaskType = ref([
+  {
+    label: '集群',
+    value: 1
+  },
+  {
+    label: '广播',
+    value: 2
+  },
+  {
+    label: 'Sharding',
+    value: 3
+  },
+  {
+    label: 'Map',
+    value: 4
+  },
+  {
+    label: 'MapReduce',
+    value: 5
+  },
+])
+const showTablepane = ref({
+  rw: true,
+  zm: true,
+  zx: true,
+  fz: true,
+  bq: true,
+  cf: true,
+  zt: true,
+  rwl: true,
+  cfl: true,
+  jg: true,
+  zs: true,
+  cs: true,
+  gx: true,
+})
+const formJi = ref({
+  jobName: '',
+  groupName: '',
+  jobStatus: '',
+  taskType: '',
+  executorInfo: '',
+  executorInfoTy: '',
+  type: '',
+  mdContact: '',
+  mdUnit: '',
+  devUnit: '',
+  devContact: '',
+  deployDir: '',
+  deployIp: '',
+  deployPort: '',
+  mirrorImageUrl: '',
+  mdRunCmd: '',
+  envOs: '',
+  envDisk: '',
+  envGpuMem: '11',
+  evnArmX86: '',
+  envCpuNum: '',
+  envGpuType: '',
+  envGpuNum: '2',
+  envMem: '',
+  mdInNote: '',
+  mdOutNote: '',
+  triggerInterval: ''
+});
+const selectedRows = ref([])
+const rulesJi = reactive({
+  jobName: [{required: true, message: '必填', trigger: 'blur'}],
+  groupName: [{required: true, message: '必填', trigger: 'blur'}],
+  taskType: [{required: true, message: '必填', trigger: 'blur'}],
+  jobStatus: [{required: true, message: '必填', trigger: 'blur'}],
+  executorInfo: [{required: true, message: '必填', trigger: 'blur'}],
+  routeKey: [{required: true, message: '可选', trigger: 'blur'}],
+  blockStrategy: [{required: true, message: '必填', trigger: 'blur'}],
+  triggerInterval: [{required: true, message: '必填', trigger: 'blur'}],
+  executorTimeout: [{required: true, message: '必填', trigger: 'blur'}],
+  maxRetryTimes: [{required: true, message: '必填', trigger: 'blur'}],
+  retryInterval: [{required: true, message: '必填', trigger: 'blur'}],
+  triggerType: [{required: true, message: '必填', trigger: 'blur'}],
+});
+const formRefJi = ref();
+const formTree = ref({
+  itemName: '',
+  catePid: '',
+  itemNo: '',
+  itemNotes: ''
+});
+let openCron = ref(false);
+const expression = ref("");
+const handleSelectionChange = (selection) => {
+  selectedRows.value = selection;
+};
+
+function snaliTrigger(row) {
+  var par = {
+    jobId: row.id,
+    taskType: row.taskType
+  }
+  snailTri(par).then(res => {
+    if (res.code === 200) {
+      proxy.$modal.msgSuccess(res.msg);
+    }
+  })
+}
+
+function showCode() {
+  Object.keys(rulesJi).forEach(key => {
+    delete rulesJi[key];
+  });
+  if (formJi.value.executorInfoTy === '自定义执行器') {
+    Object.assign(rulesJi, {
+      jobName: [{required: true, message: '必填', trigger: 'blur'}],
+      groupName: [{required: true, message: '必填', trigger: 'blur'}],
+      taskType: [{required: true, message: '必填', trigger: 'blur'}],
+      jobStatus: [{required: true, message: '必填', trigger: 'blur'}],
+      executorInfo: [{required: true, message: '必填', trigger: 'blur'}],
+      routeKey: [{required: true, message: '可选', trigger: 'blur'}],
+      blockStrategy: [{required: true, message: '必填', trigger: 'blur'}],
+      triggerInterval: [{required: true, message: '必填', trigger: 'blur'}],
+      executorTimeout: [{required: true, message: '必填', trigger: 'blur'}],
+      maxRetryTimes: [{required: true, message: '必填', trigger: 'blur'}],
+      retryInterval: [{required: true, message: '必填', trigger: 'blur'}],
+      triggerType: [{required: true, message: '必填', trigger: 'blur'}],
+    });
+  }
+  if (formJi.value.executorInfoTy === '内置执行器') {
+    Object.assign(rulesJi, {
+      jobName: [{required: true, message: '必填', trigger: 'blur'}],
+      groupName: [{required: true, message: '必填', trigger: 'blur'}],
+      taskType: [{required: true, message: '必填', trigger: 'blur'}],
+      jobStatus: [{required: true, message: '必填', trigger: 'blur'}],
+      executorInfo: [{required: true, message: '必填', trigger: 'blur'}],
+      routeKey: [{required: true, message: '可选', trigger: 'blur'}],
+      blockStrategy: [{required: true, message: '必填', trigger: 'blur'}],
+      triggerInterval: [{required: true, message: '必填', trigger: 'blur'}],
+      executorTimeout: [{required: true, message: '必填', trigger: 'blur'}],
+      maxRetryTimes: [{required: true, message: '必填', trigger: 'blur'}],
+      retryInterval: [{required: true, message: '必填', trigger: 'blur'}],
+      triggerType: [{required: true, message: '必填', trigger: 'blur'}],
+    });
+  }
+}
+
+function delAll() {
+  var parDel = ''
+  selectedRows.value.forEach(item => {
+    parDel = parDel + item.id + ','
+  })
+  parDel = parDel.slice(0, -1)
+  proxy.$modal.confirm('是否确认删除?').then(function () {
+    var par = {
+      jobIds: parDel
+    }
+    return delSnamil(par);
+  }).then(() => {
+    getTable();
+    proxy.$modal.msgSuccess("删除成功");
+  }).catch(() => {
+  });
+}
+
+function showDe(row) {
+  dialogVisibleTree.value = true
+  var par = {
+    jobId: row.id
+  }
+  snailDe(par).then(res => {
+    formJi.value = res.data
+    if (formJi.value.jobStatus === 0) {
+      formJi.value.jobStatus = '启用'
+    }
+    if (formJi.value.jobStatus === 1) {
+      formJi.value.jobStatus = '关闭'
+    }
+  })
+}
+
+async function changejobStatus(row) {
+  var par = {
+    jobId: row.id,
+    status: row.jobStatus === true ? 1 : 0
+  }
+  await snailSta(par).then(res => {
+    formJi.value = res
+    if (res.code === 200) {
+      proxy.$modal.msgSuccess("修改成功");
+    }
+  })
+
+}
+
+function clearForm() {
+  formJi.value = {}
+}
+
+function formatTimestamp(ms) {
+  const date = new Date(ms);
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, '0'); // 补零
+  const day = String(date.getDate()).padStart(2, '0'); // 补零
+  return `${year}年${month}月${day}日`;
+}
+
+function getTable() {
+  queryParams.value.executorInfo = 'cleanDataJob'
+  snailList(queryParams.value).then(res => {
+    tableData.value = res.rows
+    tableData.value.forEach(item => {
+      if (item.jobStatus === 0) {
+        item.jobStatus = false
+      }
+      if (item.jobStatus === 1) {
+        item.jobStatus = true
+      }
+      item.nextTriggerAt = formatTimestamp(item.nextTriggerAt)
+    })
+    total.value = res.total
+  })
+}
+
+function showAdd() {
+  isAddTa.value = true
+  title.value = '新增'
+  dialogVisible.value = true
+  formJi.value = {
+    taskType: 1,
+    jobStatus: 1,
+    routeKey: 4,
+    blockStrategy: 1,
+    triggerType: 3,
+    executorTimeout: 60,
+    maxRetryTimes: 3,
+    retryInterval: 1,
+  }
+}
+
+function showEdit(row) {
+  isAddTa.value = false
+  title.value = '编辑'
+  dialogVisible.value = true
+  var par = {
+    jobId: row.id
+  }
+  snailDe(par).then(res => {
+    formJi.value = res.data
+  })
+}
+
+function clearFromTree() {
+  formTree.value = {
+    itemName: '',
+    catePid: '',
+    itemNo: '',
+    itemNotes: ''
+  }
+}
+
+function addTa() {
+  formRefJi.value.validate(async (valid) => {
+    if (valid) {
+      formJi.value.executorInfo = 'cleanDataJob'
+      formJi.value.argsStrMap = JSON.parse(formJi.value.argsStr)
+      await addSnail(formJi.value).then(res => {
+        if (res.code === 200) {
+          proxy.$modal.msgSuccess("新增成功");
+          getTable()
+          dialogVisible.value = false
+        }
+      })
+    }
+  });
+}
+
+function saveEditTa() {
+  formJi.value.executorInfo = 'cleanDataJob'
+  formJi.value.argsStrMap = JSON.parse(formJi.value.argsStr)
+  snailEdit(formJi.value).then(res => {
+    if (res.code === 200) {
+      proxy.$modal.msgSuccess("修改成功");
+      dialogVisible.value = false
+      getTable()
+    }
+  })
+}
+
+function delRow(row) {
+  proxy.$modal.confirm('是否确认删除?').then(function () {
+    var par = {
+      jobIds: row.id
+    }
+    return delSnamil(par);
+  }).then(() => {
+    getTable();
+    proxy.$modal.msgSuccess("删除成功");
+  }).catch(() => {
+  });
+}
+
+/** 调度周期按钮操作 */
+function handleShowCron() {
+  expression.value = formJi.value.triggerInterval;
+  openCron.value = true;
+}
+
+/** 确定后回传值 */
+function crontabFill(value) {
+  formJi.value.triggerInterval = value;
+}
+
+
+onMounted(() => {
+  getTable()
+})
+</script>
+<style scoped>
+.no-margin-top {
+  margin-top: 0 !important;
+}
+
+.drag-handle {
+  cursor: move;
+}
+
+.ghost {
+  opacity: 0.5;
+  background: #c8ebfb;
+}
+
+/* 防止文字选中 */
+:deep(.el-table__row) {
+  user-select: none;
+  -webkit-user-select: none;
+}
+</style>
+<style scoped lang="scss">
+
+
+.el-table .el-table__row td {
+  height: 60px !important; /* 行高 */
+}
+
+.custom-tree-node {
+  display: flex; /* 启用 Flex 布局 */
+  align-items: center; /* 垂直居中 */
+  gap: 8px; /* 图标与文字间距 */
+}
+
+.custom-tree-node {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 14px;
+  padding-right: 8px;
+}
+</style>

+ 31 - 0
ruoyi-ui/src/views/dpp/utils/opBase.js

@@ -0,0 +1,31 @@
+/**
+ * 存放一些节点操作的公共方法
+ */
+import "@/assets/system/styles/global.scss";
+
+// 表输入的规则
+export function renameRuleToRuleConfig(data) {
+    return data
+        .filter(col => Array.isArray(col.cleanRuleList) && col.cleanRuleList.length > 0)
+        .map(col => {
+            return col.cleanRuleList.map(rule => {
+                let parsedRule = {};
+                try {
+                    parsedRule = JSON.parse(rule.rule); // 原来的 rule 解析成对象
+                } catch (e) {
+                    console.warn(`rule JSON 解析失败: ${rule.rule}`, e);
+                }
+                const ruleConfig = {
+                    ...parsedRule,
+                    columns: [col.columnName]
+                };
+                const {rule: _, ...rest} = rule;
+                return {
+                    ...rest,
+                    columns: [col.columnName],
+                    ruleConfig
+                };
+            });
+        })
+        .flat();
+}

+ 3 - 1
ruoyi-ui/src/views/map/index.vue

@@ -9,7 +9,9 @@
 import {getBizDataShowConfigList} from "@/api/standardization/bizDataShowConfig.js";
 import BizDataCard from "@/views/map/components/bizDataCard.vue";
 import GwMap from "./components/map.vue"
+import {useRoute} from "vue-router";
 
+const route = useRoute();
 const mapRef = ref(null);
 const mapConfig = ref({
   zoom: 9,
@@ -117,7 +119,7 @@ const mapConfig = ref({
 const bizDataShowConfigList = ref([]);
 
 function getBizDataConfigList() {
-  getBizDataShowConfigList().then(res => {
+  getBizDataShowConfigList({appId: route.params.id}).then(res => {
     bizDataShowConfigList.value = res.data;
   })
 }