<template> <el-dialog v-if="show" v-model="show" width="94vw" class="dialog-okr" :show-close="false" :close-on-click-modal="false" > <template #header> <div class="dialog-okr-header"> <div class="flex-1 flex items-center" style="line-height: 50px"> <div class="dialog-okr-title"> <Icon icon="ep:circle-check-filled" color="#30d1fc" :size="14" /> <span>目标</span> </div> <el-divider direction="vertical" /> <span class="text-14px ml-0.25">ork落地</span> <div class="ml-20px text-14px"> <span>【节点】</span> <span>{{ nodeInfo.allNodeName }}</span> </div> </div> <div class="flex items-center"> <el-button link @click="emit('edit')"> <el-tooltip content="编辑" placement="top" effect="dark"> <Icon icon="ep:edit" :size="16" /> </el-tooltip> </el-button> <el-button link @click="show = false"> <el-tooltip content="关闭" placement="top" effect="dark"> <Icon icon="ep:close" :size="16" /> </el-tooltip> </el-button> </div> </div> </template> <template #default> <div class="dialog-okr-body"> <div class="dialog-okr-content pr-10px"> <div class="dialog-okr-content-detail"> <div class="detail-basic-title"> <div class="basic-title-item"> <div class="basic-title-label">创建人</div> <div class="basic-title-value">{{ nodeInfo.creator }}</div> </div> <div class="basic-title-item"> <div class="basic-title-label">执行人</div> <div class="basic-title-value">{{ nodeInfo.executorName }}</div> </div> <div class="basic-title-item"> <div class="basic-title-label">目标数</div> <div class="basic-title-value">{{ okrList.length }}</div> </div> <div class="basic-title-item" style="min-width: 200px"> <div class="basic-title-label">总体进度</div> <el-progress :percentage="nodeInfo.progress || 0" :stroke-width="8" /> </div> </div> <div class="flex detail-basic-info"> <span>开始日期:{{ nodeInfo.startTime }}</span> <span>截止日期:{{ nodeInfo.endTime }}</span> <span>最新更新时间:{{ nodeInfo.updateTime }}</span> </div> </div> <div class="flex" style="position: relative; flex: 1; height: 100px"> <el-tabs v-model="workIndex" style="flex: 1"> <el-tab-pane label="目标/关键成果" name="okr"> <div class="content-wrap"> <OkrTable ref="okrTableRef" :canEdit="canEdit" /> </div> </el-tab-pane> </el-tabs> <el-button class="sav-btn" type="primary" @click="handleSaveProcess"> 保存并更新 </el-button> </div> </div> <div class="dialog-okr-side pl-10px"> <el-tabs v-model="sideIndex" style="flex: 1; height: 100%"> <el-tab-pane label="子节点" name="subNode"> <div class="overflow-y-auto" style="height: calc(100% - 50px)"> <div v-for="item in childNodeList" :key="item.nodeId" class="border-b-1 child-item" style="padding: 10px 5px; cursor: pointer" @click="handleChildItem" > <div class="flex justify-between items-center overflow-hidden text-16px" style="line-height: 30px" > <div class="child-label">【节点】{{ item.nodeName }}</div> <el-progress type="line" :percentage="item.progress || 0" :stroke-width="6" style="width: 120px" /> </div> <div class="ml-10px flex items-center text-13px" style="line-height: 30px"> <span>目标数:</span> <span style="color: #aaa">{{ item.objectiveCount }}</span> <el-divider direction="vertical" style="margin: 0 20px" /> <div class="flex-1 overflow-hidden h-30px" style="text-overflow: ellipsis; white-space: nowrap" > <span>执行人:</span> <span style="color: #aaa">{{ item.executorName }}</span> </div> </div> <div class="ml-10px flex items-center text-12px" style="line-height: 20px; color: #aaa" > <span>开始日期:{{ item.startTime }}</span> <el-divider direction="vertical" style="margin: 0 20px" /> <span>截止日期:{{ item.endTime }}</span> </div> </div> </div> </el-tab-pane> <el-tab-pane label="评论" name="conclusion"> <div class="relative overflow-y-auto" style="height: calc(100% - 50px)"> <div class="flex justify-between items-center"> <el-select v-if="addNewComment" v-model="form.commentType" filterable size="small" style="width: 120px" @change="getCommentTemplate" > <el-option v-for="it in commentTypeOptions" :label="it.label" :value="it.id" :key="it.id" /> </el-select> <div v-if="addNewComment"> <el-button size="small" @click="addNewComment = false"> 取消 </el-button> <el-button type="primary" size="small" @click="handleSaveComment"> 发布 </el-button> </div> <el-button v-else type="primary" size="small" @click="handleInsertComment"> 新增评论 </el-button> </div> <div class="mt-10px" v-if="addNewComment"> <Editor v-model:modelValue="form.commentValue" height="300px" :toolbarConfig="toolbarConfig" /> </div> <div v-for="(it, index) in commentList" :key="it.commentId" class="border-b-1" style="padding: 10px 5px" > <div class="flex items-center justify-between overflow-hidden text-16px" style="line-height: 30px" > <div class="flex items-center"> <el-avatar shape="circle" style=" background-color: var(--el-color-primary-light-3); width: 30px; height: 30px; " fit="fill" > <span class="text-12px">{{ it.creatorName.slice(-2) }}</span> </el-avatar> <div class="ml-10px text-16px">{{ it.creatorName }}</div> </div> </div> <div class="ml-10px" v-dompurify-html="it.content"></div> <div class="ml-10px mt-10px flex items-center justify-between text-12px" style="line-height: 20px; color: #aaa" > <div class="flex items-center"> <div class="flex items-center mr-50px"> <el-button link @click="good(it)"> <Icon icon="fa:thumbs-o-up" :size="16" :color="it.currentUserIsLike ? 'var(--el-color-primary)' : '#333'" /> </el-button> <span class="ml-5px" :style="{ color: it.currentUserIsLike ? 'var(--el-color-primary)' : '#333' }" >{{ it.likeCount }}</span > </div> <div class="flex items-center mr-50px"> <el-button link @click="showComment(index)"> <Icon icon="ep:chat-dot-square" :size="16" color="#333" /> </el-button> <span class="ml-5px" style="color: #333">{{ it.commentCount }}</span> </div> </div> <div class="ml-10px text-13px text-gray-400"> {{ formatDate(it.createTime, 'YYYY-MM-DD HH:mm') }} </div> </div> <!-- 评论 --> <div v-if="showCommentIndex == index" class="bg-gray-100 pl-10px pr-10px pt-5px pb-5px" style="margin: 10px 10px 0 10px; border-radius: 4px" label="评论" > <div v-for="subComment in it.children.sort((a, b) => a.createTime - b.createTime)" :key="subComment.commentId" class="text-14px" style="line-height: 24px" > <span class="font-bold">{{ subComment.creatorName }}:</span> <span> {{ subComment.content }} </span> </div> <div class="mt-10px relative"> <!-- <el-input v-model="form.commentValue" placeholder="请输入评论" type="textarea" :autosize="{ minRows: 4 }" clearable size="small" style="width: 100%" /> --> <el-mention v-model="form.commentValue" type="textarea" :autosize="{ minRows: 4 }" :options="employeeOptions" style="width: 100%" size="small" whole placeholder="请输入评论" @select="handleMention" > <template #label="{ item }"> <div class="flex items-center justify-between h-full"> <span class="text-14px text-dark-700">{{ item.name }}</span> <span class="text-12px text-gray-400">{{ item.dept }}</span> </div> </template> </el-mention> <el-button type="primary" size="small" style="position: absolute; right: 2px; bottom: 2px" @click="handleSendCommnet(index)" > 发布 </el-button> </div> </div> </div> </div> </el-tab-pane> <el-tab-pane label="进度历史" name="history"> <div class="overflow-y-auto pl-15px" style="height: calc(100% - 50px)"> <el-timeline class="ml-10px"> <el-timeline-item v-for="item in nodeRecords" :key="item.recordId" placement="bottom" :timestamp="formatDate(item.createTime, 'YYYY-MM-DD HH:mm:ss')" color="#30d1fc" > <div>{{ item.creator }}</div> <div class="mt-10px text-14px" style="line-height: 24px; color: #666"> {{ item.content }} </div> </el-timeline-item> </el-timeline> </div> </el-tab-pane> </el-tabs> </div> </div> </template> </el-dialog> </template> <script setup name="DialogOkr"> import { formatDate } from '@/utils/formatTime' import OkrTable from './OkrTable.vue' import { getOkrNodeDetail, getOkrNodeHistory } from '@/api/okr/okr' import { getCommentTypeOptions, createComment, getCommentPage, likeComment } from '@/api/okr/comment' import { listToTree } from '@/utils/tree' import { getEmployeeSimpleList } from '@/api/pers/employee' const message = useMessage() const emit = defineEmits(['edit']) const show = ref(false) const canEdit = ref(false) const toolbarConfig = { toolbarKeys: [ 'bold', // 加粗 'underline', // 下划线 'italic', // 斜体 'color', // 文字颜色 'bgColor', // 背景色 'fontSize', // 字号 'bulletedList', // 无序列表 'numberedList', // 有序列表 'insertTable', // 插入表格 'insertLink', // 插入链接 'undo' // 撤销 ] } const workIndex = ref('okr') const okrList = ref([]) const childNodeList = ref([]) const sideIndex = ref('subNode') const okrTableRef = ref(null) const nodeInfo = ref({}) const nodeRecords = ref([]) const commentTypeOptions = ref([]) async function open(curNode) { canEdit.value = curNode.canEdit nodeInfo.value.nodeId = curNode.nodeId // 获取数据详情 searchInfo(curNode) show.value = true } const employeeOptions = ref([]) function searchInfo(curNode) { try { getOkrNodeDetail(curNode.nodeId).then((resp) => { nodeInfo.value = resp if (resp.objectives) { okrList.value = resp.objectives.map((item) => ({ ...item, keyResults: item.keyResults || [] })) } else { okrList.value = [] } childNodeList.value = [...resp.children] nextTick(() => { okrTableRef.value.prepareData(okrList.value) }) }) getOkrNodeHistory(curNode.nodeId).then((resp) => { nodeRecords.value = resp }) getEmployeeSimpleList({ status: 0 }).then((resp) => { employeeOptions.value = resp.map((item) => ({ ...item, label: item.name, value: item.name })) }) getCommentTypeOptions().then((resp) => { commentTypeOptions.value = resp }) searchCommentList() } finally { } } function close() { show.value = false } defineExpose({ open, close }) function handleMention(item) { form.value.mentionedUserIdList.push(item.id) } function handleSaveProcess() { okrTableRef.value.updateProcess(nodeInfo.value.nodeId).then(() => { message.success('更新成功') searchInfo() }) } function handleChildItem() { console.log('handleChildItem') } const addNewComment = ref(false) const form = ref({ commentValue: '', commentType: undefined, mentionedUserIdList: [] }) function handleInsertComment() { addNewComment.value = true const defaultComment = commentTypeOptions.value[0] if (defaultComment) { form.value = { commentType: defaultComment.id, mentionedUserIdList: [] } } getCommentTemplate() } function getCommentTemplate() { if (form.value.commentType) { form.value.commentValue = commentTypeOptions.value.find( (item) => item.id == form.value.commentType ).remark } else { form.value.commentValue = `<h2 style=\"text-align: start;\">一、工作概况</h2><p> </p><h2 style=\"text-align: start;\">二、数据统计</h2><ol><li>呼出电话数:</li><li>有效沟通数:</li><li>销售成果</li></ol><h2 style=\"text-align: start;\">三、问题与改进方案</h2><p><br></p>` } } function handleSaveComment() { addNewComment.value = false try { const data = { businessType: 1, businessId: nodeInfo.value.nodeId, contentType: 1, content: form.value.commentValue, mentionedUserIdList: form.value.mentionedUserIdList } createComment(data) .then(() => { message.success('评论成功') searchCommentList() }) .finally(() => { form.value.commentValue = '' }) } catch (error) { message.error('评论失败') } } const commentList = ref([]) function searchCommentList() { getCommentPage({ businessType: 1, businessId: nodeInfo.value.nodeId, pageNum: 1, pageSize: -1 }).then((resp) => { commentList.value = listToTree(resp.list, { id: 'commentId', pid: 'parentId', children: 'children' }) }) } function good(item) { likeComment(item.commentId).then(() => { message.success('点赞成功') searchCommentList() }) } const showCommentIndex = ref(-1) function showComment(index) { showCommentIndex.value = showCommentIndex.value == index ? -1 : index } function handleSendCommnet(idx) { try { // 过滤掉删除的用户,方式为遍历mentionedUserIdList,查找评论中是否有对应的用户名 const userList = [...form.value.mentionedUserIdList] const arr = [] userList.map((item) => { if (form.value.commentValue.indexOf(`@${item.name}`) != -1) { arr.push(item.id) // 然后移除对应的用户名,防止有多个 form.value.commentValue = form.value.commentValue.replace(`@${item.name}`, '') } }) const data = { businessType: 1, businessId: nodeInfo.value.nodeId, contentType: 1, content: form.value.commentValue, mentionedUserIdList: arr, parentId: commentList.value[idx].commentId } createComment(data) .then(() => { message.success('评论成功') searchCommentList() }) .finally(() => { form.value.commentValue = '' }) } catch (error) { message.error('评论失败') } } </script> <style lang="scss" scoped> .dialog-okr { .dialog-okr-header { display: flex; height: 51px; min-height: 51px; vertical-align: middle; justify-content: space-between; -webkit-box-align: center; -ms-flex-align: center; align-items: center; padding: 0 1.25rem; .dialog-okr-title { display: flex; margin-right: 5px; align-items: center; background: rgba(48, 209, 252, 0.1); border-radius: 3px; padding: 0 10px; line-height: 24px; span { color: #2a344b; font-size: 12px; margin-left: 6px; } } } .dialog-okr-body { display: flex; background: #f0f3fa; overflow: hidden; min-width: 1064px; height: calc(94vh - 85px); .dialog-okr-content { display: flex; flex-direction: column; height: 100%; width: 70%; flex: auto; min-width: 700px; background: #fff; margin: 0 1px 0 0; justify-content: space-between; overflow-y: hidden !important; .dialog-okr-content-detail { position: relative; margin-bottom: 30px; overflow-y: auto; display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -ms-flex-direction: column; flex-direction: column; .detail-basic-title { display: flex; font-size: 14px; margin-bottom: 16px; .basic-title-item { margin-right: 50px; .basic-title-label { color: #aaa; margin-bottom: 6px; } .basic-title-value { color: #2a344b; font-size: 14px; } } } .detail-basic-info { font-size: 14px; color: #aaa; span { margin-right: 40px; } } } .content-wrap { overflow-y: auto; max-height: calc(100% - 70px); } } .dialog-okr-side { padding-left: 10px; display: flex; flex-direction: column; width: 30%; overflow: hidden; min-width: 364px; max-width: 500px; background: #fff; margin: 0; .child-item:hover { background: #f0f3fa; } } } } .sav-btn { position: absolute; right: 10px; top: 0; width: auto; } </style>