记录工作工作树的预览

83 阅读11分钟

主要记录 对于选择工作项的树形结构数据生成一个预览的表格。 生成这个表格还有一个小bug,目前对于分组下面没有工作项和评估内容的值,表格预览出来不好看,因为没有值的时候,我把边框也干掉了。直接给空字符串或null 还是会生成空白的td这个bug。

image.png

image.png

image.png image.png 上代码 列表分页的代码

  <ContentWrap>
    <!-- 搜索工作栏 -->
    <el-form
      class="-mb-15px"
      :model="queryParams"
      ref="queryFormRef"
      :inline="true"
      label-width="68px"
    >
      <el-form-item label="应用名称" prop="appName">
        <el-input
          v-model="queryParams.appName"
          placeholder="请输入应用名称"
          clearable
          @keyup.enter="handleQuery"
          class="!w-240px"
        />
      </el-form-item>
      <el-form-item>
        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
        <el-button type="success" plain @click="handleExport" :loading="exportLoading">
          <Icon icon="ep:download" class="mr-5px" /> 模板下载
        </el-button>
      </el-form-item>
    </el-form>
  </ContentWrap>

  <!-- 列表 -->
  <ContentWrap>
    <el-table
      v-loading="loading"
      :data="list"
      :stripe="true"
      :show-overflow-tooltip="true"
      :tree-props="{ children: 'children', hasChildren: 'havingChildFlag' }"
      :load="load"
      row-key="appId"
      lazy
    >
      <el-table-column label="应用名称" header-align="center" align="left" prop="appName" />
      <el-table-column align="center" label="应用类型" prop="status" min-width="90">
        <template #default="scope">
          <dict-tag :type="DICT_TYPE.CLASSIFY_FLAG" :value="scope.row.classifyFlag" />
        </template>
      </el-table-column>
      <el-table-column label="应用编码" align="center" prop="appCode" />
      <el-table-column label="运维工作要求" align="center">
        <template #default="scope">
          <span
            v-if="!scope.row.havingDefaultOps"
            class="cursor-[pointer] text-[#1b94ff]"
            @click="addOpen('create', scope.row.appId)"
            >新建</span
          >
          <span
            v-if="scope.row.havingDefaultOps"
            class="cursor-[pointer] text-[#1b94ff]"
            @click="addOpen('edit', scope.row.appId)"
            >编辑</span
          >
          <span
            v-if="scope.row.havingDefaultOps"
            class="cursor-[pointer] text-[#1b94ff] p-0 px-2"
            @click="addOpen('detail', scope.row.appId)"
            >查看</span
          >
          <span
            v-if="scope.row.havingDefaultOps"
            class="cursor-[pointer] text-[#ec8fbb]"
            @click="handleDelete('work', scope.row.appId)"
            >删除</span
          >
        </template>
      </el-table-column>
      <el-table-column label="运维考核要求" align="center">
        <template #default="scope">
          <span
            v-if="!scope.row.havingDefaultEval"
            class="cursor-[pointer] text-[#1b94ff]"
            @click="addEval('create', scope.row.appId)"
            >新建</span
          >
          <span
            v-if="scope.row.havingDefaultEval"
            class="cursor-[pointer] text-[#1b94ff]"
            @click="addEval('evaledit', scope.row.appId)"
            >编辑</span
          >
          <span
            v-if="scope.row.havingDefaultEval"
            class="cursor-[pointer] text-[#1b94ff] p-0 px-2"
            @click="addEval('detail', scope.row.appId)"
            >查看</span
          >
          <span
            v-if="scope.row.havingDefaultEval"
            class="cursor-[pointer] text-[#ec8fbb]"
            @click="handleDelete('eval', scope.row.appId)"
            >删除</span
          >
        </template>
      </el-table-column>

      <el-table-column label="应用详情" align="center" min-width="120px">
        <template #default="scope">
          <el-button
            link
            type="primary"
            @click="openDetails('detail', scope.row.appId, scope.row.classifyFlag)"
          >
            查看
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- 分页 -->
    <Pagination
      :total="total"
      v-model:page="queryParams.pageNo"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />
  </ContentWrap>

  <!-- 运维工作要求的添加/修改/查看 -->
  <WorkForm ref="workRef" @success="getList" />
  <!-- 运维考核要求的添加/修改/查看 -->
  <EvalForm ref="evalRef" @success="getList" />
  <!-- 新增应用组表单弹窗:添加/修改 -->
  <AppGruop ref="addGroupRef" @success="getList" />
  <!-- 新增应用表单弹窗:添加/修改 -->
  <AppForm ref="formRef" @success="getList" />
</template>

<script setup lang="ts">
import download from '@/utils/download'
import { AppApi, AppVO } from '@/api/md/app'
import { DefaultEvalItemApi } from '@/api/md/DefaultEvalItemForm'
import WorkForm from './WorkForm.vue'
import EvalForm from './EvalForm.vue'
import AppGruop from '../appManage/AppGruop.vue'
import AppForm from '../appManage/AppForm.vue'
import { DICT_TYPE } from '@/utils/dict'

/** 应用系统 列表 */
defineOptions({ name: 'App' })

const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化

const loading = ref(true) // 列表的加载中
const list = ref<AppVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
  pageNo: 1,
  pageSize: 10,
  classifyFlag: undefined,
  appCode: undefined,
  appName: undefined,
  appDesc: undefined,
  statusFlag: undefined,
  databaseType: undefined,
  middleware: undefined,
  os: undefined,
  architectureType: undefined,
  launchDate: [],
  systemDivisionId: undefined,
  systemDivisionName: undefined,
  systemDirectorId: undefined,
  systemDirectorname: undefined,
  systemManagerId: undefined,
  systemManagerName: undefined,
  businessDivisionId: undefined,
  businessDivisionName: undefined,
  businessDirectorId: undefined,
  businessDirectorName: undefined,
  businessManagerId: undefined,
  businessManagerName: undefined,
  remark: undefined,
  createTime: []
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
const getList = async () => {
  loading.value = true
  try {
    const data = await AppApi.getAppPage(queryParams)
    list.value = data.records
    // console.log(data.records)
    list.value = await Promise.all(
      data.records.map(async (item) => {
        // 使用 item.appId 作为参数调用子项接口
        const childrenData = await AppApi.getlistChild({ appGroupId: item.appId })
        return {
          ...item,
          children: childrenData || []
        }
      })
    )
    total.value = data.total
  } catch (error) {
    console.error('获取列表数据失败', error)
    // 可以添加错误提示逻辑
  } finally {
    loading.value = false
  }
}

/** 搜索按钮操作 */
const handleQuery = () => {
  queryParams.pageNo = 1
  getList()
}

/** 重置按钮操作 */
const resetQuery = () => {
  queryFormRef.value.resetFields()
  handleQuery()
}

/** 运维工作项删除按钮操作 */
const handleDelete = async (type?: string, id: number) => {
  try {
    // 删除的二次确认
    await message.delConfirm()
    // 发起删除
    type == 'work'
      ? await AppApi.deleteWork(id)
      : await DefaultEvalItemApi.deleteDefaultEvalItem(id)
    message.success(t('common.delSuccess'))
    // 刷新列表
    await getList()
  } catch {}
}

/** 导出按钮操作 */
const handleExport = async () => {
  try {
    // 导出的二次确认
    await message.exportConfirm()
    // 发起导出
    exportLoading.value = true
    const data = await AppApi.exportApp(queryParams)
    download.excel(data, '应用系统.xls')
  } catch {
  } finally {
    exportLoading.value = false
  }
}
const appGroupId = ref()
/** 查询列表 */
const load = async () => {
  try {
    const data = await AppApi.getlistChild(appGroupId)
    console.log(data)
  } finally {
  }
}
/** 按钮操作 */
const workRef = ref()
const addOpen = (type: string, id?: number) => {
  workRef.value.open(type, id)
}
const evalRef = ref()
const addEval = (type: string, id?: number) => {
  evalRef.value.open(type, id)
}

const formRef = ref()
const addGroupRef = ref()
//应用组与应用的详情
const openDetails = (type: string, id?: number, classifyFlag?: number) => {
  classifyFlag == 1 ? addGroupRef.value.open(type, id) : formRef.value.open(type, id)
}
/** 初始化 **/
onMounted(() => {
  getList()
})
</script>

新增和编辑公用一个弹框的代码

  <Dialog :title="dialogTitle" v-model="dialogVisible">
    <el-form
      ref="formRef"
      :model="formData"
      :rules="formRules"
      label-width="125px"
      v-loading="formLoading"
      label-position="left"
    >
      <el-form-item label="运维要求名称" prop="opsRequireName">
        <el-input
          v-model="formData.opsRequireName"
          placeholder="请输入运维要求名称,长度限制2~20个字符"
          :disabled="disabled"
        />
      </el-form-item>
      <el-form-item label="运维单位名称" prop="opsUnitName">
        <el-input
          v-model="formData.opsUnitName"
          placeholder="输入运维单位名称,长度限制2~20个字符"
          :disabled="disabled"
        />
      </el-form-item>
      <el-form-item label="运维时间" prop="operationTime">
        <el-date-picker
          v-model="formData.operationTime"
          value-format="X"
          type="daterange"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          :default-time="[new Date('1970-01-01'), new Date('1970-01-01')]"
          class="!w-240px"
          :disabled="disabled"
          @change="handleDateRangeChange"
        />
      </el-form-item>
      <el-form-item label="运维工作项选择" prop="opsItemTree">
        <el-button type="primary" plain @click="editWork('edit')" :disabled="disabled">
          <Icon icon="ep:edit" class="mr-5px" /> 编辑
        </el-button>
        <el-button type="primary" plain @click="lookModel" :disabled="disabled">
          <Icon icon="ep:document" class="mr-5px" /> 预览
        </el-button>
      </el-form-item>
      <el-form-item label="备注" prop="remark" label-position="right">
        <el-input v-model="formData.remark" placeholder="请输入备注" :disabled="disabled" />
      </el-form-item>
    </el-form>
    <template #footer v-if="formType != 'detail'">
      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
      <el-button @click="dialogVisible = false">取 消</el-button>
    </template>
    <AddItem ref="modelRef" @model="model" />
    <!-- 预览 -->
    <WorkLook ref="lookRef" />
  </Dialog>
</template>
<script setup lang="ts">
import { DefaultOpsItemApi, DefaultOpsItemVO } from '@/api/md/defaultopsitem'
import AddItem from '@/views/base/assessment/AddItem.vue'
import WorkLook from './WorkLook.vue'
// import { de } from 'element-plus/es/locale'

/** 默认工作项 表单 */
defineOptions({ name: 'DefaultOpsItemForm' })

const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗

const disabled = ref(false) // 弹窗的是否展示
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
  defaultItemId: undefined,
  appId: undefined,
  opsItemId: undefined,
  opsContent: undefined,
  remark: undefined,
  opsRequireName: undefined,
  opsUnitName: undefined,
  opsStartTime: undefined, // 运维开始时间(时间戳)
  opsEndTime: undefined, // 运维结束时间(时间戳)
  operationTime: undefined, // 运维时间(用于前端显示,时间戳数组)
  opsItemTree: undefined // 运维工作项选择
})
const formRef = ref() // 表单 Ref

// 表单验证规则
const formRules = reactive({
  opsRequireName: [{ required: true, message: '运维要求名称不能为空', trigger: 'blur' }],
  opsUnitName: [{ required: true, message: '运维单位名称不能为空', trigger: 'blur' }],
  operationTime: [
    {
      required: true,
      message: '请选择运维时间范围',
      trigger: 'change'
    },
    {
      validator: (rule, value, callback) => {
        if (!value || value.length !== 2) {
          callback(new Error('请选择完整的开始日期和结束日期'))
        } else if (value[0] > value[1]) {
          callback(new Error('开始日期不能大于结束日期'))
        } else {
          // 更新拆分的日期字段(时间戳格式)
          formData.value.opsStartTime = value[0]
          formData.value.opsEndTime = value[1]
          callback()
        }
      },
      trigger: 'change'
    }
  ],
  opsItemTree: [
    { required: true, message: '请选择运维工作项', trigger: 'change' },
    {
      validator: (rule, value, callback) => {
        if (!value || !Array.isArray(value) || value.length === 0) {
          opsItemTreeError.value = '请选择运维工作项'
          callback(new Error('请选择运维工作项'))
        } else {
          opsItemTreeError.value = ''
          callback()
        }
      },
      trigger: 'change'
    }
  ]
})
const workId = ref() // 工作项ID
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
  dialogVisible.value = true
  disabled.value = false
  dialogTitle.value = t('action.' + type)
  formType.value = type
  console.log('打开弹窗的类型:', formType.value) // 调试用
  if (formType.value == 'detail') {
    disabled.value = true
  }
  workId.value = id
  resetForm()
  formData.value.appId = id // 确保 appId 被正确设置
  // 修改时,设置数据
  if (id && formType.value !== 'create') {
    console.log('打开弹窗的ID:', id) // 调试用
    formLoading.value = true
    try {
      const response = await DefaultOpsItemApi.getDefaultOpsItem(id)
      console.log('后端返回的数据:', response) // 调试用
      formData.value = {
        ...response,
        appId: id, // 确保 appId 被保留
        // 正确处理后端返回的时间戳,直接赋值给 operationTime
        operationTime:
          response.opsStartTime && response.opsEndTime
            ? [
                // 确保是秒级时间戳(如果后端返回的是毫秒级,需要除以1000)
                Math.floor(response.opsStartTime),
                Math.floor(response.opsEndTime)
              ]
            : undefined
      }
      console.log('回显的时间戳:', formData.value.operationTime) // 调试用
      formData.value.opsItemTree = response.details
    } finally {
      formLoading.value = false
    }
  }
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗

/** 处理日期范围变化 */
const handleDateRangeChange = (value: [number, number] | null) => {
  if (value && value.length === 2) {
    // 确保是秒级时间戳
    formData.value.opsStartTime = Math.floor(value[0])
    formData.value.opsEndTime = Math.floor(value[1])
  } else {
    formData.value.opsStartTime = undefined
    formData.value.opsEndTime = undefined
  }
}

/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
  // 校验表单
  await formRef.value.validate()
  // 提交请求
  formLoading.value = true
  try {
    // 确保数据中有拆分的日期字段(已经是时间戳格式)
    if (formData.value.operationTime && formData.value.operationTime.length === 2) {
      formData.value.opsStartTime = formData.value.operationTime[0]
      formData.value.opsEndTime = formData.value.operationTime[1]
    }

    const data = { ...formData.value }
    delete data.operationTime // 移除前端使用的日期范围字段

    const payload = data as unknown as DefaultOpsItemVO
    console.log('提交的数据:', payload) // 调试用
    // 实际提交代码
    if (formType.value === 'create') {
      await DefaultOpsItemApi.createDefaultOpsItem(payload)
      message.success(t('common.createSuccess'))
    } else {
      await DefaultOpsItemApi.updateDefaultOpsItem(payload)
      message.success(t('common.updateSuccess'))
    }
    dialogVisible.value = false
    // 发送操作成功的事件
    emit('success')
  } finally {
    formLoading.value = false
  }
}

/** 重置表单 */
const resetForm = () => {
  formData.value = {
    defaultItemId: undefined,
    appId: undefined,
    opsItemId: undefined,
    opsContent: undefined,
    remark: undefined,
    opsRequireName: undefined,
    opsUnitName: undefined,
    opsStartTime: undefined,
    opsEndTime: undefined,
    operationTime: undefined,
    opsItemTree: undefined
  }
  formRef.value?.resetFields()
}

// 运维工作项选择
const modelRef = ref()
const editWork = (type: string) => {
  modelRef.value.open(type, workId.value)
}
const model = (data: any) => {
  console.log('接收到子组件传递的数据:', data)
  formData.value.opsItemTree = data
  // 手动触发验证
  formRef.value?.validateField('opsItemTree')
}
const opsItemTreeError = ref('') // 新增:运维工作项选择错误提示

// 预览模板
const lookRef = ref()
const lookModel = () => {
  console.log(formData.value.opsItemTree)
  const data = { items: formData.value.opsItemTree }
  lookRef.value.open(data)
}
</script>

弹框中点击编辑打开弹框的树形数据页面(这个页面的代码在其他模块中也有用到,属于共用的)
  <Dialog
    :title="formType === 'edit' ? '运维工作项选择' : '运维考核项选择'"
    v-model="dialogVisible"
    width="60%"
  >
    <div class="container">
      <el-card class="box-card">
        <!-- 新增表单区域 -->
        <el-form
          :model="formData"
          label-width="100px"
          class="mb-4"
          ref="GroupRef"
          :rules="formRules"
          v-if="!['edit', 'add'].includes(formType)"
        >
          <el-form-item label="模板名称" prop="templateName">
            <el-input
              v-model="formData.templateName"
              placeholder="请输模板名称,长度限制在2~20个字符"
            />
          </el-form-item>
          <el-form-item label="模板描述" prop="templateDesc">
            <el-input
              v-model="formData.templateDesc"
              placeholder="请输入模板描述,长度限制在2~200个字符"
            />
          </el-form-item>
        </el-form>
        <div class="flex">
          <!-- 左侧:所有考核项树形表格 -->
          <div class="w12 pr-4">
            <el-card class="box-card mb-4">
              <template #header>
                <div class="clearfix">
                  <span>{{ formType === 'edit' ? '所有工作项' : '所有考核项' }}</span>
                  <el-button
                    @click="toggleAllSelection"
                    style="float: right; padding: 3px 0"
                    type="text"
                  >
                    {{ isAllSelected ? '取消全选' : '全选' }}
                  </el-button>
                </div>
              </template>
              <div>
                <el-table
                  ref="tableRef"
                  :data="tableData"
                  row-key="evalItemId"
                  :tree-props="{ children: 'details', hasChildren: 'hasChildren' }"
                  @selection-change="handleSelectionChange"
                  @row-click="handleRowClick"
                  border
                  fit
                  highlight-current-row
                  show-overflow-tooltip
                >
                  <el-table-column type="selection" width="55" :reserve-selection="true" />
                  <el-table-column
                    prop="evalItemName"
                    :label="formType === 'edit' ? '工作项名称' : '考核项名称'"
                  />
                  <el-table-column
                    prop="evalContent"
                    :label="formType === 'edit' ? '工作内容' : '考核内容'"
                    show-overflow-tooltip
                  />
                </el-table>
              </div>
            </el-card>
          </div>
          <!-- 右侧:已选考核项树形表格 -->
          <div class="w12 pl-4">
            <el-card class="box-card mb-4">
              <template #header>
                <div class="clearfix">
                  <span>{{ formType === 'edit' ? '已选工作项' : '已选考核项' }}</span>
                  <div class="flex float-right">
                    <el-button
                      type="text"
                      v-if="!['edit', 'add'].includes(formType)"
                      @click="lookModel"
                    >
                      预览
                    </el-button>
                    <el-button
                      type="text"
                      @click="batchRemoveSelected"
                      :disabled="selectedRemoveData.length === 0"
                      style="padding: 3px 8px"
                    >
                      批量移除
                    </el-button>
                  </div>
                </div>
              </template>
              <div>
                <el-table
                  ref="selectedTableRef"
                  :data="formattedSelectedData"
                  row-key="evalItemId"
                  :tree-props="{ children: 'details', hasChildren: 'hasChildren' }"
                  @row-click="handleSelectedRowClick"
                  @selection-change="handleSelectedSelectionChange"
                  border
                  fit
                  highlight-current-row
                  show-overflow-tooltip
                >
                  <!-- 添加选择列 -->
                  <el-table-column type="selection" width="55" />
                  <el-table-column
                    prop="evalItemName"
                    :label="formType === 'edit' ? '工作项名称' : '考核项名称'"
                  />
                  <el-table-column
                    prop="evalContent"
                    :label="formType === 'edit' ? '工作内容' : '考核内容'"
                    show-overflow-tooltip
                  />
                  <el-table-column label="操作" width="80">
                    <template #default="scope">
                      <el-button
                        type="text"
                        size="small"
                        @click.stop="removeItem(scope.row)"
                        style="color: #f56c6c"
                      >
                        移除
                      </el-button>
                    </template>
                  </el-table-column>
                </el-table>
              </div>
              <div class="mt-4 text-right">
                <span>已选择: {{ selectedData.length }} 项</span>
              </div>
            </el-card>
          </div>
        </div>
      </el-card>
      <div class="footer">
        <el-button type="primary" @click="submitSelection" :disabled="selectedData.length === 0"
          >确定</el-button
        >
        <el-button @click="dialogVisible = false">取消</el-button>
      </div>
    </div>
    <LookModel ref="modelRef" />
  </Dialog>
</template>

<script setup>
import { ref, reactive, onMounted, watch, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { TemplateEvalItemApi } from '@/api/md/templateevalitem'
import { DefaultOpsItemApi } from '@/api/md/defaultopsitem' // 运维工作要求
import { DefaultEvalItemApi } from '@/api/md/DefaultEvalItemForm' // 运维考核要求
import { EvalTemplateApi } from '@/api/md/evaltemplate'
import LookModel from './LookModel.vue'
const message = useMessage() // 消息弹窗

// 表格数据
const tableRef = ref(null)
const selectedTableRef = ref(null)
const tableData = ref([])
const selectedData = ref([])
const selectedRemoveData = ref([]) // 右侧表格选中的待移除数据
const formattedSelectedData = ref([])
const isAllSelected = ref(false)
const loading = ref(false)
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formType = ref('')
const formData = ref({
  templateName: undefined,
  templateDesc: undefined,
  templateId: undefined
})
const formLoading = ref(false)
const queryParams = reactive({
  pageNo: 1,
  pageSize: 10,
  templateId: undefined,
  evalItemId: undefined,
  parentItemId: undefined
})
const GroupRef = ref()
const formRules = reactive({
  templateName: [
    { required: true, message: '模板名称不能为空', trigger: 'blur' },
    { min: 2, max: 20, message: '长度限制在2~20个字符', trigger: 'blur' }
  ],
  templateDesc: [
    { required: true, message: '模板描述不能为空', trigger: 'blur' },
    { min: 2, max: 200, message: '长度限制在2~200个字符', trigger: 'blur' }
  ]
})
//预览模板
const modelRef = ref()
const lookModel = () => {
  const data = {
    templateName: formData.value.templateName,
    templateDesc: formData.value.templateDesc,
    templateId: formData.value.templateId,
    items: formattedSelectedData.value
  }
  modelRef.value.open(data)
}

// 获取接口数据
const fetchData = async (selectedItemIds = []) => {
  loading.value = true
  try {
    const data =
      formType.value === 'edit' // 编辑模式下,获取运维工作项树形结构
        ? await TemplateEvalItemApi.getTemplateOpsItemPage(queryParams)
        : await TemplateEvalItemApi.getTemplateEvalItemPage(queryParams) // 运维考核项树形结构
    tableData.value = data

    // 处理树形结构,确保每个节点都有details属性
    processTreeData(tableData.value)

    // 如果提供了已选ID列表,设置初始勾选状态
    if (selectedItemIds && selectedItemIds.length > 0) {
      await nextTick()
      setInitialSelection(selectedItemIds)
    }
  } catch (error) {
    console.error(error)
  } finally {
    loading.value = false
  }
}

// 确保所有节点都有正确的子节点结构
const processTreeData = (items) => {
  items.forEach((item) => {
    if (!item.details) {
      item.details = []
    }

    if (item.details && item.details.length > 0) {
      processTreeData(item.details)
    }
  })
}

// 设置初始勾选状态
const setInitialSelection = (selectedItemIds) => {
  if (
    !tableData.value ||
    tableData.value.length === 0 ||
    !selectedItemIds ||
    selectedItemIds.length === 0
  )
    return

  const selectedIdSet = new Set(selectedItemIds)

  const processNodes = (items) => {
    items.forEach((item) => {
      if (selectedIdSet.has(item.evalItemId)) {
        tableRef.value?.toggleRowSelection(item, true)

        if (item.details && item.details.length > 0) {
          item.details.forEach((child) => {
            selectedIdSet.add(child.evalItemId)
            processNodes(item.details)
          })
        }
      } else {
        tableRef.value?.toggleRowSelection(item, false)
      }
    })
  }

  processNodes(tableData.value)
}

// 处理选中状态变化
const handleSelectionChange = (val) => {
  selectedData.value = val
  formatSelectedData()

  const allRows = flattenTreeData(tableData.value)
  isAllSelected.value = allRows.length > 0 && val.length === allRows.length
}

// 右侧表格选中状态变化
const handleSelectedSelectionChange = (val) => {
  selectedRemoveData.value = val
}

// 处理行点击 - 左侧表格
const handleRowClick = (row) => {
  const isSelected = selectedData.value.some((item) => item.evalItemId === row.evalItemId)
  tableRef.value?.toggleRowSelection(row, !isSelected)
}

// 处理已选表格行点击 - 右侧表格
const handleSelectedRowClick = (row) => {
  removeItem(row)
}

// 移除单个项目
const removeItem = (row) => {
  // 收集要移除的项目ID(包括子节点)
  const itemIdsToRemove = new Set()
  itemIdsToRemove.add(row.evalItemId)

  // 如果是父节点,递归收集所有子节点ID
  if (row.details && row.details.length > 0) {
    flattenTreeData([row]).forEach((child) => {
      itemIdsToRemove.add(child.evalItemId)
    })
  }

  // 批量取消左侧表格的选择状态
  batchDeselectItems(itemIdsToRemove)

  // 从selectedData中移除这些项目
  selectedData.value = selectedData.value.filter((item) => !itemIdsToRemove.has(item.evalItemId))

  // 清空右侧表格的选择
  selectedTableRef.value?.clearSelection()

  // 强制刷新右侧表格
  formatSelectedData()
}

// 批量移除选中项目
const batchRemoveSelected = () => {
  if (selectedRemoveData.value.length === 0) {
    ElMessage.warning('请先选择要移除的项目')
    return
  }

  // 收集所有要移除的项目ID(包括子节点)
  const itemIdsToRemove = new Set()
  selectedRemoveData.value.forEach((row) => {
    itemIdsToRemove.add(row.evalItemId)

    // 收集子节点ID
    if (row.details && row.details.length > 0) {
      flattenTreeData([row]).forEach((child) => {
        itemIdsToRemove.add(child.evalItemId)
      })
    }
  })

  // 批量取消左侧表格的选择状态
  batchDeselectItems(itemIdsToRemove)

  // 从selectedData中移除这些项目
  selectedData.value = selectedData.value.filter((item) => !itemIdsToRemove.has(item.evalItemId))

  // 清空右侧表格的选择
  selectedTableRef.value?.clearSelection()

  // 强制刷新右侧表格
  formatSelectedData()

  // ElMessage.success(`已移除 ${selectedRemoveData.value.length} 个项目`)
}

// 批量取消选择项目
const batchDeselectItems = (itemIdsToRemove) => {
  nextTick(() => {
    // 遍历左侧表格数据,取消选择状态
    const allRows = flattenTreeData(tableData.value)
    allRows.forEach((item) => {
      if (itemIdsToRemove.has(item.evalItemId)) {
        tableRef.value?.toggleRowSelection(item, false)
      }
    })
  })
}

// 全选/取消全选
const toggleAllSelection = () => {
  if (isAllSelected.value) {
    tableRef.value?.clearSelection()
  } else {
    tableRef.value?.clearSelection()
    const allRows = flattenTreeData(tableData.value)
    allRows.forEach((row) => {
      tableRef.value?.toggleRowSelection(row, true)
    })
  }
}

// 清空所有选择
const clearSelected = () => {
  tableRef.value?.clearSelection()
  selectedTableRef.value?.clearSelection()
  // 清空selectedData
  selectedData.value = []
}
const emit = defineEmits(['model'])

// 展开所有节点
const expandAll = () => {
  const allRows = flattenTreeData(tableData.value)
  allRows.forEach((row) => {
    tableRef.value?.toggleRowExpansion(row, true)
  })
}
// 提交选中数据
const submitSelection = async () => {
  // 模拟提交数据到服务器
  console.log('提交选中的数据:', formattedSelectedData.value)
  // ElMessage.success(`已提交 ${selectedData.value.length} 个选中节点`)
  const data = {
    templateName: formData.value.templateName,
    templateDesc: formData.value.templateDesc,
    templateId: formData.value.templateId,
    items: formattedSelectedData.value
  }
  // 当表单类型为'edit'或'add'时,直接触发model事件
  if (formType.value === 'edit' || formType.value === 'add') {
    dialogVisible.value = false
    emit('model', formattedSelectedData.value)
    return
  }
  await GroupRef.value.validate()
  try {
    if (formType.value === 'create') {
      await EvalTemplateApi.createEvalTemplate(data)
      message.success('新增成功')
    } else {
      await EvalTemplateApi.updateEvalTemplate(data)
      message.success('修改成功')
    }
    dialogVisible.value = false
    // 发送操作成功的事件
    emit('success')
  } catch (error) {
    // message.error(t('common.requestError'))
  } finally {
    formLoading.value = false
  }
}
// 工具函数:将树形数据展平
const flattenTreeData = (treeData) => {
  let result = []
  treeData.forEach((item) => {
    result.push(item)
    if (item.details && item.details.length > 0) {
      result = result.concat(flattenTreeData(item.details))
    }
  })
  return result
}

// 格式化选中的数据
const formatSelectedData = () => {
  const selectedIds = selectedData.value.map((item) => item.evalItemId)
  const formattedData = []

  const processNode = (node) => {
    if (selectedIds.includes(node.evalItemId)) {
      const formattedNode = { ...node }

      if (node.details && node.details.length > 0) {
        formattedNode.details = node.details
          .map((child) => processNode(child))
          .filter((child) => child !== null)
      }

      return formattedNode
    }

    if (node.details && node.details.length > 0) {
      const selectedChildren = node.details
        .map((child) => processNode(child))
        .filter((child) => child !== null)

      if (selectedChildren.length > 0) {
        const formattedNode = { ...node, details: selectedChildren }
        return formattedNode
      }
    }

    return null
  }

  tableData.value.forEach((node) => {
    const formattedNode = processNode(node)
    if (formattedNode !== null) {
      formattedData.push(formattedNode)
    }
  })

  formattedSelectedData.value = formattedData

  nextTick(() => {
    const allSelectedRows = flattenTreeData(formattedSelectedData.value)
    allSelectedRows.forEach((row) => {
      selectedTableRef.value?.toggleRowExpansion(row, true)
    })
  })
}
/** 打开弹窗 */
const open = async (type, id) => {
  dialogVisible.value = true
  // dialogTitle.value = type
  console.log(type, id)
  resetForm()
  formType.value = type
  if (id) {
    formLoading.value = true
    try {
      const templateData =
        formType.value === 'edit'
          ? await DefaultOpsItemApi.getDefaultOpsItem(id)
          : formType.value === 'add'
            ? await DefaultEvalItemApi.getDefaultEvalItem(id)
            : await EvalTemplateApi.getEvalTemplate(id)

      formData.value = templateData
      // debugger
      const selectedItemIds = templateData.details?.map((item) => item.evalItemId) || []

      await fetchData(selectedItemIds)
    } catch (error) {
      ElMessage.error('加载数据失败')
      console.error(error)
    } finally {
      formLoading.value = false
    }
  } else {
    await fetchData()
  }
}

/** 重置表单 */
const resetForm = () => {
  formData.value = {
    templateName: undefined,
    templateDesc: undefined,
    templateId: undefined
  }

  nextTick(() => {
    tableRef.value?.clearSelection()
    selectedTableRef.value?.clearSelection()
    selectedData.value = []
    selectedRemoveData.value = []
  })
}

defineExpose({ open })

// 初始化加载数据
onMounted(() => {
  // 示例:模拟打开弹窗并加载已有数据
  // open('edit', 1)
})

// 监听表格数据变化
watch(
  tableData,
  (newVal) => {
    formatSelectedData()
  },
  { deep: true }
)

// 监听选中数据变化
watch(
  selectedData,
  (newVal) => {
    formatSelectedData()
  },
  { deep: true }
)
</script>

<style scoped>
.container {
  padding: 20px;
  max-width: 1600px;
  margin: 0 auto;
}

.box-card {
  margin-bottom: 20px;
  height: calc(100% - 20px);
}

.mt-4 {
  margin-top: 10px;
}

.flex {
  display: flex;
}

.w12 {
  width: 50%;
}

.pr-4 {
  padding-right: 10px;
}

.pl-4 {
  padding-left: 10px;
}

.justify-between {
  justify-content: space-between;
}
</style>

最后是点击预览打开的页面

  <Dialog
    title="预览"
    v-model="dialogVisible"
    width="50%"
    style="max-height: 90vh; overflow-y: auto"
  >
    <div class="container mx-auto p-4">
      <el-table :data="tableData" border fit highlight-current-row :span-method="cellMerge">
        <el-table-column label="分组" min-width="300">
          <template #default="scope">
            <div class="parent-node">
              {{ scope.row.isParent ? scope.row.name : '' }}
            </div>
          </template>
        </el-table-column>
        <el-table-column label="描述内容" min-width="120">
          <template #default="scope">
            {{ scope.row.isParent ? scope.row.value : '' }}
          </template>
        </el-table-column>
        <el-table-column label="工作项" min-width="100">
          <template #default="scope">
            {{ scope.row.isChild ? scope.row.name : '' }}
          </template>
        </el-table-column>
        <el-table-column label="评估内容" min-width="100">
          <template #default="scope">
            {{ scope.row.isChild ? scope.row.value : '' }}
          </template>
        </el-table-column>
      </el-table>
    </div>
  </Dialog>
</template>

<script setup>
import { ref, computed } from 'vue'
const dialogVisible = ref(false)
const treeData = ref([])

/** 打开弹窗 */
const open = async (data) => {
  console.log('data', data)
  treeData.value = data.items
  dialogVisible.value = true
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗

// 将 formattedData 改为 computed 属性,使其依赖于 treeData 的变化
const formattedData = computed(() => {
  return treeData.value.map((item) => ({
    id: item.evalItemId,
    name: item.evalItemName,
    value: item.evalContent,
    type: '分组',
    children: item.details?.map((detail) => ({
      id: detail.evalItemId,
      name: detail.evalItemName,
      value: detail.evalContent,
      type: '考核项',
      children: detail.details
    }))
  }))
})

// 处理表格数据
const tableData = computed(() => {
  const result = []

  function processNodes(nodes, level = 0, parentId = null) {
    nodes.forEach((node) => {
      const row = {
        ...node,
        level,
        parentId,
        isParent: level === 0,
        childCount: node.children ? node.children.length : 0,
        isChild: level > 0
      }

      result.push(row)

      if (node.children && node.children.length > 0) {
        processNodes(node.children, level + 1, node.id)
      }
    })
  }

  processNodes(formattedData.value)
  return result
})

// 单元格合并方法
const cellMerge = ({ row, column, rowIndex, columnIndex }) => {
  // 只处理第一列和第二列的合并
  if (columnIndex < 2) {
    // 父节点的情况
    if (row.isParent) {
      // 查找下一个父节点的索引
      let nextParentIndex = rowIndex + 1
      while (
        nextParentIndex < tableData.value.length &&
        !tableData.value[nextParentIndex].isParent
      ) {
        nextParentIndex++
      }

      // 计算需要合并的行数
      const rowspan = nextParentIndex - rowIndex
      if (rowspan > 1) {
        return {
          rowspan,
          colspan: 1
        }
      }
    } else {
      // 子节点的情况 - 不显示内容
      return {
        rowspan: 0,
        colspan: 0
      }
    }
  } else if (columnIndex >= 2) {
    // 第三列和第四列 - 子节点显示,父节点不显示
    if (row.isChild) {
      return {
        rowspan: 1,
        colspan: 1
      }
    } else {
      return {
        rowspan: 0,
        colspan: 0
      }
    }
  }

  // 默认不合并
  return {
    rowspan: 1,
    colspan: 1
  }
}
</script>

<style scoped>
.container {
  max-width: 1200px;
}

.parent-node {
  font-weight: bold;
  padding: 8px 0;
}
</style>