优化日报模块
This commit is contained in:
1 parent
79d5ae6b66
commit
51fd90fc51
17 files changed
+1767
-257
No files matched your search
@@ -30,6 +30,18 @@ export function fetchMyDailyReports(params = {}) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的日报近期热力图(默认含本月往前 3 个月)
|
||||
* @param {{months?:number}} params
|
||||
* @returns {{start_date:string,end_date:string,total:number,submitted:number,days:Array<{date:string,status:number,id:number}>}}
|
||||
*/
|
||||
export function fetchMyDailyReportCalendar(params = {}) {
|
||||
return request({
|
||||
url: '/api/oa/dailyreport/calendar',
|
||||
data: { months: params.months || 3 }
|
||||
})
|
||||
}
|
||||
|
||||
/** 日报详情:{ report, comments, can_edit, can_delete, can_comment } */
|
||||
export function fetchDailyReportDetail(id) {
|
||||
return request({ url: `/api/oa/dailyreport/detail/${id}` })
|
||||
@@ -58,6 +70,11 @@ export function deleteDailyReport(id) {
|
||||
return request({ url: `/api/oa/dailyreport/delete/${id}`, method: 'DELETE' })
|
||||
}
|
||||
|
||||
/** 撤销提交:已提交日报退回草稿,本次提交内容写入修改痕迹,需重新提交 */
|
||||
export function withdrawDailyReport(id) {
|
||||
return request({ url: `/api/oa/dailyreport/withdraw/${id}`, method: 'POST' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 团队日报汇总
|
||||
* @param {{date?:string,orgId?:number,keyword?:string,page?:number,pageSize?:number}} params
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 || ''
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user