优化日报模块
This commit is contained in:
1 parent
79d5ae6b66
commit
51fd90fc51
17 files changed
+1767
-257
No files matched your search
@@ -25,7 +25,25 @@
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<el-button
|
||||
v-if="detail.can_edit"
|
||||
v-if="detail.report.status === 0 && detail.can_edit"
|
||||
link
|
||||
type="success"
|
||||
:loading="submittingReport"
|
||||
@click="handleSubmitReport"
|
||||
>
|
||||
提交
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="detail.report.status === 1 && detail.can_withdraw"
|
||||
link
|
||||
type="warning"
|
||||
:loading="withdrawing"
|
||||
@click="handleWithdraw"
|
||||
>
|
||||
撤销提交
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="detail.report.status === 0 && detail.can_edit"
|
||||
link
|
||||
type="primary"
|
||||
@click="$emit('edit', detail.report)"
|
||||
@@ -33,7 +51,7 @@
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="detail.can_delete"
|
||||
v-if="detail.report.status === 0 && detail.can_delete"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDeleteReport"
|
||||
@@ -43,6 +61,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="detail.is_author && detail.report.status === 1 && !detail.can_withdraw"
|
||||
class="lock-tip"
|
||||
>
|
||||
日报已提交并锁定;超出补写窗口后不再支持撤销提交
|
||||
</div>
|
||||
|
||||
<section class="detail-section">
|
||||
<h4>今日完成</h4>
|
||||
<div class="section-text">{{ detail.report.today_done || '—' }}</div>
|
||||
@@ -74,6 +99,29 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="detail.histories?.length" class="detail-section">
|
||||
<h4>修改痕迹 <span class="comment-count">{{ detail.histories.length }}</span></h4>
|
||||
<div class="history-list">
|
||||
<div v-for="h in detail.histories" :key="h.id" class="history-item">
|
||||
<div class="history-head">
|
||||
<el-tag
|
||||
:type="h.action === 2 ? 'success' : 'warning'"
|
||||
size="small"
|
||||
effect="plain"
|
||||
>
|
||||
{{ h.action === 2 ? '重新提交' : '撤销提交' }}
|
||||
</el-tag>
|
||||
<span class="history-time">{{ h.create_time }}</span>
|
||||
</div>
|
||||
<div class="history-text">
|
||||
<div><span class="history-label">今日完成:</span>{{ h.today_done || '—' }}</div>
|
||||
<div><span class="history-label">明日计划:</span>{{ h.tomorrow_plan || '—' }}</div>
|
||||
<div><span class="history-label">问题与建议:</span>{{ h.issues || '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-section comment-section">
|
||||
<h4>评论 <span class="comment-count">{{ detail.comments.length }}</span></h4>
|
||||
|
||||
@@ -155,6 +203,8 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Paperclip } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDailyReportDetail,
|
||||
saveDailyReport,
|
||||
withdrawDailyReport,
|
||||
addDailyReportComment,
|
||||
deleteDailyReportComment,
|
||||
deleteDailyReport
|
||||
@@ -171,6 +221,8 @@ const detail = ref(null)
|
||||
const commentText = ref('')
|
||||
const replyTo = ref(null)
|
||||
const submitting = ref(false)
|
||||
const submittingReport = ref(false)
|
||||
const withdrawing = ref(false)
|
||||
|
||||
watch(
|
||||
() => props.reportId,
|
||||
@@ -201,6 +253,62 @@ function startReply(comment) {
|
||||
replyTo.value = { id: comment.id, user_name: comment.user_name }
|
||||
}
|
||||
|
||||
// 草稿直接在详情页提交:提交后内容锁定,需撤销提交才能再改。
|
||||
async function handleSubmitReport() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'提交后日报将锁定,如需修改需先撤销提交。确定提交这条日报吗?',
|
||||
'提交日报',
|
||||
{ type: 'warning', confirmButtonText: '提交', cancelButtonText: '取消' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
submittingReport.value = true
|
||||
try {
|
||||
const report = detail.value.report
|
||||
await saveDailyReport({
|
||||
date: report.report_date,
|
||||
today_done: report.today_done || '',
|
||||
tomorrow_plan: report.tomorrow_plan || '',
|
||||
issues: report.issues || '',
|
||||
attachments: JSON.stringify(report.attachments || []),
|
||||
submit: true
|
||||
})
|
||||
ElMessage.success('日报已提交')
|
||||
await loadDetail()
|
||||
emit('changed')
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '提交失败')
|
||||
} finally {
|
||||
submittingReport.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 撤销提交:退回草稿并留存修改痕迹,修改后可重新提交。
|
||||
async function handleWithdraw() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'撤销提交后日报退回草稿,本次提交的内容会作为修改痕迹保留,修改完成后需重新提交。确定撤销吗?',
|
||||
'撤销提交',
|
||||
{ type: 'warning', confirmButtonText: '撤销提交', cancelButtonText: '取消' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
withdrawing.value = true
|
||||
try {
|
||||
await withdrawDailyReport(props.reportId)
|
||||
ElMessage.success('已撤销提交,可修改后重新提交')
|
||||
await loadDetail()
|
||||
emit('changed')
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '撤销提交失败')
|
||||
} finally {
|
||||
withdrawing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitComment() {
|
||||
const content = commentText.value.trim()
|
||||
if (!content) {
|
||||
@@ -321,6 +429,53 @@ function formatSize(size) {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.lock-tip {
|
||||
margin-top: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--el-color-warning-light-9, #fdf6ec);
|
||||
font-size: 12px;
|
||||
color: var(--el-color-warning, #e6a23c);
|
||||
}
|
||||
|
||||
.history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
padding: 10px 12px;
|
||||
border: 1px dashed #ebeef5;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.history-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.history-time {
|
||||
margin-left: auto;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.history-text {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: #606266;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.history-label {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px dashed #ebeef5;
|
||||
@@ -470,10 +625,24 @@ html.dark & {
|
||||
.comment-time,
|
||||
.file-size,
|
||||
.comment-disabled,
|
||||
.history-label,
|
||||
.reply-banner {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.history-item {
|
||||
background: var(--el-fill-color-light);
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.history-text {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.lock-tip {
|
||||
background: var(--el-color-warning-light-9);
|
||||
}
|
||||
|
||||
.attachment-item,
|
||||
.comment-item {
|
||||
background: var(--el-fill-color-light);
|
||||
|
||||
@@ -16,61 +16,118 @@
|
||||
<el-tabs v-model="activeTab" class="report-tabs" @tab-change="handleTabChange">
|
||||
<!-- ==================== 我的日报 ==================== -->
|
||||
<el-tab-pane label="我的日报" name="mine">
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">本月日报</div>
|
||||
<div class="stat-value is-blue">{{ summary.month_total || 0 }}</div>
|
||||
<div class="stat-sub">条记录</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">本月已提交</div>
|
||||
<div class="stat-value is-green">{{ summary.month_submitted || 0 }}</div>
|
||||
<div class="stat-sub">条已提交</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">今日状态</div>
|
||||
<div class="stat-value" :class="todayStatusClass">
|
||||
{{ todayStatusText }}
|
||||
<div class="mine-overview">
|
||||
<div class="overview-left">
|
||||
<div class="mine-stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">本月日报</div>
|
||||
<div class="stat-value is-blue">{{ summary.month_total || 0 }}</div>
|
||||
<div class="stat-sub">条记录</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">本月已提交</div>
|
||||
<div class="stat-value is-green">{{ summary.month_submitted || 0 }}</div>
|
||||
<div class="stat-sub">条已提交</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">今日状态</div>
|
||||
<div class="stat-value" :class="todayStatusClass">
|
||||
{{ todayStatusText }}
|
||||
</div>
|
||||
<div class="stat-sub">{{ summary.window_end || todayStr() }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">补写窗口</div>
|
||||
<div class="stat-value is-orange window-value">
|
||||
{{ summary.window_start || '—' }}
|
||||
</div>
|
||||
<div class="stat-sub">至 {{ summary.window_end || '—' }},共 {{ profile.backfill_days || 7 }} 天</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-sub">{{ summary.window_end || todayStr() }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">补写窗口</div>
|
||||
<div class="stat-value is-orange window-value">
|
||||
{{ summary.window_start || '—' }}
|
||||
|
||||
<div class="mine-filters">
|
||||
<div class="filter-row">
|
||||
<el-date-picker
|
||||
v-model="mineRange"
|
||||
class="mine-range"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:disabled-date="disableFutureDate"
|
||||
unlink-panels
|
||||
/>
|
||||
<el-button type="primary" :icon="Search" @click="searchMine">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="resetMineFilters">重置</el-button>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<span class="backfill-label">补写往日</span>
|
||||
<el-date-picker
|
||||
v-model="backfillDate"
|
||||
class="backfill-date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
:disabled-date="disableOutsideWindow"
|
||||
:clearable="false"
|
||||
/>
|
||||
<el-button @click="openBackfill">发布</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-sub">至 {{ summary.window_end || '—' }},共 {{ profile.backfill_days || 7 }} 天</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<el-date-picker
|
||||
v-model="mineRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:disabled-date="disableFutureDate"
|
||||
unlink-panels
|
||||
style="width: 260px"
|
||||
/>
|
||||
<el-button type="primary" :icon="Search" @click="searchMine">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" @click="resetMineFilters">重置</el-button>
|
||||
|
||||
<div class="backfill-group">
|
||||
<span class="backfill-label">补写往日</span>
|
||||
<el-date-picker
|
||||
v-model="backfillDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
:disabled-date="disableOutsideWindow"
|
||||
:clearable="false"
|
||||
style="width: 150px"
|
||||
/>
|
||||
<el-button @click="openBackfill">填写该日</el-button>
|
||||
<div class="overview-right">
|
||||
<div class="heatmap-card">
|
||||
<div class="heatmap-head">
|
||||
<div>
|
||||
<div class="heatmap-title">日报热力图</div>
|
||||
<div class="heatmap-sub">
|
||||
{{ calendarSubmitted }} 篇已发布 · 共 {{ calendarTotal }} 条
|
||||
</div>
|
||||
</div>
|
||||
<div class="heatmap-controls">
|
||||
<span class="legend-item"><i class="legend-cell is-submitted" />已提交</span>
|
||||
<span class="legend-item"><i class="legend-cell is-draft" />草稿</span>
|
||||
<span class="legend-item"><i class="legend-cell is-none" />未填写</span>
|
||||
<span class="legend-item"><i class="legend-cell is-backfill">补</i>可补写</span>
|
||||
<span class="heatmap-range">{{ calendarRangeText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-loading="calendarLoading" class="heatmap-body">
|
||||
<div class="heatmap-months">
|
||||
<div v-for="m in calendarMonths" :key="m.key" class="heatmap-month">
|
||||
<div class="month-head">
|
||||
<span>{{ m.label }}</span>
|
||||
<span class="month-count" :class="{ 'is-zero': !m.submitted }">
|
||||
{{ m.submitted }} 篇
|
||||
</span>
|
||||
</div>
|
||||
<div class="month-weeks">
|
||||
<div
|
||||
v-for="(week, wi) in m.weeks"
|
||||
:key="wi"
|
||||
class="week-col"
|
||||
:class="{ 'is-pad': week.pad }"
|
||||
>
|
||||
<div
|
||||
v-for="(cell, ci) in week.cells"
|
||||
:key="ci"
|
||||
class="heat-cell"
|
||||
:class="[
|
||||
cellClass(cell),
|
||||
{ 'is-today': cell && cell.date === todayStr(), 'is-backfill': cellBackfill(cell) }
|
||||
]"
|
||||
:title="cellTooltip(cell)"
|
||||
@click="handleCellClick(cell)"
|
||||
>
|
||||
<span v-if="cellBackfill(cell)" class="cell-pad">补</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -132,7 +189,7 @@
|
||||
:report-id="mineSelectedId"
|
||||
@edit="handleDetailEdit"
|
||||
@deleted="handleMineDeleted"
|
||||
@changed="loadMine"
|
||||
@changed="handleMineChanged"
|
||||
/>
|
||||
<el-empty
|
||||
v-else
|
||||
@@ -202,8 +259,8 @@
|
||||
<div class="split-layout">
|
||||
<aside class="list-pane">
|
||||
<div class="list-head">
|
||||
<span>团队成员</span>
|
||||
<span class="list-count">共 {{ teamPagination.total }} 人</span>
|
||||
<span>日报池</span>
|
||||
<span class="list-count">共 {{ teamPagination.total }} 条已提交</span>
|
||||
</div>
|
||||
<div v-loading="teamLoading" class="report-cards">
|
||||
<div
|
||||
@@ -216,15 +273,16 @@
|
||||
<div class="card-top">
|
||||
<span class="member-name">{{ row.user_name }}</span>
|
||||
<el-tag v-if="row.submitted" type="success" size="small">已提交</el-tag>
|
||||
<el-tag v-else-if="row.status === 0" type="info" size="small">草稿</el-tag>
|
||||
<el-tag v-else type="danger" size="small" effect="plain">未提交</el-tag>
|
||||
</div>
|
||||
<div class="card-summary member-org">
|
||||
{{ row.org_name || '未分配部门' }}
|
||||
<span v-if="row.account" class="member-account">{{ row.account }}</span>
|
||||
</div>
|
||||
<div class="card-summary member-content">
|
||||
{{ row.summary || '(内容为空)' }}
|
||||
</div>
|
||||
<div class="card-foot">
|
||||
<span>{{ row.submit_time || '—' }}</span>
|
||||
<span>发布于 {{ row.submit_time || '—' }}</span>
|
||||
<el-tag v-if="row.has_issue" type="warning" size="small" effect="plain">
|
||||
有反馈
|
||||
</el-tag>
|
||||
@@ -232,7 +290,7 @@
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="!teamLoading && !teamList.length"
|
||||
description="暂无团队成员"
|
||||
description="当日还没有成员发布日报"
|
||||
:image-size="70"
|
||||
/>
|
||||
</div>
|
||||
@@ -259,7 +317,7 @@
|
||||
/>
|
||||
<el-empty
|
||||
v-else
|
||||
description="选择左侧已提交的成员查看日报"
|
||||
description="选择左侧日报查看内容"
|
||||
:image-size="80"
|
||||
/>
|
||||
</section>
|
||||
@@ -285,6 +343,7 @@ import { Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDailyReportProfile,
|
||||
getMyDailyReports,
|
||||
getMyDailyReportCalendar,
|
||||
getDailyReportDetail,
|
||||
getTeamDailyReports,
|
||||
getTeamDailyReportOrgs
|
||||
@@ -310,6 +369,13 @@ const backfillDate = ref(todayStr())
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const mineSelectedId = ref(0)
|
||||
|
||||
// 近期热力图(近三个月):按天展示草稿/已提交,按月分块、按周分列
|
||||
const calendarLoading = ref(false)
|
||||
const calendarDays = ref([])
|
||||
const calendarRange = ref({ start: '', end: '' })
|
||||
const calendarTotal = ref(0)
|
||||
const calendarSubmitted = ref(0)
|
||||
|
||||
// ---------------- 团队日报 ----------------
|
||||
const teamLoading = ref(false)
|
||||
const teamList = ref([])
|
||||
@@ -343,9 +409,71 @@ const todayStatusClass = computed(() => {
|
||||
return 'is-red'
|
||||
})
|
||||
|
||||
const calendarMap = computed(() => {
|
||||
const map = new Map()
|
||||
calendarDays.value.forEach((day) => {
|
||||
// 后端 DATE 字段可能带出 "2026-10-21 00:00:00 +0800 CST",统一截取到 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
|
||||
if (!start || !end) return ''
|
||||
return `${start} 至 ${end}`
|
||||
})
|
||||
|
||||
// 按区间生成热力图网格:每列为一周(周一在上、周日在下),月内首尾补空格
|
||||
const calendarMonths = computed(() => {
|
||||
const map = calendarMap.value
|
||||
const today = todayStr()
|
||||
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 })
|
||||
}
|
||||
// 每月固定按 6 周(42 天)渲染,保证三个月格子大小一致
|
||||
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
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await loadProfile()
|
||||
await loadMine()
|
||||
loadCalendar()
|
||||
})
|
||||
|
||||
async function loadProfile() {
|
||||
@@ -393,6 +521,61 @@ function handleMinePageChange(page) {
|
||||
loadMine()
|
||||
}
|
||||
|
||||
async function loadCalendar() {
|
||||
calendarLoading.value = true
|
||||
try {
|
||||
const res = await getMyDailyReportCalendar({ months: 3 })
|
||||
const data = res?.data || {}
|
||||
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 (err) {
|
||||
ElMessage.error(err?.message || '热力图加载失败')
|
||||
} finally {
|
||||
calendarLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
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 cellTooltip(cell) {
|
||||
if (!cell) return ''
|
||||
if (!cell.record) {
|
||||
if (cell.isFuture) return `${cell.date}(未到)`
|
||||
if (cellBackfill(cell)) return `${cell.date}(可补写,点击填写)`
|
||||
return `${cell.date}(未填写)`
|
||||
}
|
||||
return `${cell.date}(${cell.record.status === 1 ? '已提交' : '草稿'})`
|
||||
}
|
||||
|
||||
function handleCellClick(cell) {
|
||||
if (!cell || cell.isFuture) return
|
||||
if (cell.record?.id) {
|
||||
mineSelectedId.value = cell.record.id
|
||||
return
|
||||
}
|
||||
if (inWindow(cell.date)) {
|
||||
openEdit(null, cell.date)
|
||||
return
|
||||
}
|
||||
ElMessage.info('该日期没有日报,且已超出补写窗口')
|
||||
}
|
||||
|
||||
function searchMine() {
|
||||
pagination.page = 1
|
||||
loadMine()
|
||||
@@ -426,6 +609,13 @@ async function openBackfill() {
|
||||
const res = await getMyDailyReports({ page: 1, pageSize: 1, start_date: date, end_date: date })
|
||||
const existed = res?.data?.list?.[0]
|
||||
if (existed) {
|
||||
// 已提交日报已锁定,不再直接打开编辑弹窗,引导到详情中撤销提交。
|
||||
if (existed.status === 1) {
|
||||
mineSelectedId.value = existed.id
|
||||
detailVersion.value++
|
||||
ElMessage.info('该日日报已提交并锁定,如需修改请先在详情中撤销提交')
|
||||
return
|
||||
}
|
||||
editingReport.value = existed
|
||||
editDefaultDate.value = date
|
||||
editVisible.value = true
|
||||
@@ -442,7 +632,15 @@ async function openEditById(id) {
|
||||
try {
|
||||
const res = await getDailyReportDetail(id)
|
||||
const report = res?.data?.report
|
||||
if (report) openEdit(report)
|
||||
if (!report) return
|
||||
// 已提交日报已锁定,引导到详情中撤销提交后再改。
|
||||
if (report.status === 1) {
|
||||
mineSelectedId.value = report.id
|
||||
detailVersion.value++
|
||||
ElMessage.info('该日报已提交并锁定,如需修改请先在详情中撤销提交')
|
||||
return
|
||||
}
|
||||
openEdit(report)
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '日报加载失败')
|
||||
}
|
||||
@@ -461,9 +659,15 @@ function handleDetailEdit(report) {
|
||||
function handleMineDeleted() {
|
||||
mineSelectedId.value = 0
|
||||
loadMine()
|
||||
loadCalendar()
|
||||
if (profile.value.can_view_team && activeTab.value === 'team') loadTeam()
|
||||
}
|
||||
|
||||
function handleMineChanged() {
|
||||
loadMine()
|
||||
loadCalendar()
|
||||
}
|
||||
|
||||
function handleTeamDeleted() {
|
||||
teamSelectedId.value = 0
|
||||
loadTeam()
|
||||
@@ -472,6 +676,7 @@ function handleTeamDeleted() {
|
||||
function handleSaved() {
|
||||
detailVersion.value++
|
||||
loadMine()
|
||||
loadCalendar()
|
||||
if (profile.value.can_view_team) loadTeam()
|
||||
}
|
||||
|
||||
@@ -553,9 +758,14 @@ function refreshAll() {
|
||||
} else {
|
||||
loadTeam()
|
||||
}
|
||||
loadCalendar()
|
||||
}
|
||||
|
||||
// ---------------- 工具 ----------------
|
||||
function pad2(n) {
|
||||
return String(n).padStart(2, '0')
|
||||
}
|
||||
|
||||
function todayStr() {
|
||||
const now = new Date()
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
@@ -642,6 +852,275 @@ function buildOrgTree(rows) {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
// ---------------- 我的日报:概览 + 热力图 ----------------
|
||||
.mine-overview {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 7fr) minmax(0, 3fr);
|
||||
gap: 14px;
|
||||
margin-bottom: 14px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.overview-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mine-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mine-stats .stat-card {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.mine-stats .stat-value {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.mine-filters {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-row :deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.mine-range {
|
||||
flex: 1 1 230px;
|
||||
min-width: 230px;
|
||||
}
|
||||
|
||||
.backfill-date {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.overview-right {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.heatmap-card {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px 14px 14px;
|
||||
background: #f7f9fc;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.heatmap-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.heatmap-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.heatmap-sub {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.heatmap-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.legend-cell {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
background: #eef1f6;
|
||||
|
||||
&.is-submitted {
|
||||
background: #67c23a;
|
||||
}
|
||||
|
||||
&.is-draft {
|
||||
background: #e6a23c;
|
||||
}
|
||||
|
||||
// 可补写图例:与热力格子一致的虚线 + 「补」
|
||||
&.is-backfill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f4f8ff;
|
||||
outline: 1px dashed #a0cfff;
|
||||
outline-offset: -1px;
|
||||
font-size: 8px;
|
||||
line-height: 1;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.heatmap-range {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.heatmap-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.heatmap-months {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.heatmap-month {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.heatmap-month + .heatmap-month {
|
||||
border-left: 1px dashed #dcdfe6;
|
||||
}
|
||||
|
||||
.month-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.month-count {
|
||||
color: #67c23a;
|
||||
|
||||
&.is-zero {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
|
||||
.month-weeks {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
align-items: stretch;
|
||||
column-gap: 3px;
|
||||
}
|
||||
|
||||
.week-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.week-col + .week-col {
|
||||
border-left: 1px dashed #ebeef5;
|
||||
}
|
||||
|
||||
.week-col.is-pad {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.heat-cell {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 2px;
|
||||
background: #eef1f6;
|
||||
transition: transform 0.12s ease;
|
||||
|
||||
&.is-empty,
|
||||
&.is-future {
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.is-future {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
&.is-submitted {
|
||||
background: #67c23a;
|
||||
}
|
||||
|
||||
&.is-draft {
|
||||
background: #e6a23c;
|
||||
}
|
||||
|
||||
&.is-none {
|
||||
background: #eef1f6;
|
||||
}
|
||||
|
||||
// 可补写:虚线描边 + 中间「补」字
|
||||
&.is-backfill {
|
||||
background: #f4f8ff;
|
||||
outline: 1px dashed #a0cfff;
|
||||
outline-offset: -1px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cell-pad {
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
color: #409eff;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&:not(.is-empty):not(.is-future):hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
&.is-today {
|
||||
box-shadow: 0 0 0 1.5px var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
@@ -695,18 +1174,36 @@ function buildOrgTree(rows) {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.backfill-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.backfill-label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
@media (max-width: 1500px) {
|
||||
.heatmap-months {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.month-weeks {
|
||||
column-gap: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.mine-overview {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mine-stats {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.heatmap-months {
|
||||
justify-content: flex-start;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- 左右结构 ----------------
|
||||
.split-layout {
|
||||
display: flex;
|
||||
@@ -815,6 +1312,11 @@ function buildOrgTree(rows) {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.member-content {
|
||||
margin-top: 4px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.card-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -868,6 +1370,7 @@ html.dark & {
|
||||
.stat-label,
|
||||
.stat-sub,
|
||||
.member-account,
|
||||
.member-content,
|
||||
.list-count,
|
||||
.card-foot {
|
||||
color: var(--el-text-color-secondary);
|
||||
@@ -914,5 +1417,64 @@ html.dark & {
|
||||
.backfill-label {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.mine-filters {
|
||||
background: var(--el-bg-color);
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.heatmap-card {
|
||||
background: var(--el-fill-color-lighter);
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.heatmap-title {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.heatmap-sub,
|
||||
.heatmap-range,
|
||||
.legend-item,
|
||||
.month-head {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.heatmap-month + .heatmap-month,
|
||||
.week-col + .week-col {
|
||||
border-color: var(--el-border-color);
|
||||
}
|
||||
|
||||
.heat-cell {
|
||||
&.is-none {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
&.is-future {
|
||||
background: var(--el-fill-color-lighter);
|
||||
}
|
||||
|
||||
&.is-empty {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.is-backfill {
|
||||
background: var(--el-fill-color-light);
|
||||
outline-color: var(--el-color-primary-light-5);
|
||||
}
|
||||
|
||||
.cell-pad {
|
||||
color: var(--el-color-primary-light-3);
|
||||
}
|
||||
}
|
||||
|
||||
.legend-cell.is-none {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.legend-cell.is-backfill {
|
||||
background: var(--el-fill-color-light);
|
||||
outline-color: var(--el-color-primary-light-5);
|
||||
color: var(--el-color-primary-light-3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user