优化日报模块

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

+21 -1
View File
@@ -21,8 +21,19 @@ export function getMyDailyReports(params) {
})
}
// 我的日报近期热力图:params = { months }(默认 3,含本月往前数)
// data = { start_date, end_date, total, submitted, days: [{ date, status, id }] }
export function getMyDailyReportCalendar(params) {
return request({
url: '/backend/oa/dailyreport/calendar',
method: 'get',
params
})
}
// 保存日报:{ date, today_done, tomorrow_plan, issues, attachments, submit }
// submit=true 提交,false 存草稿;同一人同一天只会有一条记录
// 注意:已提交的日报会被锁定,需先调用 withdrawDailyReport 撤销提交后才能修改
export function saveDailyReport(data) {
return request({
url: '/backend/oa/dailyreport/save',
@@ -31,7 +42,16 @@ export function saveDailyReport(data) {
})
}
// 日报详情:{ report, comments, can_edit, can_delete, can_comment }
// 撤销提交:已提交日报退回草稿,同时把本次提交的内容快照写入修改痕迹,改完需重新提交
export function withdrawDailyReport(id) {
return request({
url: `/backend/oa/dailyreport/withdraw/${id}`,
method: 'post'
})
}
// 日报详情:{ report, comments, histories, is_author, can_edit, can_delete, can_comment, can_withdraw }
// can_edit 仅在「本人草稿且处于补写窗口」时为 true;can_withdraw 仅在已提交且可撤销时为 true
export function getDailyReportDetail(id) {
return request({
url: `/backend/oa/dailyreport/detail/${id}`,
@@ -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);
+629 -67
View File
@@ -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>