初始提交:实习工作日志系统(设计方案 + Vue3 应用 + Electron 打包)
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import Icon from '../components/Icon.vue'
|
||||
import DayPanel from '../components/log/DayPanel.vue'
|
||||
import { getWorkRule, listAttendanceBetween, listLogItemsBetween } from '../db/repo'
|
||||
import { computeDay } from '../domain/attendance'
|
||||
import { addMonths, toDateStr, todayStr } from '../lib/date'
|
||||
import { attendanceChanged, focusDate } from '../lib/bus'
|
||||
|
||||
const monthRef = ref(todayStr())
|
||||
const year = computed(() => Number(monthRef.value.slice(0, 4)))
|
||||
const month0 = computed(() => Number(monthRef.value.slice(5, 7)) - 1)
|
||||
const title = computed(() => year.value + ' 年 ' + (month0.value + 1) + ' 月')
|
||||
const WEEK_LABELS = ['一', '二', '三', '四', '五', '六', '日']
|
||||
|
||||
const cells = computed(() => {
|
||||
const first = new Date(year.value, month0.value, 1)
|
||||
const lead = (first.getDay() + 6) % 7
|
||||
const total = new Date(year.value, month0.value + 1, 0).getDate()
|
||||
const arr: { date: string; inMonth: boolean; weekend: boolean }[] = []
|
||||
for (let i = 0; i < lead; i++) arr.push({ date: '', inMonth: false, weekend: false })
|
||||
for (let d = 1; d <= total; d++) {
|
||||
const dt = new Date(year.value, month0.value, d)
|
||||
const wd = dt.getDay()
|
||||
arr.push({ date: toDateStr(dt), inMonth: true, weekend: wd === 0 || wd === 6 })
|
||||
}
|
||||
return arr
|
||||
})
|
||||
|
||||
interface CellInfo { status: string; label: string; location: string; hasLog: boolean; hasMilestone: boolean }
|
||||
const infoMap = ref(new Map<string, CellInfo>())
|
||||
const t = todayStr()
|
||||
|
||||
async function loadMonth() {
|
||||
const start = toDateStr(new Date(year.value, month0.value, 1))
|
||||
const end = toDateStr(new Date(year.value, month0.value + 1, 0))
|
||||
const results = await Promise.all([
|
||||
getWorkRule(),
|
||||
listAttendanceBetween(start, end),
|
||||
listLogItemsBetween(start, end)
|
||||
])
|
||||
const rule = results[0]
|
||||
const attMap = new Map(results[1].map(a => [a.date, a]))
|
||||
const items = results[2]
|
||||
const m = new Map<string, CellInfo>()
|
||||
for (const c of cells.value) {
|
||||
if (!c.inMonth) continue
|
||||
const di = computeDay(attMap.get(c.date), c.date, rule, c.date === t)
|
||||
const dayItems = items.filter(i => i.date === c.date)
|
||||
m.set(c.date, {
|
||||
status: di.status,
|
||||
label: di.label,
|
||||
location: (attMap.get(c.date)?.location) || '',
|
||||
hasLog: dayItems.length > 0,
|
||||
hasMilestone: dayItems.some(i => i.is_milestone === 1)
|
||||
})
|
||||
}
|
||||
infoMap.value = m
|
||||
}
|
||||
watch([year, month0], loadMonth, { immediate: true })
|
||||
// 打卡、补卡、休假等变更后实时刷新格子状态
|
||||
watch(attendanceChanged, () => { loadMonth() })
|
||||
|
||||
function statusOf(date: string): string { return infoMap.value.get(date)?.status ?? '' }
|
||||
function cellTitle(date: string): string {
|
||||
const info = infoMap.value.get(date)
|
||||
if (!info) return date
|
||||
return date + ' · ' + info.label + (info.location ? ' · ' + info.location : '')
|
||||
}
|
||||
function hasLogOf(date: string): boolean { return !!infoMap.value.get(date)?.hasLog }
|
||||
function hasStarOf(date: string): boolean { return !!infoMap.value.get(date)?.hasMilestone }
|
||||
|
||||
function prevMonth() { monthRef.value = addMonths(monthRef.value, -1) }
|
||||
function nextMonth() { monthRef.value = addMonths(monthRef.value, 1) }
|
||||
function goToday() { monthRef.value = todayStr() }
|
||||
|
||||
const selected = ref<string | null>(null)
|
||||
function selectDay(date: string, inMonth: boolean) {
|
||||
if (inMonth) selected.value = date
|
||||
}
|
||||
|
||||
// 全局搜索命中工作记录后跳转并打开当日抽屉
|
||||
watch(focusDate, d => {
|
||||
if (!d) return
|
||||
monthRef.value = d
|
||||
selected.value = d
|
||||
focusDate.value = null
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cal-page">
|
||||
<div class="cal-toolbar">
|
||||
<button class="icon-btn" @click="prevMonth"><Icon name="chevron-left" :size="16" /></button>
|
||||
<span class="cal-title num">{{ title }}</span>
|
||||
<button class="icon-btn" @click="nextMonth"><Icon name="chevron-right" :size="16" /></button>
|
||||
<button class="btn btn-sm" @click="goToday">回到今天</button>
|
||||
</div>
|
||||
|
||||
<div class="cal-head">
|
||||
<span v-for="w in WEEK_LABELS" :key="w" class="cal-week muted">{{ w }}</span>
|
||||
</div>
|
||||
<div class="cal-grid">
|
||||
<button
|
||||
v-for="(c, idx) in cells" :key="idx"
|
||||
class="cal-cell"
|
||||
:class="{ blank: !c.inMonth, weekend: c.weekend, selected: selected === c.date }"
|
||||
:title="c.inMonth ? cellTitle(c.date) : undefined"
|
||||
@click="selectDay(c.date, c.inMonth)"
|
||||
>
|
||||
<template v-if="c.inMonth">
|
||||
<span class="daynum num" :class="{ today: c.date === t }">{{ Number(c.date.slice(8, 10)) }}</span>
|
||||
<span class="cal-marks">
|
||||
<span v-if="statusOf(c.date)" class="dot" :class="statusOf(c.date)"></span>
|
||||
<Icon v-if="hasLogOf(c.date)" name="check-circle" :size="12" class="mark-log" />
|
||||
<Icon v-if="hasStarOf(c.date)" name="star" :size="12" class="mark-star" />
|
||||
</span>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<transition name="drawer">
|
||||
<aside v-if="selected" class="drawer">
|
||||
<div class="drawer-head">
|
||||
<span class="drawer-title">当日详情</span>
|
||||
<button class="icon-btn" @click="selected = null"><Icon name="close" :size="16" /></button>
|
||||
</div>
|
||||
<div class="drawer-body">
|
||||
<DayPanel :date="selected" />
|
||||
</div>
|
||||
</aside>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cal-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
|
||||
.cal-title { font-size: 14px; font-weight: 600; min-width: 96px; text-align: center; }
|
||||
|
||||
.cal-head {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
border-top: 1px solid var(--border);
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
.cal-week {
|
||||
text-align: center;
|
||||
padding: 6px 0;
|
||||
font-size: 12px;
|
||||
border-right: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.cal-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
border-top: 1px solid var(--border);
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
.cal-cell {
|
||||
min-height: 92px;
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
background: var(--bg-page);
|
||||
border: none;
|
||||
border-right: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
}
|
||||
.cal-cell:hover { background: var(--bg-subtle); }
|
||||
.cal-cell.blank { cursor: default; }
|
||||
.cal-cell.blank:hover { background: var(--bg-page); }
|
||||
.cal-cell.selected { background: var(--accent-bg); }
|
||||
.daynum.today { color: var(--accent); font-weight: 600; }
|
||||
.cal-cell.weekend .daynum { color: var(--text-3); }
|
||||
.cal-marks { display: flex; align-items: center; gap: 4px; margin-top: 8px; height: 14px; }
|
||||
.mark-log { color: var(--text-3); }
|
||||
.mark-star { color: var(--accent); }
|
||||
|
||||
.drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: min(420px, 100%);
|
||||
background: var(--bg-page);
|
||||
border-left: 1px solid var(--border);
|
||||
box-shadow: var(--shadow-float);
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.drawer-head {
|
||||
height: 48px;
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.drawer-title { font-weight: 600; }
|
||||
.drawer-body { flex: 1; overflow: auto; padding: 16px; }
|
||||
|
||||
.drawer-enter-active, .drawer-leave-active { transition: opacity 0.15s ease; }
|
||||
.drawer-enter-from, .drawer-leave-to { opacity: 0; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user