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