diff --git a/src/components/checkin/DayRing.jsx b/src/components/checkin/DayRing.jsx new file mode 100644 index 0000000..6a3f2eb --- /dev/null +++ b/src/components/checkin/DayRing.jsx @@ -0,0 +1,50 @@ +// 打卡日历的日期标记: 环形进度 (F-CL-06) +// +// 为什么是环而不是圆点: +// 旧实现是 4px 小圆点, 只按状态上色 —— 3/5 和 5/5 都是同一个绿点, +// 看不出"当天做到几成"。改成环形后 **弧长 = 当天完成比例**, 颜色仍表状态: +// 0 < actual < expected -> 弧长按比例 (部分完成 / 半填充) +// actual >= expected -> 整圈 (全部完成 / 实心) +// actual = 0 且 expected > 0 -> 只有底环 (未开始 / 空心) +// expected = 0 由父组件判断, 不渲染本组件 (当天没有应打卡项) +// +// 颜色不写在这里: 由 CSS 按 `.mv-day` 作用域上色 (各视图调色板可能不同)。 +const DEFAULT_SIZE = 30 +const DEFAULT_STROKE = 2.5 + +export default function DayRing({ + status = 'partial', + ratio = 0, + size = DEFAULT_SIZE, + stroke = DEFAULT_STROKE, +}) { + const p = Math.max(0, Math.min(1, Number(ratio) || 0)) + const r = (size - stroke) / 2 + const c = 2 * Math.PI * r + const half = size / 2 + return ( + + ) +} diff --git a/src/pages/checkin/CalendarView.jsx b/src/pages/checkin/CalendarView.jsx deleted file mode 100644 index 3196631..0000000 --- a/src/pages/checkin/CalendarView.jsx +++ /dev/null @@ -1,173 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' -import { Button, DotLoading, Empty, Segmented, Toast } from 'antd-mobile' -import { Celebrate } from '../../components/RingProgress.jsx' -import { checkinApi } from '../../api/checkin.js' -import useRefreshOnVisible from '../../hooks/useRefreshOnVisible.js' -import { - toDateStr, addDays, addMonths, monthGrid, weekdayLabels, dayLabel, fmtMonth, -} from './date.js' - -// 日历视图: 月历网格 + 今天/昨天/明天 三视图 + 日期标记 (F-CL-*) -export default function CalendarView({ date, onDateChange }) { - const today = toDateStr() - const [month, setMonth] = useState(() => date.slice(0, 7)) - const [cal, setCal] = useState(null) // 月历数据 - const [detail, setDetail] = useState(null) // 选中日详情 - const [err, setErr] = useState('') - const [bursts, setBursts] = useState({}) - - const loadCal = useCallback((silent) => { - if (!silent) setErr('') - checkinApi.calendar(month) - .then(setCal) - .catch((e) => { if (!silent) setErr(e.message) }) - }, [month]) - - const loadDetail = useCallback((silent) => { - checkinApi.day(date).then(setDetail).catch(() => {}) - }, [date]) - - useEffect(() => { loadCal(false) }, [loadCal]) - useEffect(() => { setDetail(null); loadDetail(false) }, [loadDetail]) - useRefreshOnVisible(() => { loadCal(true); loadDetail(true) }) - - // 切换日期时, 若跨月则同步月份 - useEffect(() => { - const m = date.slice(0, 7) - setMonth((cur) => (cur === m ? cur : m)) - }, [date]) - - const days = useMemo(() => monthGrid(month), [month]) - const byDate = useMemo(() => { - const map = new Map() - ;(cal?.days || []).forEach((d) => map.set(d.date, d)) - return map - }, [cal]) - - const quickValue = useMemo(() => { - if (date === today) return 0 - if (date === addDays(today, -1)) return -1 - if (date === addDays(today, 1)) return 1 - return null - }, [date, today]) - - async function checkFromCalendar(item) { - if (!item.canCheck) return - const snapshot = detail - setDetail((d) => ({ - ...d, - items: d.items.map((x) => { - if (x.id !== item.id) return x - const done = x.done + 1 - const countOnDate = (x.countOnDate || 0) + 1 - return { - ...x, done, countOnDate, - percent: Math.min(1, done / x.target), - completed: done >= x.target, - over: Math.max(0, done - x.target), - dayDone: countOnDate >= x.expectedPerDay, - } - }), - })) - try { - const res = await checkinApi.check(item.id, date) - const fresh = await checkinApi.day(date) - setDetail(fresh) - if (res.item.completed) setBursts((m) => ({ ...m, [item.id]: (m[item.id] || 0) + 1 })) - loadCal(true) - } catch (e) { - setDetail(snapshot) - Toast.show({ content: e.message || '打卡失败' }) - } - } - - const items = detail?.items || [] - const isFuture = !!detail?.isFuture - - return ( -
-
- setMonth((m) => addMonths(m, -1))}>‹ - {fmtMonth(month)} - setMonth((m) => addMonths(m, 1))}>› - onDateChange(today)}>回到今天 -
- -
- onDateChange(addDays(today, Number(v)))} - /> -
- - {err &&
{err} loadCal(false)}>重试
} - -
-
- {weekdayLabels().map((w) => {w})} -
-
- {days.map((d) => { - const info = byDate.get(d) - const inMonth = d.slice(0, 7) === month - const status = info?.status || 'empty' - return ( -
onDateChange(d)} - > - {Number(d.slice(8, 10))} - {inMonth && status !== 'empty' && ( - - )} -
- ) - })} -
-
- - {cal && ( -
本月完成率 {Math.round((cal.rate || 0) * 100)}%
- )} - -
-
- {dayLabel(date, today)} · {date} - {isFuture && 计划(不可打卡)} -
- - {detail === null &&
加载中...
} - {detail !== null && items.length === 0 && } - - {items.map((item) => ( -
- - {item.name} - - {item.periodLabel} {item.target} 次 · 当日 {item.countOnDate || 0}/{item.expectedPerDay} - - - {item.canCheck ? ( - - ) : ( - {item.dayDone ? '计划' : '待办'} - )} - - -
- ))} -
-
- ) -} diff --git a/src/pages/checkin/HabitView.jsx b/src/pages/checkin/HabitView.jsx deleted file mode 100644 index 56eeab4..0000000 --- a/src/pages/checkin/HabitView.jsx +++ /dev/null @@ -1,338 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' -import { - Button, Dialog, DotLoading, Empty, Input, Popup, Selector, Stepper, Toast, -} from 'antd-mobile' -import RingProgress, { Celebrate } from '../../components/RingProgress.jsx' -import useHabitDrag from '../../components/checkin/useHabitDrag.js' -import { checkinApi } from '../../api/checkin.js' -import useRefreshOnVisible from '../../hooks/useRefreshOnVisible.js' -import { toDateStr, dayLabel } from './date.js' - -const PERIOD_OPTIONS = [ - { label: '每天', value: 'day' }, - { label: '每周', value: 'week' }, - { label: '每月', value: 'month' }, - { label: '每年', value: 'year' }, -] - -const COLORS = ['#1677ff', '#00b578', '#ff8f1f', '#ff3141', '#a855f7', '#14b8a6', '#f43f9e'] - -const EMPTY_FORM = { id: null, name: '', period: 'day', target: 1, color: COLORS[0], note: '' } - -// 打卡视图: 今日待办列表 + 环形进度 + 打卡/撤销 + 新建/编辑 -export default function HabitView({ date, onDateChange }) { - const [data, setData] = useState(null) // { today, date, items, summary } - const [err, setErr] = useState('') - const [bursts, setBursts] = useState({}) // { habitId: 触发计数 } - const [form, setForm] = useState(EMPTY_FORM) - const [showForm, setShowForm] = useState(false) - - const load = useCallback((silent) => { - if (!silent) setErr('') - checkinApi.habits(date) - .then((d) => setData(d)) - .catch((e) => { if (!silent) setErr(e.message) }) - }, [date]) - - useEffect(() => { load(false) }, [load]) - useRefreshOnVisible(() => load(true)) - - const items = useMemo(() => { - const list = data?.items || [] - // 手动排序优先 (sortOrder), 拖动排序后覆盖 F-CK-16 的"未完成在前" - return [...list].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0) || a.id - b.id) - }, [data]) - - // 按周期分组 (每天/每周/每月/每年), 组内顺序 = 手动排序 - const groups = useMemo(() => { - const out = [] - for (const p of PERIOD_OPTIONS) { - const list = items.filter((i) => i.period === p.value) - if (list.length) out.push({ key: p.value, label: p.label, list }) - } - const known = new Set(PERIOD_OPTIONS.map((p) => p.value)) - const rest = items.filter((i) => !known.has(i.period)) - if (rest.length) out.push({ key: 'other', label: '其他', list: rest }) - return out - }, [items]) - - const drag = useHabitDrag({ onDrop: reorderHabit }) - - // 组内拖动落位: 重排后按展平顺序整体重写 sortOrder - async function reorderHabit(dragged, targetId, after) { - const snapshot = data - const g = groups.find((x) => x.key === dragged.period) - if (!g) return - const rest = g.list.filter((i) => i.id !== dragged.id) - const ti = rest.findIndex((i) => i.id === targetId) - if (ti < 0) return - rest.splice(after ? ti + 1 : ti, 0, dragged) - const flat = groups.map((x) => (x.key === g.key ? rest : x.list)).flat() - const withOrder = flat.map((it, i) => ({ ...it, sortOrder: i })) - setData((d) => ({ ...d, items: withOrder })) - try { - await checkinApi.reorderHabits(flat.map((i) => i.id)) - } catch (e) { - setData(snapshot) - Toast.show({ content: e.message || '排序保存失败' }) - } - } - - const fireBurst = (id) => setBursts((m) => ({ ...m, [id]: (m[id] || 0) + 1 })) - - // 乐观更新: 先本地 +1, 成功用服务端结果覆盖, 失败回滚 (F-SY-06/E-02) - async function doCheck(item) { - const before = data - const patch = (it) => { - const done = it.done + 1 - return { - ...it, - done, - remaining: Math.max(0, it.target - done), - percent: Math.min(1, done / it.target), - completed: done >= it.target, - over: Math.max(0, done - it.target), - countOnDate: (it.countOnDate || 0) + 1, - dayDone: (it.countOnDate || 0) + 1 >= it.expectedPerDay, - } - } - setData((d) => ({ ...d, items: d.items.map((x) => (x.id === item.id ? patch(x) : x)) })) - try { - const res = await checkinApi.check(item.id, date) - setData((d) => ({ ...d, items: d.items.map((x) => (x.id === item.id ? res.item : x)) })) - if (res.item.completed) fireBurst(item.id) - } catch (e) { - setData(before) - Toast.show({ content: e.message || '打卡失败' }) - } - } - - async function doUncheck(item) { - const before = data - setData((d) => ({ - ...d, - items: d.items.map((x) => { - if (x.id !== item.id) return x - const done = Math.max(0, x.done - 1) - return { - ...x, - done, - remaining: Math.max(0, x.target - done), - percent: Math.min(1, done / x.target), - completed: done >= x.target, - over: Math.max(0, done - x.target), - countOnDate: Math.max(0, (x.countOnDate || 0) - 1), - } - }), - })) - try { - const res = await checkinApi.uncheck(item.id, date) - setData((d) => ({ ...d, items: d.items.map((x) => (x.id === item.id ? res.item : x)) })) - } catch (e) { - setData(before) - Toast.show({ content: e.message || '撤销失败' }) - } - } - - async function submitForm() { - const name = String(form.name || '').trim() - if (!name) return Toast.show({ content: '请填写名称' }) - if (name.length > 20) return Toast.show({ content: '名称不能超过 20 个字' }) - const payload = { name, period: form.period, target: Number(form.target) || 1, color: form.color, note: form.note } - try { - if (form.id) await checkinApi.updateHabit(form.id, payload) - else await checkinApi.createHabit(payload) - setShowForm(false) - setForm(EMPTY_FORM) - load(true) - } catch (e) { - Toast.show({ content: e.message || '保存失败' }) - } - } - - async function removeHabit(item) { - const ok = await Dialog.confirm({ - content: `归档「${item.name}」?归档后不再出现在待办,历史记录保留。`, - confirmText: '归档', - cancelText: '取消', - }) - if (!ok) return - try { - await checkinApi.deleteHabit(item.id) - Toast.show({ content: '已归档' }) - load(true) - } catch (e) { - Toast.show({ content: e.message || '操作失败' }) - } - } - - const summary = data?.summary || { total: 0, done: 0 } - const isToday = data ? data.date === data.today : true - - return ( -
-
-
-
{dayLabel(date, data?.today || toDateStr())}
-
- 已完成 {summary.done} / {summary.total} - {!isToday && 非今天} -
-
-
- -
-
- - {err && ( -
- {err} - load(false)}>重试 -
- )} - - {data === null && !err &&
加载中...
} - - {data !== null && !err && items.length === 0 && ( -
- - -
- )} - -
- {groups.map((g) => ( -
-
- {g.label} - {g.list.length} -
- {g.list.map((item) => ( -
- { if (!drag.justEnded()) doCheck(item) }} - /> -
-
{item.name}
-
- {item.periodLabel} {item.target} 次 - {item.streak > 0 && 连续 {item.streak}} -
- {item.note &&
{item.note}
} -
- {item.periodStart === item.periodEnd - ? '今日' - : `${item.periodStart.slice(5)} ~ ${item.periodEnd.slice(5)}`} - {item.over > 0 && 超额 {item.over}} -
-
-
- -
-
- {item.done > 0 && ( - { if (!drag.justEnded()) doUncheck(item) }}>撤销 - )} - { - if (drag.justEnded()) return - setForm({ id: item.id, name: item.name, period: item.period, target: item.target, color: item.color, note: item.note }) - setShowForm(true) - }} - >编辑 - { if (!drag.justEnded()) removeHabit(item) }}>归档 -
- -
- ))} -
- ))} -
- - {/* 新建 / 编辑 */} - setShowForm(false)} - bodyStyle={{ borderTopLeftRadius: 12, borderTopRightRadius: 12, paddingBottom: 16 }} - > -
-
{form.id ? '编辑打卡项' : '新建打卡项'}
-
- - setForm((f) => ({ ...f, name: v }))} - /> -
-
- - setForm((f) => ({ ...f, period: v[0] }))} - /> -
-
- - setForm((f) => ({ ...f, target: v }))} /> -
-
- -
- {COLORS.map((c) => ( - setForm((f) => ({ ...f, color: c }))} - /> - ))} -
-
-
- - setForm((f) => ({ ...f, note: v }))} - /> -
-
- - -
-
-
-
- ) -} diff --git a/src/pages/checkin/MergedView.jsx b/src/pages/checkin/MergedView.jsx index 60a09ee..56ca633 100644 --- a/src/pages/checkin/MergedView.jsx +++ b/src/pages/checkin/MergedView.jsx @@ -9,6 +9,7 @@ import { Button, Dialog, DotLoading, Empty, Input, Popup, Selector, Stepper, Toast, } from 'antd-mobile' import RingProgress, { Celebrate } from '../../components/RingProgress.jsx' +import DayRing from '../../components/checkin/DayRing.jsx' import useHabitDrag from '../../components/checkin/useHabitDrag.js' import { checkinApi } from '../../api/checkin.js' import useRefreshOnVisible from '../../hooks/useRefreshOnVisible.js' @@ -266,6 +267,8 @@ export default function MergedView({ date, onDateChange }) { const info = byDate.get(d) const inMonth = d.slice(0, 7) === month const status = info?.status || 'empty' + // 环的弧长 = 当天完成比例 (F-CL-06); 未来日期不画环 (计划 != 未完成) + const showRing = inMonth && !!info && !info.isFuture && (info.expected || 0) > 0 return (
onDateChange(d)} > - {Number(d.slice(8, 10))} - {inMonth && status !== 'empty' && } + + {showRing && ( + + )} + {Number(d.slice(8, 10))} +
) })} @@ -284,8 +291,13 @@ export default function MergedView({ date, onDateChange }) { {cal && (
- 本月完成率 - {Math.round((cal.rate || 0) * 100)}% + 本月完成率 {Math.round((cal.rate || 0) * 100)}% + {/* 图例: 弧长=完成比例, 颜色=状态 */} + + 部分 + 完成 + 超额 +
)} diff --git a/src/styles/globals.css b/src/styles/globals.css index 7adf449..0c03a0c 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -666,54 +666,11 @@ body.me-safe-top .app-safe-top { height: 0; } } /* ----- 打卡视图 ----- */ -.habit-view { padding: 0 12px 20px; } -.habit-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 8px; padding: 10px 2px; } -.habit-date { font-size: 18px; font-weight: 700; color: var(--color-text); } -.habit-summary { font-size: 12px; color: var(--color-muted); margin-top: 2px; } -.habit-summary strong { color: var(--color-primary); font-size: 14px; } -.habit-badge { margin-left: 6px; font-size: 11px; color: #ad6800; background: #fff7e6; border-radius: 4px; padding: 1px 5px; } -.habit-head-right { display: flex; gap: 6px; flex-shrink: 0; } .checkin-empty { padding: 48px 0; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 12px; } -.habit-list { display: flex; flex-direction: column; gap: 10px; } -.habit-card { - position: relative; background: #fff; border-radius: 10px; padding: 12px; - display: grid; grid-template-columns: auto 1fr auto; grid-template-areas: "ring main actions" "foot foot foot"; - gap: 8px 12px; box-shadow: 0 1px 2px rgba(0,0,0,.04); - transition: background 0.2s ease; -} -.habit-card:active { background: #fafafa; } -.habit-card.done { background: #f7fbf8; } -.habit-card .ring-wrap { grid-area: ring; } -.habit-main { grid-area: main; min-width: 0; } -.habit-actions { grid-area: actions; display: flex; align-items: center; } -.habit-foot { - grid-area: foot; display: flex; gap: 14px; padding-top: 8px; border-top: 1px dashed var(--color-border); -} -.habit-name { font-size: 15px; font-weight: 600; color: var(--color-text); word-break: break-all; } -.habit-meta { font-size: 12px; color: var(--color-muted); margin-top: 3px; } -.habit-streak { margin-left: 8px; color: #ff8f1f; background: rgba(255,143,31,.12); border-radius: 4px; padding: 1px 6px; } -.habit-note { font-size: 11px; color: var(--color-muted); margin-top: 3px; } -.habit-range { font-size: 11px; color: var(--color-muted); margin-top: 3px; font-variant-numeric: tabular-nums; } -.habit-over { margin-left: 8px; color: #ff8f1f; } .habit-link { font-size: 12px; color: var(--color-primary); cursor: pointer; } .habit-link.danger { color: var(--color-danger); } -/* 打卡项分组 + 长按拖动排序 (组内) */ -.habit-view { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; } -.habit-view input, .habit-view textarea { -webkit-user-select: text; user-select: text; } -.habit-group-title { - display: flex; align-items: center; gap: 7px; padding: 10px 2px 4px; - font-size: 12px; color: var(--color-muted); font-weight: 600; -} -.habit-group-title::before { content: ""; width: 3px; height: 12px; border-radius: 2px; background: var(--color-primary); } -.habit-group-title .cnt { color: #b8bcc4; font-weight: 400; } -.habit-card { touch-action: pan-y; } -.habit-card.dragging { opacity: 0.55; outline: 2px dashed var(--color-primary); outline-offset: 1px; } -.habit-card.dragover { outline: 2px solid var(--color-primary); outline-offset: 1px; } -.habit-card.dragover.after { border-bottom: 3px solid var(--color-primary); } -.habit-card.dragover.before { border-top: 3px solid var(--color-primary); } - /* 拖动把手 (⠿): touch-action:none —— 浏览器完全不接管该触摸, 指针流不会被滚动抢占取消, 是 Android WebView 上唯一可靠的拖动入口 */ .grip { @@ -777,46 +734,6 @@ body.me-safe-top .app-safe-top { height: 0; } box-shadow: 0 12px 28px rgba(15,23,42,.28); } -/* ----- 日历视图 ----- */ -.calendar-view { padding: 0 12px 20px; } -.cal-month-bar { display: flex; align-items: center; gap: 10px; padding: 8px 2px; } -.cal-month { font-size: 16px; font-weight: 700; color: var(--color-text); } -.cal-arrow { font-size: 20px; color: var(--color-muted); cursor: pointer; padding: 0 4px; line-height: 1; } -.cal-today-btn { margin-left: auto; font-size: 12px; color: var(--color-primary); cursor: pointer; } -.cal-quick { padding: 2px 0 10px; } -.cal-grid { background: #fff; border-radius: 10px; padding: 8px 6px 10px; box-shadow: 0 1px 2px rgba(0,0,0,.04); } -.cal-weekday, .cal-days { display: grid; grid-template-columns: repeat(7, 1fr); } -.cal-weekday span { text-align: center; font-size: 11px; color: var(--color-muted); padding: 4px 0; } -.cal-day { - position: relative; height: 38px; display: flex; flex-direction: column; align-items: center; - justify-content: center; border-radius: 8px; cursor: pointer; -} -.cal-day-num { font-size: 13px; color: var(--color-text); font-variant-numeric: tabular-nums; } -.cal-day.out .cal-day-num { color: #c8c8c8; } -.cal-day.future .cal-day-num { color: #b0b0b0; } -.cal-day.today .cal-day-num { color: var(--color-primary); font-weight: 700; } -.cal-day.selected { background: var(--color-primary); } -.cal-day.selected .cal-day-num { color: #fff; font-weight: 600; } -.cal-dot { width: 5px; height: 5px; border-radius: 50%; margin-top: 3px; } -.cal-dot.partial { background: #ffc53d; } -.cal-dot.full { background: #00b578; } -.cal-dot.over { background: #a855f7; } -.cal-day.selected .cal-dot { background: #fff; } -.cal-rate { font-size: 12px; color: var(--color-muted); padding: 8px 2px; } -.cal-rate strong { color: var(--color-primary); } -.cal-detail { margin-top: 6px; } -.cal-detail-title { font-size: 14px; font-weight: 600; color: var(--color-text); padding: 6px 2px 8px; } -.cal-item { - position: relative; display: flex; align-items: center; gap: 8px; background: #fff; - border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; box-shadow: 0 1px 2px rgba(0,0,0,.04); -} -.cal-item.done { background: #f7fbf8; } -.cal-item-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } -.cal-item-name { font-size: 14px; font-weight: 600; color: var(--color-text); } -.cal-item-meta { font-size: 11px; color: var(--color-muted); flex: 1; text-align: right; font-variant-numeric: tabular-nums; } -.cal-item-right { flex-shrink: 0; } -.cal-item-plan { font-size: 12px; color: var(--color-muted); } - /* ----- 清单视图 ----- */ .list-view { padding: 0 12px 20px; } .list-view-head { display: flex; align-items: center; gap: 8px; padding: 8px 0 10px; } @@ -938,6 +855,12 @@ body.me-safe-top .app-safe-top { height: 0; } /* 上: 月历; 下: 当日打卡项, 按 每天/每周/每月 分组. 卡片紧凑化. */ .mv-view { display: flex; flex-direction: column; min-height: 0; } +/* ---- 日期标记环 (共享, 见 components/checkin/DayRing.jsx) ---- + 弧长 = 当天完成比例; 颜色按状态由各视图 CSS 分别上色 (调色板不同) */ +.day-ring { position: absolute; inset: 0; pointer-events: none; } +.day-ring-bg { stroke: #e6e9ee; } +.day-ring-bar { stroke-linecap: round; } + /* ---- 月历 ---- */ .mv-cal { background: #fff; border-radius: 10px; margin: 8px 12px 0; padding: 6px 6px 8px; box-shadow: 0 1px 2px rgba(0,0,0,.04); } .mv-month { display: flex; align-items: center; gap: 8px; padding: 2px 4px 6px; } @@ -955,13 +878,26 @@ body.me-safe-top .app-safe-top { height: 0; } .mv-day.future { color: #b0b0b0; } .mv-day.today { color: var(--color-primary); font-weight: 700; } .mv-day.sel { background: var(--color-primary); color: #fff; font-weight: 600; } -.mv-day i { width: 4px; height: 4px; border-radius: 50%; margin-top: 2px; } -.mv-day i.partial { background: #ff8f1f; } -.mv-day i.full { background: #00b578; } -.mv-day i.over { background: #ff6b6b; } -.mv-day.sel i { background: #fff; } +/* 日期标记: 环形进度 (弧长=完成比例) */ +.mv-day-inner { + position: relative; width: 26px; height: 26px; + display: flex; align-items: center; justify-content: center; +} +.mv-day .day-ring-bar.partial { stroke: #ff8f1f; } +.mv-day .day-ring-bar.full { stroke: #00b578; } +.mv-day .day-ring-bar.over { stroke: #ff6b6b; } +/* 选中日: 蓝底之上改用白色环, 弧长仍表达完成度 */ +.mv-day.sel .day-ring-bg { stroke: rgba(255, 255, 255, .38); } +.mv-day.sel .day-ring-bar { stroke: #fff; } .mv-rate { display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: var(--color-muted); padding: 7px 6px 0; } .mv-rate b { color: var(--color-primary); } +/* 图例: 环颜色 -> 状态 */ +.mv-legend { display: inline-flex; align-items: center; gap: 4px; color: #b0b6bf; } +.mv-legend i { width: 7px; height: 7px; border-radius: 50%; display: inline-block; margin-left: 5px; } +.mv-legend i:first-child { margin-left: 0; } +.mv-legend i.partial { background: #ff8f1f; } +.mv-legend i.full { background: #00b578; } +.mv-legend i.over { background: #ff6b6b; } /* ---- 当日栏 ---- */ .mv-daybar { display: flex; align-items: center; gap: 8px; padding: 10px 14px 2px; font-size: 13px; font-weight: 600; }