优化日报模块

This commit is contained in:
hero920103 committed 2026-10-22 00:27:26 +08:00
1 parent 79d5ae6b66
commit 51fd90fc51
17 files changed
+1767 -257

No files matched your search

+30
View File
@@ -25,6 +25,13 @@
</view>
<view class="head-actions">
<text v-if="detail.can_edit" class="action-link" @tap="openEdit">编辑</text>
<text
v-if="detail.can_withdraw"
class="action-link is-warning"
@tap="handleWithdraw"
>
撤销提交
</text>
<text v-if="detail.can_delete" class="action-link is-danger" @tap="handleDelete">
删除
</text>
@@ -138,6 +145,7 @@ import { computed, ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import {
fetchDailyReportDetail,
withdrawDailyReport,
addDailyReportComment,
deleteDailyReportComment,
deleteDailyReport
@@ -183,6 +191,24 @@ function openEdit() {
})
}
// 撤销提交:退回草稿并留存修改痕迹,改完需重新提交
function handleWithdraw() {
uni.showModal({
title: '撤销提交',
content: '撤销后日报退回草稿,本次提交的内容会留痕,修改完成后需重新提交',
success: async (res) => {
if (!res.confirm) return
try {
await withdrawDailyReport(reportId.value)
uni.showToast({ title: '已撤销提交', icon: 'success' })
await loadDetail()
} catch (e) {
uni.showToast({ title: e?.message || '撤销提交失败', icon: 'none' })
}
}
})
}
function startReply(item) {
replyTo.value = { id: item.id, user_name: item.user_name }
}
@@ -347,6 +373,10 @@ function formatSize(size) {
&.is-danger {
color: $color-danger;
}
&.is-warning {
color: $color-warning;
}
}
.submit-time {
+12
View File
@@ -147,6 +147,18 @@ async function loadDetail(id) {
function applyDetail(data) {
const report = data?.report
if (!report) return
// 已提交的日报已锁定:不进入编辑,引导到详情中撤销提交
if (report.status === 1) {
uni.showModal({
title: '日报已提交',
content: '已提交的日报已锁定,如需修改请先在详情中撤销提交',
showCancel: false,
success: () => {
uni.redirectTo({ url: `/pages/oa/dailyreport/detail?id=${report.id}` })
}
})
return
}
reportId.value = report.id
reportDate.value = report.report_date
form.todayDone = report.today_done || ''
+365 -2
View File
@@ -47,6 +47,64 @@
</view>
</view>
<view class="heatmap-card">
<view class="heatmap-head">
<view class="heatmap-title-wrap">
<text class="heatmap-title">日报热力图</text>
<text class="heatmap-sub">{{ calendarSubmitted }} 篇已发布 · 共 {{ calendarTotal }} 条</text>
</view>
<text class="heatmap-range">{{ calendarRangeText }}</text>
</view>
<view class="heatmap-legend">
<view class="legend-item">
<view class="legend-cell is-submitted"></view>
<text class="legend-text">已提交</text>
</view>
<view class="legend-item">
<view class="legend-cell is-draft"></view>
<text class="legend-text">草稿</text>
</view>
<view class="legend-item">
<view class="legend-cell is-none"></view>
<text class="legend-text">未填写</text>
</view>
<view class="legend-item">
<view class="legend-cell is-backfill">补</view>
<text class="legend-text">可补写</text>
</view>
</view>
<view v-if="calendarMonths.length" class="heatmap-months">
<view v-for="m in calendarMonths" :key="m.key" class="heatmap-month">
<view class="month-head">
<text class="month-label">{{ m.label }}</text>
<text class="month-count" :class="{ 'is-zero': !m.submitted }">
{{ m.submitted }} 篇
</text>
</view>
<view class="month-weeks">
<view
v-for="(week, wi) in m.weeks"
:key="wi"
class="week-col"
>
<view
v-for="(cell, ci) in week.cells"
:key="ci"
class="heat-cell"
:class="[
cellClass(cell),
{ 'is-today': cell && cell.date === today, 'is-backfill': cellBackfill(cell) }
]"
@tap="onCalendarCellTap(cell)"
>
<text v-if="cellBackfill(cell)" class="cell-pad">补</text>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="backfill-bar">
<text class="backfill-label">补写往日</text>
<picker
@@ -58,7 +116,7 @@
>
<view class="date-chip">{{ backfillDate }}</view>
</picker>
<view class="backfill-btn" @tap="openBackfill">填写该日</view>
<view class="backfill-btn" @tap="openBackfill">发布</view>
</view>
<view v-if="loading && !mineList.length" class="state-box">
@@ -142,7 +200,7 @@
<u-empty
v-else-if="!teamList.length"
mode="list"
text="暂无团队成员"
text="当日暂无成员提交日报"
:icon-size="80"
/>
<view v-else class="card-list">
@@ -193,6 +251,7 @@ import { computed, ref } from 'vue'
import { onShow, onReachBottom } from '@dcloudio/uni-app'
import {
fetchDailyReportProfile,
fetchMyDailyReportCalendar,
fetchMyDailyReports,
fetchTeamDailyReports,
fetchTeamDailyReportOrgs,
@@ -209,6 +268,12 @@ const mineList = ref([])
const minePage = ref(1)
const hasMore = ref(false)
// 近期热力图(近三个月)
const calendarDays = ref([])
const calendarRange = ref({ start: '', end: '' })
const calendarTotal = ref(0)
const calendarSubmitted = ref(0)
const backfillDate = ref(today)
const teamDate = ref(today)
@@ -235,6 +300,116 @@ const todayStatusClass = computed(() => {
return 'is-red'
})
const calendarMap = computed(() => {
const map = new Map()
calendarDays.value.forEach((day) => {
// 后端 DATE 字段可能带出时间,统一截到 YYYY-MM-DD 再建索引
const key = String(day?.date || '').slice(0, 10)
if (key) map.set(key, day)
})
return map
})
const calendarRangeText = computed(() => {
const { start, end } = calendarRange.value
return start && end ? `${start} 至 ${end}` : ''
})
// 按区间生成热力图:每列一周(周一在上、周日在下),月内补空格,每月固定 6 周
const calendarMonths = computed(() => {
const map = calendarMap.value
const { start, end } = calendarRange.value
if (!start || !end) return []
const [startYear, startMonth] = start.split('-').map(Number)
const [endYear, endMonth] = end.split('-').map(Number)
const months = []
let year = startYear
let month = startMonth
while (year < endYear || (year === endYear && month <= endMonth)) {
const prefix = `${year}-${pad2(month)}`
const daysInMonth = new Date(year, month, 0).getDate()
const offset = (new Date(year, month - 1, 1).getDay() + 6) % 7
const cells = []
for (let i = 0; i < offset; i++) cells.push(null)
let total = 0
let submitted = 0
for (let d = 1; d <= daysInMonth; d++) {
const date = `${prefix}-${pad2(d)}`
const record = map.get(date)
if (record) {
total++
if (record.status === 1) submitted++
}
cells.push({ date, record, isFuture: date > today })
}
while (cells.length < 42) cells.push(null)
const weeks = []
for (let i = 0; i < 42; i += 7) {
const weekCells = cells.slice(i, i + 7)
weeks.push({ cells: weekCells, pad: weekCells.every((c) => !c) })
}
months.push({ key: prefix, label: `${month} 月`, weeks, total, submitted })
month++
if (month > 12) {
month = 1
year++
}
}
return months
})
function pad2(n) {
return String(n).padStart(2, '0')
}
async function loadCalendar() {
try {
const data = await fetchMyDailyReportCalendar({ months: 3 })
calendarDays.value = data?.days || []
calendarRange.value = {
start: String(data?.start_date || '').slice(0, 10),
end: String(data?.end_date || '').slice(0, 10)
}
calendarTotal.value = data?.total || 0
calendarSubmitted.value = data?.submitted || 0
} catch (e) {
// 热力图加载失败不阻断列表展示
}
}
function inWindow(date) {
const start = profile.value.window_start || summary.value.window_start
const end = profile.value.window_end || summary.value.window_end
if (!start || !end) return date === today
return date >= start && date <= end
}
function cellClass(cell) {
if (!cell) return 'is-empty'
if (cell.record?.status === 1) return 'is-submitted'
if (cell.record?.status === 0) return 'is-draft'
return cell.isFuture ? 'is-future' : 'is-none'
}
// 补写窗口内、尚未写日报的日期标注「补」,点击直接进入补写
function cellBackfill(cell) {
if (!cell || cell.isFuture || cell.record) return false
return inWindow(cell.date)
}
function onCalendarCellTap(cell) {
if (!cell || cell.isFuture) return
if (cell.record?.id) {
openDetail(cell.record.id)
return
}
if (cellBackfill(cell)) {
uni.navigateTo({ url: `/pages/oa/dailyreport/edit?date=${cell.date}` })
return
}
uni.showToast({ title: '该日期没有日报,且已超出补写窗口', icon: 'none' })
}
onShow(() => {
if (!isLoggedIn()) {
uni.reLaunch({ url: '/pages/login/login' })
@@ -254,6 +429,7 @@ onReachBottom(() => {
async function reload() {
await loadProfile()
if (activeTab.value === 'mine') {
loadCalendar()
await loadMine(true)
} else {
if (!orgOptions.value.length) await loadTeamOrgs()
@@ -300,6 +476,7 @@ function switchTab(name) {
if (activeTab.value === name) return
activeTab.value = name
if (name === 'mine') {
loadCalendar()
loadMine(true)
} else {
if (!orgOptions.value.length) loadTeamOrgs()
@@ -318,6 +495,12 @@ function openBackfill() {
function writeToday() {
const id = summary.value.today_report_id
if (id) {
// 已提交的日报已锁定,直接进详情,可在详情中撤销提交
if (summary.value.today_status === 1) {
uni.showToast({ title: '今日日报已提交并锁定,可先在详情中撤销提交', icon: 'none' })
openDetail(id)
return
}
uni.navigateTo({ url: `/pages/oa/dailyreport/edit?id=${id}&date=${today}` })
} else {
uni.navigateTo({ url: `/pages/oa/dailyreport/edit?date=${today}` })
@@ -547,6 +730,186 @@ function openTeamReport(item) {
font-weight: 500;
}
.heatmap-card {
margin-top: 20rpx;
padding: 22rpx 24rpx 24rpx;
@include card;
}
.heatmap-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12rpx;
}
.heatmap-title {
display: block;
font-size: 26rpx;
font-weight: 600;
color: $color-text;
}
.heatmap-sub {
display: block;
margin-top: 4rpx;
font-size: 20rpx;
color: $color-text-muted;
}
.heatmap-range {
flex-shrink: 0;
font-size: 20rpx;
color: $color-text-muted;
}
.heatmap-legend {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 18rpx;
margin-top: 14rpx;
}
.legend-item {
display: flex;
align-items: center;
gap: 6rpx;
}
.legend-text {
font-size: 20rpx;
color: $color-text-muted;
}
.legend-cell {
width: 18rpx;
height: 18rpx;
box-sizing: border-box;
border-radius: 4rpx;
background: #eef1f6;
&.is-submitted {
background: $color-success;
}
&.is-draft {
background: $color-warning;
}
&.is-backfill {
display: flex;
align-items: center;
justify-content: center;
background: $color-primary-bg;
border: 1rpx dashed $color-primary;
font-size: 14rpx;
line-height: 1;
color: $color-primary;
}
}
.heatmap-months {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8rpx;
margin-top: 16rpx;
}
.heatmap-month {
flex: 0 0 auto;
& + & {
padding-left: 8rpx;
border-left: 1rpx dashed $color-border;
}
}
.month-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8rpx;
margin-bottom: 8rpx;
}
.month-label {
font-size: 22rpx;
font-weight: 600;
color: $color-text-secondary;
}
.month-count {
font-size: 18rpx;
color: $color-primary;
&.is-zero {
color: $color-text-muted;
}
}
.month-weeks {
display: flex;
gap: 4rpx;
}
.week-col {
display: flex;
flex: 0 0 auto;
flex-direction: column;
gap: 4rpx;
}
.heat-cell {
display: flex;
align-items: center;
justify-content: center;
width: 30rpx;
height: 30rpx;
box-sizing: border-box;
border: 1rpx solid transparent;
border-radius: 6rpx;
background: #eef1f6;
&.is-empty,
&.is-future {
background: transparent;
}
&.is-future {
background: #f7f8fa;
}
&.is-submitted {
background: $color-success;
}
&.is-draft {
background: $color-warning;
}
&.is-none {
background: #eef1f6;
}
&.is-backfill {
background: $color-primary-bg;
border-color: $color-primary;
border-style: dashed;
}
&.is-today {
box-shadow: 0 0 0 2rpx $color-primary;
}
}
.cell-pad {
font-size: 19rpx;
line-height: 1;
color: $color-primary;
}
.card-list {
margin-top: 20rpx;
}