Przeglądaj źródła

feat: 新增我发出的日志模块,新增日志详情页面

qiny 1 rok temu
rodzic
commit
7612f6850a

+ 1 - 1
client/.env

@@ -2,7 +2,7 @@
 VITE_APP_TITLE=浙江万维空间企业管理系统
 
 # 项目本地运行端口号
-VITE_PORT=80
+VITE_PORT=3000
 
 # open 运行 npm run dev 时自动打开浏览器
 VITE_OPEN=flase

+ 24 - 0
client/src/router/modules/remaining.ts

@@ -207,6 +207,30 @@ const remainingRouter: AppRouteRecordRaw[] = [
         meta: {
           title: '档案详情'
         }
+      },
+      {
+        path: 'dailyLogDetail',
+        component: () => import('@/views/OaSystem/personnelManagement/dailyDetail/index.vue'),
+        name: 'dailyLogDetail',
+        meta: {
+          title: '日报详情'
+        }
+      },
+      {
+        path: 'weeklyLogDetail',
+        component: () => import('@/views/OaSystem/personnelManagement/weeklyDetail/index.vue'),
+        name: 'weeklyLogDetail',
+        meta: {
+          title: '周报详情'
+        }
+      },
+      {
+        path: 'mySendLog',
+        component: () => import('@/views/OaSystem/personnelManagement/mySendLog/index.vue'),
+        name: 'mySendLog',
+        meta: {
+          title: '我发出的日志'
+        }
       }
     ]
   },

+ 11 - 3
client/src/views/OaSystem/personnelManagement/dailyCenter/editorDetail.vue

@@ -66,13 +66,13 @@ interface IProp {
 }
 const props = defineProps<IProp>()
 const { writeData } = toRefs(props)
-
 const isUpDate = ref(false) // 是否是更新日志
 
 watch(
   () => writeData.value?.date,
   () => {
     initLog()
+    console.log('writeData', writeData.value)
   }
 )
 
@@ -132,7 +132,7 @@ const sendReportHandle = async (isTemp) => {
     message.warning('请对工作量进行分配')
     return
   }
-  const params = {
+  const params: any = {
     userId: userInfo.id ?? '',
     deptId: userInfo.deptId ?? '',
     reportStartDate: moment(writeData.value.date).valueOf(),
@@ -148,9 +148,17 @@ const sendReportHandle = async (isTemp) => {
     reportWeek: writeData.value.week, // 周
     reportDay: writeData.value.dayOfWeek // 天
   }
+  // 如果是更新的话要传一下报告id
+  if (isUpDate.value) {
+    const reportId = writeData.value?.isLog?.id ?? ''
+    params.reportId = reportId
+  }
   // console.log('提交(暂存)~~~~~~~~~~~~~~~~~~~', params, writeData.value.date)
   // return
-  const result = await request.postOriginal({ url: '/adm/report/add-report-info', data: params })
+  const result: any = await request.postOriginal({
+    url: '/adm/report/add-report-info',
+    data: params
+  })
   // console.log('提交(暂存)~~~~~~~result~~~~~~', result)
   if (result?.msg && !isTemp) {
     message.success('日志发送成功')

+ 267 - 0
client/src/views/OaSystem/personnelManagement/dailyDetail/DetailBox.vue

@@ -0,0 +1,267 @@
+<script lang="ts" setup>
+import moment from 'moment'
+import request from '@/config/axios'
+import { getUserInfo } from '@/utils/tool'
+interface IProp {
+  detail: any
+}
+const { detail } = defineProps<IProp>()
+
+// onMounted(() => {
+//   console.log('detail', detail)
+// })
+// const emojiObj = {
+//   'emoji-good': '赞!👍',
+//   'emoji-very-good': '写的太棒了!👏',
+//   'emoji-study': '学习了🌹',
+//   'emoji-share': '感谢你的分享🙏',
+//   'emoji-flower': '送你一朵小红花🌺',
+//   'emoji-go': '加油💪',
+//   'emoji-rest': '早点休息😴'
+// }
+const emojiList = ['赞!👍', '写的太棒了!👏', '学习了🌹', '感谢你的分享🙏', '加油💪', '早点休息😴']
+
+const message = useMessage()
+const userInfo = getUserInfo()
+// 评论字段
+interface IComment {
+  reportId: string // 报告id
+  commentUserId: string // 评论人用户id
+  commentContent: string // 评论内容
+  commentDate?: string // 评论时间
+  replyToUserid?: string // 被回复人用户ID
+}
+// 提交评论
+const submitComment = async (content?) => {
+  if (!content && commentInput.value == '') {
+    message.info('评论内容不能为空!')
+    return
+  }
+  const data = {
+    reportId: detail.id,
+    commentUserId: userInfo.id ?? '',
+    commentContent: content ?? commentInput.value
+  }
+  const result: any = await request.postOriginal({ url: '/adm/reportComment/send', data })
+  if (result.msg == 'success') {
+    getCommentList()
+    message.success('评论成功!')
+    // 清空输入框
+    if (!content) {
+      commentInput.value = ''
+    }
+  } else {
+    message.error('评论失败,请稍后再试')
+  }
+}
+const commentList = ref<any[]>(detail?.comments ?? [])
+// 获取报告被评论列表
+const getCommentList = async () => {
+  const result = await request.get({
+    url: '/adm/reportComment/getList',
+    params: { reportId: detail?.id, uId: detail?.userId }
+  })
+  commentList.value = result
+}
+const commentInput = ref('')
+</script>
+<template>
+  <div class="detail-box">
+    <div class="form-box">
+      <el-form ref="form" label-width="100px">
+        <el-row :gutter="24">
+          <el-col :span="8">
+            <el-form-item class="label-item" label="日志日期">
+              <div class="write-date">
+                {{ moment(detail.reportStartDate).format('YYYY-MM-DD') }}
+              </div>
+            </el-form-item>
+          </el-col>
+          <el-col :span="16">
+            <el-form-item class="label-item" label="接收人">
+              <div class="tag-list">
+                <el-tag
+                  type="info"
+                  effect="light"
+                  v-for="(item, index) in detail.receiveNames"
+                  :key="index"
+                  >{{ item }}</el-tag
+                >
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24" style="height: 100%">
+            <el-form-item class="label-item" label="今日工作">
+              <div
+                class="text-area"
+                v-html="detail?.reportContent?.replace(/(\r\n|\n|\r)/gm, '<br />')"
+              >
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24" style="height: 100%">
+            <el-form-item class="label-item" label="工作量分配">
+              <div class="text-area area2">
+                <div v-for="(item, index) in detail.workload" :key="index">
+                  <span class="work-time">{{ `(${item.workTime}小时)` }}</span>
+                  <span>{{ item.projectName }}</span>
+                </div>
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </div>
+    <div class="comment-box">
+      <div class="comment-list">
+        <div v-if="commentList.length === 0" class="not-comment">
+          <el-empty description="暂无评论" image-size="35" />
+        </div>
+        <div v-else v-for="(item, index) in commentList" :key="index" class="item">
+          <div class="left">
+            <span>{{ item.commentUserName.substr(-2) }}</span>
+          </div>
+          <div class="right">
+            <div class="title">
+              {{
+                `${item.commentUserName} ${moment(item.commentDate).format('YYYY-MM-DD HH:mm:ss')}`
+              }}
+              <span>删除</span>
+            </div>
+            <div>{{ item.commentContent }}</div>
+          </div>
+        </div>
+      </div>
+      <div class="comment-emoji">
+        <span v-for="(item, index) in emojiList" :key="index" @click="submitComment(item)">{{
+          item
+        }}</span>
+      </div>
+      <div class="comment-input">
+        <el-input v-model="commentInput" placeholder="请输入评论内容" />
+        <el-button class="submit" type="primary" @click="submitComment()">发表评价</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+<style scoped lang="scss">
+.detail-box {
+  width: 100%;
+  height: 100%;
+  .form-box {
+    width: 100%;
+    .label-item {
+      .el-form-item__label {
+        font-weight: 400 !important;
+        font-size: 16px !important;
+      }
+      .write-date {
+        width: 100px;
+        border: 1px solid #dee0e3;
+        border-radius: 4px;
+        text-align: center;
+        background: #f9f9f9;
+      }
+      .text-area {
+        width: 100%;
+        height: 132px;
+        overflow-y: scroll;
+        background: #f9f9f9;
+        border-radius: 4px 4px 4px 4px;
+        border: 1px solid #c7ceda;
+        padding: 5px 10px;
+      }
+      .area2 {
+        height: 90px;
+        .work-time {
+          font-weight: bold;
+          font-size: 14px;
+          color: #1b80eb;
+        }
+      }
+      .tag-list {
+        width: 100%;
+        max-height: 65px;
+        overflow-y: scroll;
+        .el-tag {
+          margin-right: 8px;
+        }
+      }
+    }
+  }
+  .comment-box {
+    width: 100%;
+    padding-left: 100px;
+    .comment-list {
+      height: 180px;
+      overflow-y: scroll;
+      background: #f7f8fa;
+      border-radius: 4px 4px 4px 4px;
+      border: 1px solid #eef2f9;
+      padding: 10px;
+      .item {
+        display: flex;
+        justify-content: flex-start;
+        align-items: center;
+        border-bottom: 1px solid #e3eaf5;
+        height: 85px;
+        .left {
+          width: 80px;
+          text-align: center;
+          span {
+            display: inline-block;
+            width: 44px;
+            height: 44px;
+            line-height: 44px;
+            color: #ffffff;
+            background-color: #5a99e3;
+            font-size: 14px;
+            font-weight: 400;
+            border-radius: 50%;
+          }
+        }
+        .right {
+          font-weight: 400;
+          font-size: 14px;
+          color: #121518;
+          line-height: 16px;
+          .title {
+            font-weight: 400;
+            font-size: 14px;
+            color: #8a94a4;
+            margin-bottom: 8px;
+            span {
+              color: #1b80eb;
+              margin-left: 15px;
+              cursor: pointer;
+            }
+          }
+        }
+      }
+    }
+    .comment-emoji {
+      height: 50px;
+      line-height: 50px;
+      display: flex;
+      justify-content: space-between;
+      font-size: 14px;
+      color: #626b70;
+      span {
+        cursor: pointer;
+      }
+    }
+    .comment-input {
+      width: 100%;
+      display: flex;
+      justify-content: space-between;
+      .submit {
+        margin-left: 10px;
+      }
+    }
+  }
+}
+</style>

+ 466 - 0
client/src/views/OaSystem/personnelManagement/dailyDetail/OaCalendar.vue

@@ -0,0 +1,466 @@
+<template>
+  <div class="OaCalendar">
+    <el-calendar ref="calendar" :model-value="todayValue">
+      <template #header="{ date }">
+        <div class="selectBox">
+          <p>{{ date }}</p>
+        </div>
+      </template>
+      <template #date-cell="{ data }">
+        <!-- 仅工作日显示角标 -->
+        <div
+          :class="`day-cell ${getDayClass(data.day)}`"
+          v-if="data.type === 'current-month'"
+          @mouseover="onMouseOver(data)"
+          @mouseout="onMouseOut(data)"
+        >
+          <p :class="data.isSelected ? 'is-selected' : ''">
+            {{ dayReportMap[data.day]?.show }}
+          </p>
+        </div>
+
+        <div v-else>
+          <div class="hide-day"> </div>
+        </div>
+        <div class="icon" v-if="data.type === 'current-month'">
+          <el-icon v-if="getDayClass(data.day) == 'square-filled'">
+            <Select />
+          </el-icon>
+          <el-icon v-if="getDayClass(data.day) == 'square-unfilled'"><CloseBold /></el-icon>
+          <!-- <el-icon v-if="getDayClass(data.day) == 'square-today'"><EditPen /></el-icon> -->
+          <el-icon v-if="getDayClass(data.day) == 'square-future'"><MoreFilled /></el-icon>
+        </div>
+      </template>
+    </el-calendar>
+  </div>
+</template>
+<script setup lang="ts">
+import request from '@/config/axios'
+import moment from 'moment'
+import { getUserInfo } from '@/utils/tool'
+
+interface IProp {
+  detailTime: any
+}
+const { detailTime } = defineProps<IProp>()
+// 获取用户信息
+const userInfo = getUserInfo()
+
+const calendar = ref()
+
+const todayValue = ref(moment(detailTime))
+
+watch(todayValue, () => {
+  // 不进行点击及跳转
+  todayValue.value = moment(detailTime)
+})
+
+onMounted(() => {
+  initDailyReportData()
+})
+
+interface ItemState {
+  id?: string // ID
+  date: string // 日期
+  year: string // 年份
+  month: string // 月份
+  week: string // 第几周
+  dayOfWeek: string // 第几天
+  isworkday: number //是否工作日
+  state: string //leaveMap
+}
+// const weekNum = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日']
+// 考勤状态对应字典值 2.26
+const leaveMap = {
+  1: '出勤',
+  2: '迟到',
+  3: '早退',
+  4: '旷工',
+  5: '出差',
+  11: '事假',
+  12: '病假',
+  13: '婚假',
+  14: '产假',
+  15: '陪产假',
+  19: '丧假',
+  16: '工伤',
+  17: '年假',
+  18: '调休',
+  20: '其他'
+}
+
+const dailyReportAttendances = ref<ItemState[]>([])
+const nowMonths: any = ref(moment(detailTime).format('YYYY-MM'))
+
+// const dictMap: any = getDictOptions(DICT_TYPE.ADM_ATTENDANCE_STATUS)
+const getWorkDayList = async (): Promise<any> => {
+  const nowTimeObj = moment(nowMonths.value)
+  const urlApi = `/adm/workday/list`
+  const params = {
+    dateDay: [
+      nowTimeObj.startOf('months').format('YYYY-MM-DD HH:mm:ss'),
+      nowTimeObj.endOf('months').format('YYYY-MM-DD HH:mm:ss')
+    ]
+  }
+  return await request.get({ url: urlApi, params })
+}
+
+const getAttendanceSheetListMine = async (): Promise<any> => {
+  const nowTimeObj = moment(nowMonths.value)
+  const urlApi = `/adm/attendance-sheet/list-me`
+  const params = {
+    attendanceTime: [
+      nowTimeObj.startOf('months').format('YYYY-MM-DD HH:mm:ss'),
+      nowTimeObj.endOf('months').format('YYYY-MM-DD HH:mm:ss')
+    ]
+  }
+  return await request.get({ url: urlApi, params })
+}
+
+// 日报统计
+const getLogList = async (): Promise<any> => {
+  const nowTimeObj = moment(nowMonths.value)
+  const urlApi = `/adm/report/list`
+  const params = {
+    reportType: 'daily',
+    reportYear: nowTimeObj.format('YYYY'),
+    reportMonth: Number(nowTimeObj.format('MM')),
+    userId: userInfo.id ?? ''
+  }
+  return await request.get({ url: urlApi, params })
+}
+
+const dayReportMap: any = ref({})
+const initDailyReportData = async (): Promise<any> => {
+  // 获取已填写的日志
+  const logList = await getLogList()
+  const logObj = {}
+  logList?.forEach((item) => {
+    logObj[moment(item.reportStartDate).format('YYYY-MM-DD')] = item
+  })
+  //考勤统计
+  const attendances: [] = await getAttendanceSheetListMine()
+  const attendanceState: Map<string, string> = new Map()
+  attendances.forEach((item) => {
+    if (leaveMap[item['attendanceStatus']]) {
+      attendanceState[moment(item['attendanceTime']).format('YYYY-MM-DD')] =
+        item['attendanceStatus']
+    }
+  })
+  //节假日统计
+  const workdays: any[] = await getWorkDayList()
+  const dailyReportList: any[] = []
+  workdays.forEach((item) => {
+    const date = moment(item['dateDay']).format('YYYY-MM-DD')
+    const _dailyReportMap = dailyReportList[date]
+    const _attendanceState = attendanceState[date]
+    const isLog = logObj[date] ?? false
+    if (_dailyReportMap) {
+      dailyReportList.push({
+        id: _dailyReportMap['id'],
+        date: moment(item['dateDay']).format('YYYY-MM-DD'),
+        year: item['year'],
+        month: item['month'],
+        week: item['week'],
+        dayOfWeek: item['dayOfWeek'],
+        isworkday: item['isworkday'],
+        state: _dailyReportMap['state'],
+        isLog,
+        holidayRemark: item['holidayRemark']
+      })
+    } else if (_attendanceState) {
+      dailyReportList.push({
+        id: '',
+        date: moment(item['dateDay']).format('YYYY-MM-DD'),
+        year: item['year'],
+        month: item['month'],
+        week: item['week'],
+        dayOfWeek: item['dayOfWeek'],
+        isworkday: item['isworkday'],
+        state: _attendanceState,
+        isLog,
+        holidayRemark: item['holidayRemark']
+      })
+    } else {
+      dailyReportList.push({
+        id: '',
+        date: moment(item['dateDay']).format('YYYY-MM-DD'),
+        year: item['year'],
+        month: item['month'],
+        week: item['week'],
+        dayOfWeek: item['dayOfWeek'],
+        isworkday: item['isworkday'],
+        state: item['isworkday'] === 0 ? '' : '-1',
+        isLog,
+        holidayRemark: item['holidayRemark']
+      })
+    }
+  })
+  dailyReportAttendances.value = dailyReportList
+  dailyReportList.forEach((item) => {
+    dayReportMap.value[item.date] = { ...item, show: item.date.split('-')[2] }
+  })
+}
+
+/**
+ * 日报样式设置
+ * 1、已填,绿色三角+对勾 square-filled
+ *    state == 99
+ * 2、未填,红色三角+错号 square-unfilled
+ *    state == -1
+ * 3、当天,蓝色三角+画笔 square-today
+ * 4、未到,灰色三角+省略 square-future
+ */
+const getDayClass = (data) => {
+  const { isworkday = 0, state = '', date = '' } = dayReportMap.value[data] ?? {}
+  const isFuture = moment(date).isAfter(moment())
+  // if (date == moment().format('YYYY-MM-DD')) {
+  //   return `square-today`
+  // }
+  if (isworkday && state != -1 && !isFuture) {
+    return `square-filled`
+  } else if (isworkday && !isFuture) {
+    return `square-unfilled`
+  } else if (isworkday && isFuture) {
+    return `square-future`
+  }
+  return ''
+}
+
+// 鼠标移入移出事件
+const onMouseOver = (data) => {
+  const isLog = dayReportMap.value[data.day]?.isLog ? true : false
+  if (isLog) {
+    dayReportMap.value[data.day].show = '已填'
+  } else {
+    dayReportMap.value[data.day].show = dayReportMap.value[data.day]?.holidayRemark ?? '未填'
+  }
+}
+const onMouseOut = (data) => {
+  dayReportMap.value[data.day].show = data.day.split('-')[2]
+}
+</script>
+<style lang="scss" scoped>
+.OaCalendar {
+  width: 100%;
+  :deep(.el-calendar) {
+    .el-calendar__header {
+      padding: 0 !important;
+      border-bottom: none !important;
+    }
+    .el-calendar-table {
+      border: 1px solid #e1e3ea !important;
+      thead {
+        background: #f8faff !important;
+      }
+    }
+    .el-calendar-day {
+      padding: 0;
+      position: relative;
+
+      .icon {
+        width: 20px;
+        height: 20px;
+        text-align: center;
+        position: absolute;
+        top: 2px;
+        right: -1px;
+        color: #fff;
+        font-size: 12px;
+        font-weight: bold;
+        z-index: 999;
+      }
+    }
+    .day-cell {
+      width: 100%;
+      height: 100%;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      font-size: 16px;
+      font-weight: 600;
+      font-family: DINCond-Bold, DINCond-Bold;
+      ::before {
+        content: '';
+        width: 0;
+        height: 0;
+        border-left: 30px solid transparent;
+        border-bottom: 30px solid transparent;
+        position: absolute;
+        top: 0;
+        right: 0;
+      }
+    }
+    .square-filled {
+      ::before {
+        border-right: 30px solid #05ce9e;
+      }
+    }
+    .square-unfilled {
+      ::before {
+        border-right: 30px solid #fc4308;
+      }
+    }
+    .square-today {
+      ::before {
+        border-right: 30px solid #1b80eb;
+      }
+    }
+    .square-future {
+      ::before {
+        border-right: 30px solid #c9cdd8;
+      }
+    }
+    .is-selected {
+      color: #1989fa;
+    }
+  }
+  .selectBox {
+    display: flex;
+    align-items: center;
+    margin-bottom: 10px;
+    .icon1 {
+      cursor: pointer;
+    }
+    p {
+      font-weight: 600;
+      color: #2d333c;
+      margin: 0 10px;
+      user-select: none;
+      cursor: pointer;
+    }
+  }
+  .contentBox {
+    width: 100%;
+    height: 80px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    border: 1px solid #dee0e3;
+
+    .ulBox {
+      width: calc(100% - 120px);
+      height: 100%;
+      ul {
+        width: 100%;
+        height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+      }
+      li {
+        height: 100%;
+        > div {
+          &:first-child {
+            width: 100%;
+            height: 50%;
+            border: 1px solid #dee0e3;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            border-right: 0;
+            border-top: 0;
+            background-color: #f7f8fa;
+            &.isToday {
+              background-color: #2e77e6;
+              > p {
+                color: #fff;
+              }
+            }
+          }
+        }
+        .topBox {
+          p {
+            color: #121518;
+            user-select: none;
+          }
+        }
+        .topBoxPa {
+          p {
+            color: #b9c3c9;
+          }
+        }
+        .topBox:hover {
+          p {
+            color: #1b80eb;
+          }
+        }
+
+        .bomBox {
+          width: 100%;
+          height: 50%;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          &.imgHover_-1,
+          &.imgHover_99 {
+            cursor: pointer;
+          }
+          &.imgHover_-1:hover {
+            position: relative;
+            &::after {
+              position: absolute;
+              display: block;
+              content: '补';
+              width: 100%;
+              height: 100%;
+              background-color: #fff;
+              top: 0px;
+              left: 0px;
+              line-height: 40px;
+              text-align: center;
+              color: #999;
+            }
+          }
+          &.imgHover {
+            &:hover {
+              border: 1px solid #f00;
+            }
+          }
+          img {
+            user-select: none;
+          }
+          > span {
+            cursor: pointer;
+            &.leave {
+              color: #f00;
+              cursor: default;
+            }
+          }
+        }
+      }
+      li:first-child {
+        .topBox {
+          border-left: 0;
+        }
+      }
+      li:last-child {
+        .topBox {
+          border-right: 0;
+        }
+      }
+    }
+    .infoBox {
+      width: 120px;
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      flex-wrap: wrap;
+      background-color: #edf0f4;
+      border-left: 1px solid #dee0e3;
+      span {
+        display: block;
+        width: 100%;
+        text-align: center;
+        font-size: 14px;
+      }
+      p {
+        width: 100%;
+        text-align: center;
+        font-size: 18px;
+      }
+    }
+  }
+}
+</style>

+ 79 - 0
client/src/views/OaSystem/personnelManagement/dailyDetail/index.vue

@@ -0,0 +1,79 @@
+<script lang="ts" setup>
+/**
+ * @description 日报详情
+ */
+
+import request from '@/config/axios'
+import OaCalendar from './OaCalendar.vue'
+import DetailBox from './DetailBox.vue'
+
+const { currentRoute } = useRouter()
+
+onMounted(async () => {
+  // 获取当前路由id及日报详情
+  const reportId = currentRoute.value.query.id ?? ''
+  await getDetailById(reportId)
+})
+
+interface IDetail {
+  id: string
+  receiveIds: string[]
+  receiveNames: string[]
+  reportContent: string
+  reportStartDate: number
+  reportEndDate: number
+  reportType: 'daily' | 'weekly'
+  userId: string
+}
+const detail = ref<IDetail>({})
+// 根据日报id获取日报详情
+const getDetailById = async (reportId) => {
+  const todayDetail = await request.get({
+    url: '/adm/report/query-detail',
+    params: { reportId }
+  })
+  detail.value = todayDetail
+}
+</script>
+<template>
+  <div class="daily-log-detail">
+    <div class="title">日报详情</div>
+    <div class="box" v-if="detail.id">
+      <div class="calendarBox">
+        <OaCalendar :detailTime="detail.reportStartDate" />
+      </div>
+      <div class="contentBox">
+        <DetailBox :detail="detail" />
+      </div>
+    </div>
+  </div>
+</template>
+<style scoped lang="scss">
+.daily-log-detail {
+  margin-top: 20px;
+  height: calc(100% - 20px);
+  background-color: #fff;
+  border-radius: 20px;
+  padding: 20px;
+  .title {
+    font-size: 20px;
+    margin-bottom: 20px;
+    color: #121518;
+  }
+  .box {
+    width: 100%;
+    height: calc(100% - 50px);
+    display: flex;
+    justify-content: space-between;
+    .calendarBox {
+      width: 50%;
+      position: relative;
+    }
+    .contentBox {
+      width: 50%;
+      margin-top: 20px;
+      height: calc(100% - 250px);
+    }
+  }
+}
+</style>

+ 30 - 1
client/src/views/OaSystem/personnelManagement/dailyStatistic/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="dailyStatisticBox">
-    <h4 class="title">部门日报统计</h4>
+    <h4 class="title">日报统计</h4>
     <div class="searchBox">
       <el-form :inline="true" :model="formInline" class="demo-form-inline">
         <el-form-item label="部门:">
@@ -53,7 +53,11 @@
 </template>
 
 <script setup lang="ts">
+import request from '@/config/axios'
 import { UploadFilled, Search } from '@element-plus/icons-vue'
+import moment from 'moment'
+import { getUserInfo } from '@/utils/tool'
+
 const currentPage1 = ref(1)
 const formInline = ref<{
   dept: string
@@ -88,6 +92,31 @@ const onSearchHandle = (): void => {}
 const onExportHandle = (): void => {}
 const handleCurrentChange = (): void => {}
 const handleSizeChange = (): void => {}
+
+onMounted(() => {
+  // 获取日报数据
+  getDailyStatisticData()
+})
+
+// 获取用户信息
+const userInfo = getUserInfo()
+const reportYear = ref(moment().format('YYYY'))
+const reportMonth = ref(moment().format('MM'))
+const userId = ref(userInfo.id ?? '')
+const pageSize = ref(20)
+const pageNo = ref(1)
+// 获取日报统计数据
+const getDailyStatisticData = async (): Promise<void> => {
+  const params = {
+    reportType: 'daily',
+    reportYear: reportYear.value,
+    reportMonth: reportMonth.value,
+    userId: userId.value,
+    pageSize: pageSize.value,
+    pageNo: pageNo.value
+  }
+  return await request.get({ url: '/adm/report/page', params })
+}
 </script>
 
 <style lang="scss" scoped>

+ 249 - 0
client/src/views/OaSystem/personnelManagement/mySendLog/index.vue

@@ -0,0 +1,249 @@
+<template>
+  <div class="dailyStatisticBox">
+    <h4 class="title">我发出的日志</h4>
+    <div class="searchBox">
+      <el-form :inline="true" class="demo-form-inline">
+        <el-form-item label="日志类型:">
+          <el-select v-model="reportType">
+            <el-option label="周报" value="weekly" />
+            <el-option label="日报" value="daily" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="日志时间:">
+          <el-date-picker
+            v-model="dateRange"
+            type="daterange"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            format="YYYY-MM-DD"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :icon="Search" @click="onSearchHandle">查询</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div class="tableBox">
+      <div v-for="(item, index) in tableData" :key="index" class="log-box">
+        <div class="box-title">
+          <div class="user-name">
+            <el-avatar :size="40" :src="userInfo.avatar" />
+            <span class="name">{{ userInfo.nickname }}</span>
+          </div>
+          <div v-if="isDaily" class="log-time">{{
+            moment(item.reportStartDate).format('YYYY-MM-DD')
+          }}</div>
+          <div v-else class="log-times">{{
+            `${moment(item.reportStartDate).format('YYYY/MM/DD')} - ${moment(item.reportEndDate).format('YYYY/MM/DD')}`
+          }}</div>
+        </div>
+        <div class="box-content">
+          <p class="content-title">{{ isDaily ? '今日完成工作' : '本周完成工作' }}</p>
+          <p class="content" v-html="item.reportContent.replace(/(\r\n|\n|\r)/gm, '<br />')"></p>
+          <p class="content-title">工作量分配</p>
+          <div v-for="(work, num) in item.workload" :key="num" class="content">
+            {{ `(${work.workTime}小时)${work.projectName}` }}
+          </div>
+        </div>
+        <div class="box-foot">
+          <div class="create-time">{{ moment(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
+          <div class="look-detail" @click="jumpLogDetail(item.id)">查看详情</div>
+        </div>
+      </div>
+    </div>
+    <div class="pageBox">
+      <el-pagination
+        :style="{ float: 'right', marginTop: '15px' }"
+        v-model:current-page="pageNo"
+        v-model:page-size="pageSize"
+        :total="total"
+        layout="total, prev, pager, next, sizes, jumper"
+        @current-change="handleCurrentChange"
+        @size-change="handleSizeChange"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import request from '@/config/axios'
+import { Search } from '@element-plus/icons-vue'
+import moment from 'moment'
+import { getUserInfo } from '@/utils/tool'
+
+const { push } = useRouter()
+
+// 查看详情
+const jumpLogDetail = (id: number): void => {
+  if (isDaily.value) {
+    push('/dailyLogDetail?id=' + id)
+  } else {
+    push('/weeklyLogDetail?id=' + id)
+  }
+}
+
+// 日报还是周报
+const isDaily = ref(true)
+// 查询
+const onSearchHandle = (): void => {
+  isDaily.value = reportType.value === 'daily'
+  getDailyStatisticData()
+}
+const handleCurrentChange = (current): void => {
+  pageNo.value = current
+}
+const handleSizeChange = (size): void => {
+  pageSize.value = size
+}
+
+onMounted(() => {
+  // 获取日报数据
+  getDailyStatisticData()
+})
+
+const userInfo = getUserInfo()
+const tableData = ref<any[]>([])
+const reportType = ref<'daily' | 'weekly'>('daily')
+const dateRange = ref([])
+const userId = ref<string>(userInfo.id ?? '')
+const pageSize = ref<number>(20)
+const pageNo = ref<number>(1)
+const total = ref<number>(0)
+// 获取日报统计数据
+const getDailyStatisticData = async (): Promise<void> => {
+  const params: any = {
+    reportType: reportType.value,
+    userId: userId.value,
+    pageSize: pageSize.value,
+    pageNo: pageNo.value
+  }
+  if (dateRange.value?.length > 0) {
+    params.reportDate = [
+      moment(dateRange.value[0]).format('YYYY-MM-DD HH:mm:ss'),
+      moment(dateRange.value[1]).format('YYYY-MM-DD HH:mm:ss')
+    ]
+  }
+  const { list = [], total: resTotal = 0 } = await request.get({ url: '/adm/report/page', params })
+  tableData.value = list
+  total.value = resTotal
+}
+watch([() => pageSize.value, () => pageNo.value], () => {
+  getDailyStatisticData()
+})
+</script>
+
+<style lang="scss" scoped>
+.dailyStatisticBox {
+  margin-top: 20px;
+  height: calc(100% - 20px);
+  background-color: #fff;
+  border-radius: 20px;
+  padding: 20px;
+  font-family:
+    Microsoft YaHei,
+    Microsoft YaHei;
+  .title {
+    height: 32px;
+    font-weight: bold;
+    font-size: 24px;
+    color: #121518;
+    line-height: 28px;
+    margin-bottom: 12px;
+  }
+  .tableBox {
+    width: 100%;
+    height: calc(100% - 150px);
+    overflow-y: scroll;
+    display: flex;
+    justify-content: flex-start;
+    flex-wrap: wrap;
+    .log-box {
+      width: 400px;
+      height: 300px;
+      border-radius: 12px 12px 12px 12px;
+      border: 2px solid #2e77e6;
+      margin-right: 10px;
+      margin-bottom: 10px;
+      ::-webkit-scrollbar {
+        width: 0 !important;
+      }
+      .box-title {
+        width: 100%;
+        height: 70px;
+        // line-height: 70px;
+        border-bottom: 1px solid #dee0e3;
+        padding: 0 20px;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        .user-name {
+          font-weight: bold;
+          font-size: 18px;
+          color: #2e77e6;
+          .name {
+            margin-left: 10px;
+          }
+        }
+        .log-time {
+          font-weight: bold;
+          font-size: 18px;
+          color: #2d333c;
+        }
+        .log-times {
+          font-weight: bold;
+          font-size: 14px;
+          color: #2d333c;
+        }
+        .el-avatar {
+          border: 3px solid #b5b9bf;
+          vertical-align: middle;
+        }
+      }
+      .box-content {
+        height: 140px;
+        padding: 20px;
+        border-bottom: 1px solid #dee0e3;
+        overflow-y: scroll;
+
+        .content-title {
+          font-weight: 400;
+          font-size: 14px;
+          color: #5f6d81;
+          margin-bottom: 8px;
+        }
+        .content {
+          font-weight: 400;
+          font-size: 14px;
+          color: #2d333c;
+          margin-bottom: 10px;
+        }
+      }
+      .box-foot {
+        width: 100%;
+        height: 45px;
+        line-height: 45px;
+        background: linear-gradient(180deg, rgba(242, 244, 248, 0) 0%, #e8efff 100%);
+        border-radius: 0 0 10px 10px;
+        display: flex;
+        justify-content: space-between;
+        padding: 0 20px;
+        .create-time {
+          font-weight: 400;
+          font-size: 14px;
+          color: #505a69;
+        }
+        .look-detail {
+          font-weight: 400;
+          font-size: 16px;
+          color: #2e77e6;
+          cursor: pointer;
+        }
+      }
+    }
+  }
+  .pageBox {
+    padding-top: 10px;
+    padding-right: 20px;
+  }
+}
+</style>

+ 14 - 6
client/src/views/OaSystem/personnelManagement/weeklyCenter/editorDetail.vue

@@ -149,16 +149,16 @@ const userInfo = getUserInfo()
 // 提交(暂存)
 const message = useMessage()
 const sendReportHandle = async (isTemp) => {
-  // if (formData.value.receiveUserIds.length === 0) {
-  //   message.warning('请选择接收人')
-  //   return
-  // }
+  if (formData.value.receiveUserIds.length === 0) {
+    message.warning('请选择接收人')
+    return
+  }
 
   if (formData.value.weeklyWorkloadList.length === 0) {
     message.warning('请对工作量进行分配')
     return
   }
-  const params = {
+  const params: any = {
     userId: userInfo.id ?? '',
     deptId: userInfo.deptId ?? '',
     reportStartDate: moment(weekDate.value[0]).valueOf(),
@@ -185,7 +185,15 @@ const sendReportHandle = async (isTemp) => {
     // ]
   }
   // console.log('提交(暂存)~~~~~~~~~~~~~~~~~~~', params, weekDate.value)
-  const result = await request.postOriginal({ url: '/adm/report/add-report-info', data: params })
+  // 如果是更新的话要传一下报告id
+  if (isUpDate.value) {
+    const reportId = writeData.value?.isLog?.id ?? ''
+    params.reportId = reportId
+  }
+  const result: any = await request.postOriginal({
+    url: '/adm/report/add-report-info',
+    data: params
+  })
   // console.log('提交(暂存)~~~~~~~result~~~~~~', result)
   if (result?.msg && !isTemp) {
     message.success('周报发送成功')

+ 269 - 0
client/src/views/OaSystem/personnelManagement/weeklyDetail/DetailBox.vue

@@ -0,0 +1,269 @@
+<script lang="ts" setup>
+import moment from 'moment'
+import request from '@/config/axios'
+import { getUserInfo } from '@/utils/tool'
+interface IProp {
+  detail: any
+}
+const { detail } = defineProps<IProp>()
+
+// onMounted(() => {
+//   console.log('detail', detail)
+// })
+// const emojiObj = {
+//   'emoji-good': '赞!👍',
+//   'emoji-very-good': '写的太棒了!👏',
+//   'emoji-study': '学习了🌹',
+//   'emoji-share': '感谢你的分享🙏',
+//   'emoji-flower': '送你一朵小红花🌺',
+//   'emoji-go': '加油💪',
+//   'emoji-rest': '早点休息😴'
+// }
+const emojiList = ['赞!👍', '写的太棒了!👏', '学习了🌹', '感谢你的分享🙏', '加油💪', '早点休息😴']
+
+const message = useMessage()
+const userInfo = getUserInfo()
+// 评论字段
+interface IComment {
+  reportId: string // 报告id
+  commentUserId: string // 评论人用户id
+  commentContent: string // 评论内容
+  commentDate?: string // 评论时间
+  replyToUserid?: string // 被回复人用户ID
+}
+// 提交评论
+const submitComment = async (content?) => {
+  if (!content && commentInput.value == '') {
+    message.info('评论内容不能为空!')
+    return
+  }
+  const data = {
+    reportId: detail.id,
+    commentUserId: userInfo.id ?? '',
+    commentContent: content ?? commentInput.value
+  }
+  const result: any = await request.postOriginal({ url: '/adm/reportComment/send', data })
+  if (result.msg == 'success') {
+    getCommentList()
+    message.success('评论成功!')
+    // 清空输入框
+    if (!content) {
+      commentInput.value = ''
+    }
+  } else {
+    message.error('评论失败,请稍后再试')
+  }
+}
+const commentList = ref<any[]>(detail?.comments ?? [])
+// 获取报告被评论列表
+const getCommentList = async () => {
+  const result = await request.get({
+    url: '/adm/reportComment/getList',
+    params: { reportId: detail?.id, uId: detail?.userId }
+  })
+  commentList.value = result
+}
+const commentInput = ref('')
+</script>
+<template>
+  <div class="detail-box">
+    <div class="form-box">
+      <el-form ref="form" label-width="100px">
+        <el-row :gutter="24">
+          <el-col :span="10">
+            <el-form-item class="label-item" label="日志日期">
+              <div class="write-date">
+                {{ moment(detail.reportStartDate).format('YYYY-MM-DD') }}
+                -
+                {{ moment(detail.reportEndDate).format('YYYY-MM-DD') }}
+              </div>
+            </el-form-item>
+          </el-col>
+          <el-col :span="14">
+            <el-form-item class="label-item" label="接收人">
+              <div class="tag-list">
+                <el-tag
+                  type="info"
+                  effect="light"
+                  v-for="(item, index) in detail.receiveNames"
+                  :key="index"
+                  >{{ item }}</el-tag
+                >
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24" style="height: 100%">
+            <el-form-item class="label-item" label="今日工作">
+              <div
+                class="text-area"
+                v-html="detail?.reportContent?.replace(/(\r\n|\n|\r)/gm, '<br />')"
+              >
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24" style="height: 100%">
+            <el-form-item class="label-item" label="工作量分配">
+              <div class="text-area area2">
+                <div v-for="(item, index) in detail.workload" :key="index">
+                  <span class="work-time">{{ `(${item.workTime}小时)` }}</span>
+                  <span>{{ item.projectName }}</span>
+                </div>
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </div>
+    <div class="comment-box">
+      <div class="comment-list">
+        <div v-if="commentList.length === 0" class="not-comment">
+          <el-empty description="暂无评论" image-size="35" />
+        </div>
+        <div v-else v-for="(item, index) in commentList" :key="index" class="item">
+          <div class="left">
+            <span>{{ item.commentUserName.substr(-2) }}</span>
+          </div>
+          <div class="right">
+            <div class="title">
+              {{
+                `${item.commentUserName} ${moment(item.commentDate).format('YYYY-MM-DD HH:mm:ss')}`
+              }}
+              <span>删除</span>
+            </div>
+            <div>{{ item.commentContent }}</div>
+          </div>
+        </div>
+      </div>
+      <div class="comment-emoji">
+        <span v-for="(item, index) in emojiList" :key="index" @click="submitComment(item)">{{
+          item
+        }}</span>
+      </div>
+      <div class="comment-input">
+        <el-input v-model="commentInput" placeholder="请输入评论内容" />
+        <el-button class="submit" type="primary" @click="submitComment()">发表评价</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+<style scoped lang="scss">
+.detail-box {
+  width: 100%;
+  height: 100%;
+  .form-box {
+    width: 100%;
+    .label-item {
+      .el-form-item__label {
+        font-weight: 400 !important;
+        font-size: 16px !important;
+      }
+      .write-date {
+        width: 190px;
+        border: 1px solid #dee0e3;
+        border-radius: 4px;
+        background: #f9f9f9;
+        text-align: center;
+      }
+      .text-area {
+        width: 100%;
+        height: 132px;
+        overflow-y: scroll;
+        background: #f9f9f9;
+        border-radius: 4px 4px 4px 4px;
+        border: 1px solid #c7ceda;
+        padding: 5px 10px;
+      }
+      .area2 {
+        height: 90px;
+        .work-time {
+          font-weight: bold;
+          font-size: 14px;
+          color: #1b80eb;
+        }
+      }
+      .tag-list {
+        width: 100%;
+        max-height: 65px;
+        overflow-y: scroll;
+        .el-tag {
+          margin-right: 8px;
+        }
+      }
+    }
+  }
+  .comment-box {
+    width: 100%;
+    padding-left: 100px;
+    .comment-list {
+      height: 180px;
+      overflow-y: scroll;
+      background: #f7f8fa;
+      border-radius: 4px 4px 4px 4px;
+      border: 1px solid #eef2f9;
+      padding: 10px;
+      .item {
+        display: flex;
+        justify-content: flex-start;
+        align-items: center;
+        border-bottom: 1px solid #e3eaf5;
+        height: 85px;
+        .left {
+          width: 80px;
+          text-align: center;
+          span {
+            display: inline-block;
+            width: 44px;
+            height: 44px;
+            line-height: 44px;
+            color: #ffffff;
+            background-color: #5a99e3;
+            font-size: 14px;
+            font-weight: 400;
+            border-radius: 50%;
+          }
+        }
+        .right {
+          font-weight: 400;
+          font-size: 14px;
+          color: #121518;
+          line-height: 16px;
+          .title {
+            font-weight: 400;
+            font-size: 14px;
+            color: #8a94a4;
+            margin-bottom: 8px;
+            span {
+              color: #1b80eb;
+              margin-left: 15px;
+              cursor: pointer;
+            }
+          }
+        }
+      }
+    }
+    .comment-emoji {
+      height: 50px;
+      line-height: 50px;
+      display: flex;
+      justify-content: space-between;
+      font-size: 14px;
+      color: #626b70;
+      span {
+        cursor: pointer;
+      }
+    }
+    .comment-input {
+      width: 100%;
+      display: flex;
+      justify-content: space-between;
+      .submit {
+        margin-left: 10px;
+      }
+    }
+  }
+}
+</style>

+ 311 - 0
client/src/views/OaSystem/personnelManagement/weeklyDetail/WeekCalendar.vue

@@ -0,0 +1,311 @@
+<template>
+  <div class="weeklyCenterBox">
+    <div class="selectBox">
+      <p>{{ moment(nowTime).format('YYYY 年 M 月') }}</p>
+    </div>
+    <div class="contentBox">
+      <div
+        v-for="(arr, index) in sourceList"
+        :key="index"
+        :class="['ulBox', index == checkWeek ? 'check-box' : '']"
+      >
+        <p :class="['title', getTitleClass(index)]"> 第{{ weekNum[index] }}周 </p>
+        <ul>
+          <li
+            v-for="(cItem, cIndex) in arr"
+            :key="index + cIndex"
+            :class="[
+              cItem['w'] >= 6 || cItem['m'] != moment(nowTime).format('MM') ? 'disabled' : ''
+            ]"
+          >
+            <span class="week-text"> {{ weekNum[cIndex] }} </span>
+            <span>
+              {{ cItem['d'] }}
+            </span>
+            <span v-if="cItem['w'] < 6">
+              <span v-if="moment(cItem['dm']).isAfter(moment().subtract(1, 'day'))">···</span>
+              <b v-else-if="leaveMap[cItem['am']]">{{ leaveMap[cItem['am']] }}</b>
+              <Icon icon="fa:check" v-else-if="cItem['am'] == 1" />
+              <Icon icon="fa:close" v-else />
+            </span>
+            <span v-if="cItem['w'] < 6">
+              <span v-if="moment(cItem['dm']).isAfter(moment().subtract(1, 'day'))">···</span>
+              <b v-else-if="leaveMap[cItem['pm']]">{{ leaveMap[cItem['pm']] }}</b>
+              <Icon icon="fa:check" v-else-if="cItem['pm'] == 1" />
+              <Icon icon="fa:close" v-else />
+            </span>
+          </li>
+        </ul>
+      </div>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import moment from 'moment'
+import request from '@/config/axios'
+import { useComputedDates, isWeekend } from '@/components/Calendar/calendar'
+import { getUserInfo } from '@/utils/tool'
+
+interface IProp {
+  detailTime: any[]
+}
+const { detailTime } = defineProps<IProp>()
+
+const checkWeek = ref(0)
+
+const weekNum = ['一', '二', '三', '四', '五', '六', '日']
+// 考勤状态对应字典值 2.26
+const leaveMap = {
+  1: '勤', // '出勤'
+  2: '迟', // '迟到'
+  3: '早', // '早退'
+  4: '旷', // '旷工'
+  5: '差', // '出差'
+  11: '假', // '事假'
+  12: '假', // '病假'
+  13: '假', // '婚假'
+  14: '假', // '产假'
+  15: '假', // '陪产假'
+  19: '假', // '丧假'
+  16: '假', // '工伤'
+  17: '假', // '年假'
+  18: '假', // '调休'
+  20: '假' // '其他'
+}
+// 缺勤对应列表
+// const closeList = [2, 3, 4, null, undefined, '']
+interface IDate {
+  dm: string //年月日
+  m: string | number //月份
+  d: string | number //日期
+  w: number | number //星期几
+  s?: string //用来标记状态
+}
+type DaysFunType = (lists: any) => Array<Array<IDate>>
+
+const nowTime = ref<string>(moment(detailTime[1]).format('YYYY-MM'))
+const sourceList = ref<Array<Array<IDate>>>([])
+//如果总周数大于6并且当月第一天星期大于等于6,删除第一周
+const computedDays: DaysFunType = (lists: any) => {
+  const nlists: Array<Array<IDate>> = [].concat(lists)
+  const index = isWeekend(nowTime.value)
+  if (index !== -1) {
+    nlists.splice(index, 1)
+  }
+  return nlists
+}
+
+const attendanceStatusObj = ref<any>({}) // 考勤状态
+const thisWeekNum = ref(0) // 当前周数
+onMounted(async () => {
+  await setWeekOfYear()
+  await getLogList()
+  // getMonthWeekGroup()
+  await setDayOfMonth()
+})
+
+watch(
+  () => nowTime.value,
+  async () => {
+    await getLogList()
+  }
+)
+// 设置当月日期
+const setDayOfMonth = async () => {
+  await getAttendanceSheetList()
+  sourceList.value = await computedDays(useComputedDates(nowTime.value))
+  const thisDay = moment(detailTime[1]).format('YYYY-MM-DD')
+  for (let i = 0; i < sourceList.value.length; i++) {
+    for (let j = 0; j < sourceList.value[i].length; j++) {
+      const { dm } = sourceList.value[i][j]
+      if (dm == thisDay) {
+        // 获取当前日期是第几周
+
+        thisWeekNum.value = i
+        checkWeek.value = i
+      }
+      sourceList.value[i][j]['am'] = attendanceStatusObj.value[dm]?.am
+      sourceList.value[i][j]['pm'] = attendanceStatusObj.value[dm]?.pm
+    }
+  }
+}
+
+// 获取考勤数据
+const getAttendanceSheetList = async (): Promise<any> => {
+  const startTime = moment(nowTime.value).startOf('month').startOf('week').format('YYYY-MM-DD')
+
+  const attendanceData = await request.get({
+    url: `/adm/attendance-sheet/list-me`,
+    params: {
+      attendanceDate: [
+        moment(startTime).format('YYYY-MM-DD HH:mm:ss'),
+        moment(startTime).add(34, 'days').format('YYYY-MM-DD HH:mm:ss')
+      ]
+    }
+  })
+  attendanceData?.forEach((item) => {
+    //	attendanceDate 考勤时间
+    //	attendanceType 考勤类型(1:上午;2:下午)
+    //	attendanceStatus 考勤状态
+    const { attendanceDate, attendanceType, attendanceStatus } = item
+    const type = attendanceType === 1 ? 'am' : 'pm'
+    // 获取本月的第一周的第一天日期(以周日为第一天)
+    const date = moment(attendanceDate).format('YYYY-MM-DD')
+    if (attendanceStatusObj.value[date]) {
+      attendanceStatusObj.value[date][type] = attendanceStatus
+    } else {
+      attendanceStatusObj.value[date] = {
+        [type]: attendanceStatus
+      }
+    }
+  })
+}
+
+// 获取用户信息
+const userInfo = getUserInfo()
+const weekLogObj = ref({})
+// 周报统计
+const getLogList = async (): Promise<any> => {
+  const urlApi = `/adm/report/list`
+  const params = {
+    reportType: 'weekly',
+    reportYear: moment(nowTime.value).format('YYYY'),
+    reportMonth: moment(nowTime.value).format('M'),
+    userId: userInfo.id ?? ''
+  }
+  const logList = await request.get({ url: urlApi, params })
+  logList.forEach((item: any) => {
+    const data = moment(item.reportStartDate).format('YYYY-MM-DD')
+    weekLogObj.value[data] = item
+  })
+}
+
+watch(
+  () => moment(nowTime.value).format('YYYY'),
+  (year) => {
+    setWeekOfYear(year)
+  }
+)
+
+// 获取周和年的关系
+// 把获取到的周信息汇聚起来
+const weekOfYearObj = ref({})
+const weekOfMonthObj = ref({})
+// 获取日期对应的相关周等信息
+const setWeekOfYear = async (date = moment().format('YYYY')) => {
+  // const year = moment(date).format('YYYY')
+  // const month = moment(date).format('M')
+  const result = await request.get({ url: '/adm/workday/list-week', params: { year: date } })
+  result.forEach((item) => {
+    const date = moment(item.dateDay).format('YYYY-MM-DD')
+    const month = moment(item.dateDay).format('YYYY-MM')
+    weekOfYearObj.value[date] = item
+    if (weekOfMonthObj.value[month]) {
+      weekOfMonthObj.value[month].push(item)
+    } else {
+      weekOfMonthObj.value[month] = [item]
+    }
+  })
+}
+
+// 设置本周title颜色
+const getTitleClass = (index) => {
+  const thisMonth = moment().format('YYYY-MM')
+  const isBefore = moment(nowTime.value).isBefore(thisMonth)
+  const isAfter = moment(nowTime.value).isAfter(thisMonth)
+  if (isBefore) {
+    return 'color1'
+  }
+  if (isAfter) {
+    return 'color3'
+  }
+  if (index > thisWeekNum.value) {
+    return 'color3'
+  } else if (index == thisWeekNum.value) {
+    return 'color1'
+  }
+  return 'color1'
+}
+</script>
+<style lang="scss" scoped>
+.weeklyCenterBox {
+  width: 100%;
+  height: calc(100% - 20px);
+
+  .selectBox {
+    display: flex;
+    align-items: center;
+    margin-bottom: 10px;
+    .icon1 {
+      cursor: pointer;
+    }
+    p {
+      font-weight: 600;
+      color: #2d333c;
+      margin: 0 10px;
+      user-select: none;
+      cursor: pointer;
+    }
+  }
+  .overflow-scroll::-webkit-scrollbar,
+  ::-webkit-scrollbar {
+    width: 0 !important;
+  }
+  .contentBox {
+    width: 100%;
+    height: 100%;
+    overflow-y: scroll;
+    display: flex;
+    flex-wrap: wrap;
+    padding: 20px;
+
+    .ulBox {
+      width: 315px;
+      height: 165px;
+      border: 1px solid #e5e8ed;
+      border-radius: 8px 8px 8px 8px;
+      padding: 10px;
+      margin-bottom: 15px;
+      margin-right: 20px;
+      .title {
+        border-radius: 4px 4px 4px 4px;
+        padding: 3px 0px;
+        text-align: center;
+        color: #fff;
+        &.color1 {
+          background-color: #05ce9e;
+        }
+        &.color2 {
+          background-color: #e75d5d;
+        }
+        &.color3 {
+          background-color: #a8aeb8;
+        }
+      }
+      ul {
+        display: flex;
+        li {
+          flex: 1;
+          padding: 8px 0px;
+          text-align: center;
+          display: flex;
+          flex-direction: column;
+          &.disabled {
+            color: #b9c3c9;
+          }
+          span {
+            text-align: center;
+            padding-top: 4px;
+          }
+        }
+      }
+    }
+    .ulBox:hover {
+      border: 1px solid #1b80eb;
+    }
+    .check-box {
+      border: 1.5px solid #1b80eb;
+    }
+  }
+}
+</style>

+ 79 - 0
client/src/views/OaSystem/personnelManagement/weeklyDetail/index.vue

@@ -0,0 +1,79 @@
+<script lang="ts" setup>
+/**
+ * @description 周报详情
+ */
+
+import request from '@/config/axios'
+import DetailBox from './DetailBox.vue'
+import WeekCalendar from './WeekCalendar.vue'
+
+const { currentRoute } = useRouter()
+
+onMounted(async () => {
+  // 获取当前路由id及周报详情
+  const reportId = currentRoute.value.query.id ?? ''
+  await getDetailById(reportId)
+})
+
+interface IDetail {
+  id: string
+  receiveIds: string[]
+  receiveNames: string[]
+  reportContent: string
+  reportStartDate: number
+  reportEndDate: number
+  reportType: 'daily' | 'weekly'
+  userId: string
+}
+const detail = ref<IDetail>({})
+// 根据周报id获取周报详情
+const getDetailById = async (reportId) => {
+  const todayDetail = await request.get({
+    url: '/adm/report/query-detail',
+    params: { reportId }
+  })
+  detail.value = todayDetail
+}
+</script>
+<template>
+  <div class="weekly-log-detail">
+    <div class="title">周报详情</div>
+    <div class="box" v-if="detail.id">
+      <div class="calendarBox">
+        <WeekCalendar :detailTime="[detail.reportStartDate, detail.reportEndDate]" />
+      </div>
+      <div class="contentBox">
+        <DetailBox :detail="detail" />
+      </div>
+    </div>
+  </div>
+</template>
+<style scoped lang="scss">
+.weekly-log-detail {
+  margin-top: 20px;
+  height: calc(100% - 20px);
+  background-color: #fff;
+  border-radius: 20px;
+  padding: 20px;
+  .title {
+    font-size: 20px;
+    margin-bottom: 20px;
+    color: #121518;
+  }
+  .box {
+    width: 100%;
+    height: calc(100% - 50px);
+    display: flex;
+    justify-content: space-between;
+    .calendarBox {
+      width: 50%;
+      position: relative;
+    }
+    .contentBox {
+      width: 50%;
+      margin-top: 20px;
+      height: calc(100% - 250px);
+    }
+  }
+}
+</style>