1095 lines
23 KiB
Vue
1095 lines
23 KiB
Vue
<template>
|
|
<view class="page">
|
|
<view class="header">
|
|
<view class="header-top">
|
|
<view class="title-wrap">
|
|
<text class="title">工作日报</text>
|
|
<text class="subtitle">
|
|
补写窗口 {{ profile.window_start || '-' }} 至 {{ profile.window_end || '-' }}
|
|
</text>
|
|
</view>
|
|
<view class="write-btn" @tap="writeToday">写今日日报</view>
|
|
</view>
|
|
|
|
<view class="tabs">
|
|
<view
|
|
class="tab"
|
|
:class="{ active: activeTab === 'mine' }"
|
|
@tap="switchTab('mine')"
|
|
>
|
|
我的日报
|
|
</view>
|
|
<view
|
|
v-if="profile.can_view_team"
|
|
class="tab"
|
|
:class="{ active: activeTab === 'team' }"
|
|
@tap="switchTab('team')"
|
|
>
|
|
团队日报
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- ==================== 我的日报 ==================== -->
|
|
<view v-if="activeTab === 'mine'" class="content">
|
|
<view class="summary-row">
|
|
<view class="summary-item">
|
|
<text class="summary-num">{{ summary.month_total || 0 }}</text>
|
|
<text class="summary-label">本月日报</text>
|
|
</view>
|
|
<view class="summary-item">
|
|
<text class="summary-num is-green">{{ summary.month_submitted || 0 }}</text>
|
|
<text class="summary-label">本月已提交</text>
|
|
</view>
|
|
<view class="summary-item">
|
|
<text class="summary-num" :class="todayStatusClass">{{ todayStatusText }}</text>
|
|
<text class="summary-label">今日状态</text>
|
|
</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
|
|
mode="date"
|
|
:value="backfillDate"
|
|
:start="profile.window_start || ''"
|
|
:end="profile.window_end || today"
|
|
@change="onBackfillDateChange"
|
|
>
|
|
<view class="date-chip">{{ backfillDate }}</view>
|
|
</picker>
|
|
<view class="backfill-btn" @tap="openBackfill">发布</view>
|
|
</view>
|
|
|
|
<view v-if="loading && !mineList.length" class="state-box">
|
|
<text class="state-text">加载中...</text>
|
|
</view>
|
|
<u-empty
|
|
v-else-if="!mineList.length"
|
|
mode="list"
|
|
:text="'还没有日报,点击右上角开始记录'"
|
|
:icon-size="80"
|
|
/>
|
|
<view v-else class="card-list">
|
|
<view
|
|
v-for="item in mineList"
|
|
:key="item.id"
|
|
class="report-card"
|
|
@tap="openDetail(item.id)"
|
|
>
|
|
<view class="card-head">
|
|
<view class="date-wrap">
|
|
<text class="date-text">{{ item.report_date }}</text>
|
|
<text v-if="item.report_date === today" class="today-chip">今天</text>
|
|
</view>
|
|
<u-tag
|
|
:text="item.status === 1 ? '已提交' : '草稿'"
|
|
:type="item.status === 1 ? 'success' : 'info'"
|
|
size="mini"
|
|
plain
|
|
/>
|
|
</view>
|
|
<text class="preview">{{ previewText(item) }}</text>
|
|
<view class="card-foot">
|
|
<text class="meta">评论 {{ item.comment_count || 0 }}</text>
|
|
<text class="meta">{{ item.submit_time || '未提交' }}</text>
|
|
</view>
|
|
</view>
|
|
<view class="load-more">
|
|
<text v-if="hasMore" class="load-link" @tap="loadMore">加载更多</text>
|
|
<text v-else class="load-done">没有更多了</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- ==================== 团队日报 ==================== -->
|
|
<view v-else class="content">
|
|
<view class="team-filter">
|
|
<picker mode="date" :value="teamDate" :end="today" @change="onTeamDateChange">
|
|
<view class="date-chip">{{ teamDate }}</view>
|
|
</picker>
|
|
<picker
|
|
:range="orgNames"
|
|
:value="orgIndex"
|
|
@change="onOrgChange"
|
|
>
|
|
<view class="date-chip">{{ orgNames[orgIndex] || '全部部门' }}</view>
|
|
</picker>
|
|
</view>
|
|
|
|
<view class="team-stats">
|
|
<view class="team-stat">
|
|
<text class="summary-num">{{ teamStats.total || 0 }}</text>
|
|
<text class="summary-label">应提交</text>
|
|
</view>
|
|
<view class="team-stat">
|
|
<text class="summary-num is-green">{{ teamStats.submitted || 0 }}</text>
|
|
<text class="summary-label">已提交</text>
|
|
</view>
|
|
<view class="team-stat">
|
|
<text class="summary-num is-red">{{ teamStats.missing || 0 }}</text>
|
|
<text class="summary-label">未提交</text>
|
|
</view>
|
|
<view class="team-stat">
|
|
<text class="summary-num is-orange">{{ teamStats.rate || 0 }}%</text>
|
|
<text class="summary-label">提交率</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="teamLoading && !teamList.length" class="state-box">
|
|
<text class="state-text">加载中...</text>
|
|
</view>
|
|
<u-empty
|
|
v-else-if="!teamList.length"
|
|
mode="list"
|
|
text="当日暂无成员提交日报"
|
|
:icon-size="80"
|
|
/>
|
|
<view v-else class="card-list">
|
|
<view
|
|
v-for="item in teamList"
|
|
:key="item.user_id"
|
|
class="member-card"
|
|
:class="{ clickable: item.submitted }"
|
|
@tap="openTeamReport(item)"
|
|
>
|
|
<view class="member-left">
|
|
<view class="member-avatar">{{ (item.user_name || '?').slice(0, 1) }}</view>
|
|
<view class="member-info">
|
|
<text class="member-name">{{ item.user_name }}</text>
|
|
<text class="member-org">{{ item.org_name || '未分配部门' }}</text>
|
|
</view>
|
|
</view>
|
|
<view class="member-right">
|
|
<u-tag
|
|
v-if="item.submitted"
|
|
text="已提交"
|
|
type="success"
|
|
size="mini"
|
|
plain
|
|
/>
|
|
<u-tag
|
|
v-else-if="item.status === 0"
|
|
text="草稿"
|
|
type="info"
|
|
size="mini"
|
|
plain
|
|
/>
|
|
<u-tag v-else text="未提交" type="error" size="mini" plain />
|
|
<text v-if="item.submit_time" class="member-time">{{ item.submit_time }}</text>
|
|
</view>
|
|
</view>
|
|
<view class="load-more">
|
|
<text v-if="teamHasMore" class="load-link" @tap="loadMoreTeam">加载更多</text>
|
|
<text v-else class="load-done">没有更多了</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref } from 'vue'
|
|
import { onShow, onReachBottom } from '@dcloudio/uni-app'
|
|
import {
|
|
fetchDailyReportProfile,
|
|
fetchMyDailyReportCalendar,
|
|
fetchMyDailyReports,
|
|
fetchTeamDailyReports,
|
|
fetchTeamDailyReportOrgs,
|
|
todayString
|
|
} from '@/api/oaDailyReport.js'
|
|
import { isLoggedIn } from '@/utils/auth.js'
|
|
|
|
const today = todayString()
|
|
const activeTab = ref('mine')
|
|
const profile = ref({ is_admin: false, can_view_team: false, window_start: '', window_end: '' })
|
|
const summary = ref({})
|
|
const loading = ref(false)
|
|
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)
|
|
const teamList = ref([])
|
|
const teamStats = ref({ total: 0, submitted: 0, missing: 0, rate: 0 })
|
|
const teamLoading = ref(false)
|
|
const teamPage = ref(1)
|
|
const teamHasMore = ref(false)
|
|
const orgOptions = ref([])
|
|
const orgIndex = ref(0)
|
|
const orgNames = computed(() => ['全部部门', ...orgOptions.value.map((o) => o.name)])
|
|
|
|
const todayStatusText = computed(() => {
|
|
const status = summary.value.today_status
|
|
if (status === 1) return '已提交'
|
|
if (status === 0) return '草稿'
|
|
return '未填写'
|
|
})
|
|
|
|
const todayStatusClass = computed(() => {
|
|
const status = summary.value.today_status
|
|
if (status === 1) return 'is-green'
|
|
if (status === 0) return 'is-orange'
|
|
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
|
|
if (cell.date === today) return false
|
|
return inWindow(cell.date)
|
|
}
|
|
|
|
function onCalendarCellTap(cell) {
|
|
if (!cell || cell.isFuture) return
|
|
if (cell.record?.id) {
|
|
openDetail(cell.record.id)
|
|
return
|
|
}
|
|
if (cell.date === today || 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' })
|
|
return
|
|
}
|
|
reload()
|
|
})
|
|
|
|
onReachBottom(() => {
|
|
if (activeTab.value === 'mine') {
|
|
if (hasMore.value && !loading.value) loadMine(false)
|
|
} else if (teamHasMore.value && !teamLoading.value) {
|
|
loadTeam(false)
|
|
}
|
|
})
|
|
|
|
async function reload() {
|
|
await loadProfile()
|
|
if (activeTab.value === 'mine') {
|
|
loadCalendar()
|
|
await loadMine(true)
|
|
} else {
|
|
if (!orgOptions.value.length) await loadTeamOrgs()
|
|
await loadTeam(true)
|
|
}
|
|
}
|
|
|
|
async function loadProfile() {
|
|
try {
|
|
const data = await fetchDailyReportProfile()
|
|
if (data) profile.value = { ...profile.value, ...data }
|
|
if (profile.value.window_end) backfillDate.value = profile.value.window_end
|
|
if (!profile.value.can_view_team && activeTab.value === 'team') activeTab.value = 'mine'
|
|
} catch (e) {
|
|
uni.showToast({ title: e?.message || '日报身份加载失败', icon: 'none' })
|
|
}
|
|
}
|
|
|
|
async function loadMine(reset = false) {
|
|
if (reset) {
|
|
minePage.value = 1
|
|
loading.value = true
|
|
}
|
|
try {
|
|
const data = await fetchMyDailyReports({ page: minePage.value, pageSize: 20 })
|
|
const rows = data?.list || []
|
|
mineList.value = reset ? rows : mineList.value.concat(rows)
|
|
summary.value = data?.summary || {}
|
|
hasMore.value = mineList.value.length < (data?.total || 0)
|
|
} catch (e) {
|
|
uni.showToast({ title: e?.message || '日报加载失败', icon: 'none' })
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function loadMore() {
|
|
if (!hasMore.value || loading.value) return
|
|
minePage.value += 1
|
|
loadMine(false)
|
|
}
|
|
|
|
function switchTab(name) {
|
|
if (activeTab.value === name) return
|
|
activeTab.value = name
|
|
if (name === 'mine') {
|
|
loadCalendar()
|
|
loadMine(true)
|
|
} else {
|
|
if (!orgOptions.value.length) loadTeamOrgs()
|
|
loadTeam(true)
|
|
}
|
|
}
|
|
|
|
function onBackfillDateChange(e) {
|
|
backfillDate.value = e.detail.value
|
|
}
|
|
|
|
function openBackfill() {
|
|
uni.navigateTo({ url: `/pages/oa/dailyreport/edit?date=${backfillDate.value}` })
|
|
}
|
|
|
|
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}` })
|
|
}
|
|
}
|
|
|
|
function openDetail(id) {
|
|
uni.navigateTo({ url: `/pages/oa/dailyreport/detail?id=${id}` })
|
|
}
|
|
|
|
function previewText(item) {
|
|
const text = item.today_done || item.tomorrow_plan || item.issues || ''
|
|
return text || '暂无内容'
|
|
}
|
|
|
|
// ---------------- 团队 ----------------
|
|
async function loadTeamOrgs() {
|
|
try {
|
|
const data = await fetchTeamDailyReportOrgs()
|
|
orgOptions.value = data?.list || []
|
|
} catch (e) {
|
|
// 部门列表失败不阻断团队汇总主流程
|
|
}
|
|
}
|
|
|
|
async function loadTeam(reset = false) {
|
|
if (reset) {
|
|
teamPage.value = 1
|
|
teamLoading.value = true
|
|
}
|
|
try {
|
|
const orgId = orgIndex.value > 0 ? orgOptions.value[orgIndex.value - 1]?.id || 0 : 0
|
|
const data = await fetchTeamDailyReports({
|
|
date: teamDate.value,
|
|
orgId,
|
|
page: teamPage.value,
|
|
pageSize: 20
|
|
})
|
|
const rows = data?.list || []
|
|
teamList.value = reset ? rows : teamList.value.concat(rows)
|
|
teamStats.value = data?.stats || { total: 0, submitted: 0, missing: 0, rate: 0 }
|
|
teamHasMore.value = teamList.value.length < (data?.total || 0)
|
|
} catch (e) {
|
|
uni.showToast({ title: e?.message || '团队日报加载失败', icon: 'none' })
|
|
} finally {
|
|
teamLoading.value = false
|
|
}
|
|
}
|
|
|
|
function loadMoreTeam() {
|
|
if (!teamHasMore.value || teamLoading.value) return
|
|
teamPage.value += 1
|
|
loadTeam(false)
|
|
}
|
|
|
|
function onTeamDateChange(e) {
|
|
teamDate.value = e.detail.value
|
|
loadTeam(true)
|
|
}
|
|
|
|
function onOrgChange(e) {
|
|
orgIndex.value = Number(e.detail.value) || 0
|
|
loadTeam(true)
|
|
}
|
|
|
|
function openTeamReport(item) {
|
|
if (!item.submitted) {
|
|
uni.showToast({ title: '该成员当日未提交日报', icon: 'none' })
|
|
return
|
|
}
|
|
openDetail(item.report_id)
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
@import '@/src/styles/page-common.scss';
|
|
|
|
.page {
|
|
min-height: 100vh;
|
|
background: $color-bg-page;
|
|
padding-bottom: 40rpx;
|
|
}
|
|
|
|
.header {
|
|
background: $color-card;
|
|
padding: 28rpx $page-padding-x 0;
|
|
box-shadow: $shadow-header;
|
|
}
|
|
|
|
.header-top {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 20rpx;
|
|
}
|
|
|
|
.title-wrap {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.title {
|
|
display: block;
|
|
font-size: 38rpx;
|
|
font-weight: 700;
|
|
color: $color-text;
|
|
}
|
|
|
|
.subtitle {
|
|
display: block;
|
|
margin-top: 6rpx;
|
|
font-size: 22rpx;
|
|
color: $color-text-muted;
|
|
}
|
|
|
|
.write-btn {
|
|
flex-shrink: 0;
|
|
padding: 14rpx 28rpx;
|
|
border-radius: $radius-full;
|
|
background: $color-primary;
|
|
color: #ffffff;
|
|
font-size: 26rpx;
|
|
font-weight: 500;
|
|
|
|
&:active {
|
|
background: $color-primary-dark;
|
|
}
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
gap: 48rpx;
|
|
margin-top: 24rpx;
|
|
}
|
|
|
|
.tab {
|
|
position: relative;
|
|
padding: 16rpx 0;
|
|
font-size: 28rpx;
|
|
color: $color-text-secondary;
|
|
|
|
&.active {
|
|
color: $color-primary;
|
|
font-weight: 600;
|
|
|
|
&::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 50%;
|
|
bottom: 0;
|
|
width: 40rpx;
|
|
height: 6rpx;
|
|
border-radius: 3rpx;
|
|
background: $color-primary;
|
|
transform: translateX(-50%);
|
|
}
|
|
}
|
|
}
|
|
|
|
.content {
|
|
padding: 24rpx $page-padding-x 0;
|
|
}
|
|
|
|
.summary-row {
|
|
display: flex;
|
|
@include card;
|
|
padding: 24rpx 0;
|
|
}
|
|
|
|
.summary-item {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 6rpx;
|
|
}
|
|
|
|
.summary-num {
|
|
font-size: 34rpx;
|
|
font-weight: 700;
|
|
color: $color-text;
|
|
}
|
|
|
|
.summary-label {
|
|
font-size: 22rpx;
|
|
color: $color-text-muted;
|
|
}
|
|
|
|
.is-green {
|
|
color: $color-success;
|
|
}
|
|
|
|
.is-red {
|
|
color: $color-danger;
|
|
}
|
|
|
|
.is-orange {
|
|
color: $color-warning;
|
|
}
|
|
|
|
.backfill-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16rpx;
|
|
margin-top: 20rpx;
|
|
padding: 18rpx 24rpx;
|
|
@include card;
|
|
}
|
|
|
|
.backfill-label {
|
|
font-size: 24rpx;
|
|
color: $color-text-secondary;
|
|
}
|
|
|
|
.date-chip {
|
|
padding: 10rpx 20rpx;
|
|
border-radius: $radius-sm;
|
|
background: $color-primary-bg;
|
|
color: $color-primary;
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.backfill-btn {
|
|
margin-left: auto;
|
|
font-size: 24rpx;
|
|
color: $color-primary;
|
|
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;
|
|
}
|
|
|
|
.report-card {
|
|
@include card;
|
|
padding: 24rpx;
|
|
margin-bottom: 20rpx;
|
|
|
|
&:active {
|
|
background: $color-bg-page;
|
|
}
|
|
}
|
|
|
|
.card-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16rpx;
|
|
}
|
|
|
|
.date-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12rpx;
|
|
}
|
|
|
|
.date-text {
|
|
font-size: 30rpx;
|
|
font-weight: 600;
|
|
color: $color-text;
|
|
}
|
|
|
|
.today-chip {
|
|
@include chip;
|
|
font-size: 20rpx;
|
|
}
|
|
|
|
.preview {
|
|
display: block;
|
|
margin-top: 14rpx;
|
|
font-size: 26rpx;
|
|
line-height: 1.6;
|
|
color: $color-text-secondary;
|
|
display: -webkit-box;
|
|
-webkit-box-orient: vertical;
|
|
-webkit-line-clamp: 2;
|
|
overflow: hidden;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.card-foot {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-top: 16rpx;
|
|
}
|
|
|
|
.meta {
|
|
font-size: 22rpx;
|
|
color: $color-text-muted;
|
|
}
|
|
|
|
.team-filter {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16rpx;
|
|
}
|
|
|
|
.team-stats {
|
|
display: flex;
|
|
margin-top: 20rpx;
|
|
padding: 24rpx 0;
|
|
@include card;
|
|
}
|
|
|
|
.team-stat {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 6rpx;
|
|
}
|
|
|
|
.member-card {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16rpx;
|
|
@include card;
|
|
padding: 22rpx 24rpx;
|
|
margin-bottom: 18rpx;
|
|
|
|
&.clickable:active {
|
|
background: $color-bg-page;
|
|
}
|
|
}
|
|
|
|
.member-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 18rpx;
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.member-avatar {
|
|
width: 72rpx;
|
|
height: 72rpx;
|
|
border-radius: $radius-md;
|
|
background: linear-gradient(135deg, #60a5fa, #2563eb);
|
|
color: #ffffff;
|
|
font-size: 28rpx;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.member-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
}
|
|
|
|
.member-name {
|
|
font-size: 28rpx;
|
|
font-weight: 500;
|
|
color: $color-text;
|
|
}
|
|
|
|
.member-org {
|
|
margin-top: 4rpx;
|
|
font-size: 22rpx;
|
|
color: $color-text-muted;
|
|
}
|
|
|
|
.member-right {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
gap: 6rpx;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.member-time {
|
|
font-size: 20rpx;
|
|
color: $color-text-muted;
|
|
}
|
|
|
|
.state-box {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 140rpx 0;
|
|
}
|
|
|
|
.state-text {
|
|
font-size: 26rpx;
|
|
color: $color-text-muted;
|
|
}
|
|
|
|
.load-more {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20rpx 0 8rpx;
|
|
}
|
|
|
|
.load-link {
|
|
font-size: 24rpx;
|
|
color: $color-primary;
|
|
}
|
|
|
|
.load-done {
|
|
font-size: 22rpx;
|
|
color: $color-text-muted;
|
|
}
|
|
</style>
|