feat(workspace): 日历再瘦身 + 右侧面板等高填满 + 日期跳转按钮 + 小屁浮窗化

日历
- 单元格 72→60px,日号 19→17px、字号 11→10px,事件条与周标同步收紧,
  标题区 padding 与提示文案一起压小
- 工具条右边的「月」按钮删掉,换成日历小图标按钮:悬浮/展开变蓝(primary 实底),
  点击弹出迷你月历(自带年月切换 + 回到今天),点某天即跳月并选中该日

右侧面板
- 「今日 to do」与「待办事项」改成按列高 58/42 平分,列表自身 flex 撑满并可滚动;
  之前待办被挤到底部只剩一条空文案,现在两块都填满整列
- 跳转浮层 z-index 82,压过小屁浮窗(78);设置遮罩提到 90,保证仍是最高层

AI 助手 → 小屁
- 全屏遮罩 + 右侧整条抽屉删掉,改成右上角 348px 宽浮动小窗:
  可拖动(位置记 localStorage,越界自动夹回视口)、可最小化成小条、双击标题栏切换
- App.jsx 不再因为小屁打开而 inert 主界面(只有设置才是模态),
  所以可以一边让小屁干活一边自己点系统:实测开着窗导航、选日期、填备忘录都正常
- 文案全部改名小屁,顶栏按钮、面板标题、工作台摘要里的引导语一并更新

顺手
- 顶栏搜索的 ⌘K 提示块加 nowrap,1024 宽度下不再折成两行
This commit is contained in:
Drsakura
2026-09-10 23:56:52 +08:00
parent b9d6296591
commit 4c3ec995b6
6 changed files with 294 additions and 62 deletions
+103 -2
View File
@@ -28,6 +28,8 @@ const PRIORITY_LABELS = { high: '高', medium: '中', low: '低' }
const COMPOSER_WIDTH = 306
const TIP_WIDTH = 268
const JUMP_WIDTH = 262
const JUMP_HEIGHT = 292
const MAX_CELL_EVENTS = 2
// 春节(农历正月初一)公历日期,逐年查表;2025–2035 与天文台公布数据一致。
@@ -297,9 +299,15 @@ function CalendarBoard({ todos: seedTodos, onTodoCreated }) {
const [draft, setDraft] = useState({ title: '', priority: 'medium', description: '' })
const [saving, setSaving] = useState(false)
const [formError, setFormError] = useState('')
// 快速跳转日期:工具条上的小日历按钮 + 浮出的迷你月历
const [jumpOpen, setJumpOpen] = useState(false)
const [jumpPos, setJumpPos] = useState(null)
const [jumpMonth, setJumpMonth] = useState(() => new Date(today.getFullYear(), today.getMonth(), 1))
const composerRef = useRef(null)
const tipRef = useRef(null)
const jumpRef = useRef(null)
const jumpBtnRef = useRef(null)
const anchorElRef = useRef(null)
const pendingSelectRef = useRef(null)
@@ -372,6 +380,16 @@ function CalendarBoard({ todos: seedTodos, onTodoCreated }) {
const activeKey = selectedKey || todayKey
const activeEvents = events[activeKey] || []
// 迷你月历:按真实天数出行数,避免六月/九月都留一行空
const jumpStartDay = (jumpMonth.getDay() + 6) % 7
const jumpRows = Math.ceil((jumpStartDay + new Date(jumpMonth.getFullYear(), jumpMonth.getMonth() + 1, 0).getDate()) / 7)
const jumpDays = useMemo(() => (
Array.from({ length: jumpRows * 7 }, (_, index) => {
const date = new Date(jumpMonth.getFullYear(), jumpMonth.getMonth(), index - jumpStartDay + 1)
return { date, key: toDateKey(date), currentMonth: date.getMonth() === jumpMonth.getMonth() }
})
), [jumpMonth, jumpStartDay, jumpRows])
const closeComposer = useCallback(() => {
setComposerKey(null)
setComposerAnchor(null)
@@ -487,6 +505,53 @@ function CalendarBoard({ todos: seedTodos, onTodoCreated }) {
const shiftMonth = (delta) => setMonth(current => new Date(current.getFullYear(), current.getMonth() + delta, 1))
const resetMonth = () => setMonth(new Date(today.getFullYear(), today.getMonth(), 1))
/* 跳转浮层挂在工具条按钮右缘(右对齐),下方放不下就翻到上方 */
const positionJump = useCallback((element) => {
const rect = element.getBoundingClientRect()
const left = Math.min(Math.max(12, rect.right - JUMP_WIDTH), Math.max(12, window.innerWidth - JUMP_WIDTH - 12))
const flipUp = rect.bottom + 9 + JUMP_HEIGHT > window.innerHeight && rect.top - 9 - JUMP_HEIGHT > 0
setJumpPos({ left, top: flipUp ? rect.top - 9 : rect.bottom + 9, placement: flipUp ? 'above' : 'below' })
}, [])
const toggleJump = (event) => {
if (jumpOpen) { setJumpOpen(false); return }
jumpBtnRef.current = event.currentTarget
positionJump(event.currentTarget)
setJumpMonth(new Date(month.getFullYear(), month.getMonth(), 1))
setJumpOpen(true)
}
const pickJumpDay = (key) => {
const [year, monthIndex] = key.split('-').map(Number)
setMonth(new Date(year, monthIndex - 1, 1))
setSelectedKey(key)
setJumpOpen(false)
}
// 点空白处收起跳转浮层(点按钮本身交给 toggleJump 处理)
useEffect(() => {
if (!jumpOpen) return undefined
const onPointerDown = (event) => {
if (jumpRef.current?.contains(event.target)) return
if (event.target.closest?.('.calendar-jump-btn')) return
setJumpOpen(false)
}
document.addEventListener('mousedown', onPointerDown)
return () => document.removeEventListener('mousedown', onPointerDown)
}, [jumpOpen])
// 滚动 / 缩放时跟着按钮走
useEffect(() => {
if (!jumpOpen) return undefined
const reposition = () => { if (jumpBtnRef.current) positionJump(jumpBtnRef.current) }
window.addEventListener('scroll', reposition, true)
window.addEventListener('resize', reposition)
return () => {
window.removeEventListener('scroll', reposition, true)
window.removeEventListener('resize', reposition)
}
}, [jumpOpen, positionJump])
const submitDraft = async (event) => {
event.preventDefault()
const title = draft.title.trim()
@@ -521,7 +586,7 @@ function CalendarBoard({ todos: seedTodos, onTodoCreated }) {
<h2><CalendarIcon size={18} /> 工作日历</h2>
<p className="calendar-hint">单击日期选中,再次单击添加日程 · 悬浮日程或节日查看详情</p>
</div>
<div className="calendar-toolbar"><button className="calendar-today-btn" onClick={resetMonth}>今天</button><button onClick={() => shiftMonth(-1)} aria-label="上个月">‹</button><strong>{monthLabel}</strong><button onClick={() => shiftMonth(1)} aria-label="下个月">›</button><button className="calendar-view-btn">月</button></div>
<div className="calendar-toolbar"><button className="calendar-today-btn" onClick={resetMonth}>今天</button><button onClick={() => shiftMonth(-1)} aria-label="上个月">‹</button><strong>{monthLabel}</strong><button onClick={() => shiftMonth(1)} aria-label="下个月">›</button><button type="button" ref={jumpBtnRef} className={`calendar-jump-btn${jumpOpen ? ' is-open' : ''}`} onClick={toggleJump} aria-label="快速跳转日期" aria-expanded={jumpOpen} title="快速跳转日期"><CalendarIcon size={16} /></button></div>
</div>
<div className="calendar-layout">
@@ -721,6 +786,42 @@ function CalendarBoard({ todos: seedTodos, onTodoCreated }) {
)}
</div>
)}
{jumpOpen && jumpPos && (
<div
className="calendar-popover calendar-jump-pop"
ref={jumpRef}
role="dialog"
aria-label="快速跳转日期"
style={{
left: jumpPos.left,
top: jumpPos.top,
transform: jumpPos.placement === 'above' ? 'translateY(-100%)' : 'none'
}}
>
<div className="jump-head">
<button type="button" onClick={() => setJumpMonth(current => new Date(current.getFullYear(), current.getMonth() - 1, 1))} aria-label="上个月">‹</button>
<strong>{jumpMonth.getFullYear()}年{jumpMonth.getMonth() + 1}月</strong>
<button type="button" onClick={() => setJumpMonth(current => new Date(current.getFullYear(), current.getMonth() + 1, 1))} aria-label="下个月">›</button>
</div>
<div className="jump-week">{WEEKDAYS.map(day => <span key={day}>{day.slice(1)}</span>)}</div>
<div className="jump-grid">
{jumpDays.map(({ date, key, currentMonth }) => (
<button
type="button"
key={key}
className={`jump-day${currentMonth ? '' : ' is-outside'}${key === todayKey ? ' is-today' : ''}${key === selectedKey ? ' is-selected' : ''}`}
onClick={() => pickJumpDay(key)}
aria-label={describeDate(key)}
title={describeDate(key)}
>
{date.getDate()}
</button>
))}
</div>
<button type="button" className="jump-today" onClick={() => pickJumpDay(todayKey)}>回到今天</button>
</div>
)}
</section>
)
}
@@ -755,7 +856,7 @@ function Dashboard() {
<div className="dashboard-content-grid">
<div className="dashboard-primary-column">
<section className="dashboard-panel ai-digest-panel"><div className="dashboard-panel-header"><div><span className="section-label">[AI BRIEFING]</span><h2><SparkIcon size={16} /> 今日工作摘要</h2></div><span className="panel-live-status"><i />实时</span></div><div className="ai-digest-body"><strong>系统已准备好协助你处理今天的业务。</strong><p>当前共有 <b>{stats.pendingTodos || 0}</b> 项待办、<b>{stats.activeDeals || 0}</b> 个进行中商机和 <b>{stats.orders || 0}</b> 笔进行中订单。打开右上角 AI 助手,可以直接用自然语言查阅和操作系统。</p><div className="digest-tags"><span>供应链</span><span>客户跟进</span><span>订单节奏</span></div></div></section>
<section className="dashboard-panel ai-digest-panel"><div className="dashboard-panel-header"><div><span className="section-label">[AI BRIEFING]</span><h2><SparkIcon size={16} /> 今日工作摘要</h2></div><span className="panel-live-status"><i />实时</span></div><div className="ai-digest-body"><strong>系统已准备好协助你处理今天的业务。</strong><p>当前共有 <b>{stats.pendingTodos || 0}</b> 项待办、<b>{stats.activeDeals || 0}</b> 个进行中商机和 <b>{stats.orders || 0}</b> 笔进行中订单。打开右上角的小屁,可以直接用自然语言查阅和操作系统。</p><div className="digest-tags"><span>供应链</span><span>客户跟进</span><span>订单节奏</span></div></div></section>
<section className="dashboard-panel"><div className="dashboard-panel-header"><div><span className="section-label">[ACTIVITY LOG]</span><h2><ActivityIcon size={17} /> 最近活动</h2></div><button className="panel-link">查看日志</button></div>{recentActivities.length === 0 ? <div className="dashboard-empty">暂无最近活动</div> : <ul className="activity-list">{recentActivities.map(activity => <li key={activity.id} className="activity-item"><span className="activity-time">{new Date(activity.created_at).toLocaleString('zh-CN')}</span><div className="activity-content"><strong>{activity.subject}</strong><span>{activity.content}</span></div></li>)}</ul>}</section>
</div>