ui: 全站界面重设计(文字侧栏+灰底白卡层次+组件统一) fix: 添加记录弹窗不显示/计时汇总崩溃/CSV导出崩溃/记账切换类型崩溃

This commit is contained in:
root
2026-10-26 12:40:54 +08:00
parent 0f268a495e
commit 7a36fd4e04
30 changed files with 697 additions and 768 deletions
+132 -90
View File
@@ -230,20 +230,31 @@ onMounted(async () => {
}
})
</script>
<template>
<LoginView v-if="!session" @login="onLogin" />
<div v-else class="app">
<!-- 桌面侧栏:品牌 + 分组导航 + 当前用户 -->
<nav class="side">
<template v-for="(v, i) in shownViews" :key="v.key">
<div v-if="i > 0 && v.group !== shownViews[i - 1].group" class="nav-sep"></div>
<button
class="nav-btn" :class="{ active: route === v.key }"
:data-tip="v.name" @click="nav(v.key)"
>
<Icon :name="v.icon" :size="18" />
<div class="brand" @click="nav('calendar')">
<span class="brand-mark"><Icon name="calendar" :size="15" /></span>
<span class="brand-name">实习工作日志</span>
</div>
<div class="side-nav">
<template v-for="(v, i) in shownViews" :key="v.key">
<div v-if="i === 0 || v.group !== shownViews[i - 1].group" class="nav-group">{{ v.group }}</div>
<button class="nav-btn" :class="{ active: route === v.key }" :data-tip="v.name" @click="nav(v.key)">
<Icon :name="v.icon" :size="17" />
<span class="nav-label">{{ v.name }}</span>
</button>
</template>
</div>
<div class="side-foot">
<span class="avatar">{{ (session?.username || '用户').slice(0, 1).toUpperCase() }}</span>
<span class="foot-name">{{ session?.username }}</span>
<button class="icon-btn" title="退出登录" @click="doLogout">
<Icon name="export" :size="15" />
</button>
</template>
</div>
</nav>
<div class="main">
<header class="topbar">
@@ -281,9 +292,7 @@ onMounted(async () => {
<span class="dot" style="background: var(--warn)"></span>
填写本周周记
</button>
<span class="user-chip num mobile-hide">{{ session?.username }}</span>
<button class="btn btn-sm mobile-hide" @click="doLogout">退出</button>
<button class="btn btn-primary btn-sm" :disabled="clockDone" @click="doClock">
<button class="btn btn-primary" :disabled="clockDone" @click="doClock">
<Icon :name="clockDone ? 'check-circle' : 'clock'" :size="14" />
<span class="num">{{ clockLabel }}</span>
</button>
@@ -377,40 +386,73 @@ onMounted(async () => {
</div>
</div>
</template>
<style scoped>
.app { display: flex; height: 100vh; height: 100dvh; overflow: hidden; }
.app { display: flex; height: 100vh; height: 100dvh; overflow: hidden; background: var(--bg-app); }
/* ---------- 侧栏 ---------- */
.side {
width: 56px;
width: var(--side-w);
flex: none;
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
align-items: center;
padding-top: 10px;
gap: 4px;
background: var(--bg-page);
border-right: 1px solid var(--border);
}
.brand {
display: flex; align-items: center; gap: 9px;
height: var(--topbar-h); flex: none;
padding: 0 16px;
border-bottom: 1px solid var(--border);
cursor: pointer;
}
.brand-mark {
width: 26px; height: 26px; flex: none;
border-radius: 7px;
background: var(--accent);
color: #FFFFFF;
display: flex; align-items: center; justify-content: center;
}
.brand-name { font-size: 14px; font-weight: 600; letter-spacing: 0.3px; white-space: nowrap; }
.side-nav {
flex: 1;
overflow-y: auto;
padding: 10px;
display: flex;
flex-direction: column;
gap: 2px;
}
.nav-group {
font-size: 11px;
color: var(--text-3);
padding: 12px 8px 4px;
letter-spacing: 2px;
white-space: nowrap;
}
.nav-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
color: var(--text-3);
display: flex; align-items: center; gap: 10px;
height: 36px; padding: 0 10px;
border: none; background: transparent;
border-radius: var(--radius-sm);
color: var(--text-2);
cursor: pointer;
border: none;
background: transparent;
font-size: 13px;
position: relative;
text-align: left;
transition: background 0.12s, color 0.12s;
}
.nav-btn:hover { background: var(--bg-hover); color: var(--text-1); }
.nav-btn.active { background: var(--accent-bg); color: var(--accent); }
.nav-btn.active { background: var(--accent-bg); color: var(--accent); font-weight: 500; }
.nav-btn.active::before {
content: '';
position: absolute; left: 0; top: 8px; bottom: 8px;
width: 3px; border-radius: 2px;
background: var(--accent);
}
.nav-label { white-space: nowrap; }
.nav-btn::after {
content: attr(data-tip);
position: absolute;
left: 46px;
left: 50px;
top: 50%;
transform: translateY(-50%);
background: var(--text-1);
@@ -423,40 +465,57 @@ onMounted(async () => {
opacity: 0;
pointer-events: none;
transition: opacity 0.15s;
z-index: 40;
}
.nav-btn:hover::after { opacity: 1; }
.nav-sep {
width: 26px;
height: 1px;
.side-foot {
flex: none;
background: var(--border);
margin: 5px 0;
display: flex; align-items: center; gap: 8px;
padding: 10px 12px;
border-top: 1px solid var(--border);
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
.avatar {
width: 28px; height: 28px; flex: none;
border-radius: 50%;
background: var(--accent-bg);
color: var(--accent);
display: flex; align-items: center; justify-content: center;
font-size: 13px; font-weight: 600;
}
.foot-name {
flex: 1; min-width: 0;
font-size: 13px; color: var(--text-2);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* ---------- 主区 ---------- */
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.topbar {
height: 56px;
height: var(--topbar-h);
flex: none;
background: var(--bg-page);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 12px;
padding: 0 20px;
z-index: 10;
}
.title { font-size: 14px; font-weight: 600; }
.title { font-size: 15px; font-weight: 600; white-space: nowrap; }
.topbar-actions { display: flex; align-items: center; gap: 8px; }
.gsearch { position: relative; width: 300px; max-width: 38vw; }
.gs-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--text-3); }
.gs-input { height: 30px; padding-left: 30px; }
.gsearch { position: relative; width: 320px; max-width: 38vw; }
.gs-icon { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); color: var(--text-3); }
.gs-input {
height: 32px; padding-left: 31px;
border-radius: 999px;
background: var(--bg-subtle);
border-color: transparent;
}
.gs-input:focus { background: var(--bg-page); border-color: var(--accent); }
.gs-panel {
position: absolute;
top: 36px;
top: 38px;
left: 0;
right: 0;
background: var(--bg-page);
@@ -473,7 +532,7 @@ onMounted(async () => {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
padding: 7px 8px;
border-radius: var(--radius-sm);
cursor: pointer;
}
@@ -490,12 +549,6 @@ onMounted(async () => {
.gs-date { font-size: 12px; flex: none; }
.gs-empty { padding: 10px; font-size: 12px; text-align: center; }
.user-chip {
font-size: 12px;
color: var(--text-2);
padding: 0 4px;
}
.week-tip {
display: inline-flex;
align-items: center;
@@ -504,15 +557,16 @@ onMounted(async () => {
color: var(--warn);
background: var(--bg-page);
border-radius: 999px;
height: 26px;
height: 28px;
padding: 0 10px;
font-size: 12px;
cursor: pointer;
white-space: nowrap;
}
.week-tip:hover { background: var(--bg-subtle); }
.week-tip:hover { background: #FDF4EC; }
.content { flex: 1; overflow: auto; }
.content-inner { max-width: 1200px; margin: 0 auto; padding: 20px; }
.content-inner { max-width: 1280px; margin: 0 auto; padding: 20px 24px 48px; }
.toast-wrap {
position: fixed;
@@ -537,11 +591,11 @@ onMounted(async () => {
.toast-enter-from, .toast-leave-to { opacity: 0; transform: translateY(-4px); }
.toast-enter-active, .toast-leave-active { transition: all 0.15s ease; }
/* ---- 新增的手机端组件样式(桌面端隐藏) ---- */
/* ---------- 手机端组件 ---------- */
.mob-search-btn {
display: none;
width: 30px;
height: 30px;
width: 32px;
height: 32px;
border: none;
background: transparent;
border-radius: var(--radius-sm);
@@ -550,8 +604,6 @@ onMounted(async () => {
align-items: center;
justify-content: center;
}
.mobile-hide { display: initial; }
.tabbar {
display: none;
position: fixed;
@@ -565,7 +617,7 @@ onMounted(async () => {
}
.tab-btn {
flex: 1;
height: calc(52px - 6px);
height: 50px;
display: flex;
flex-direction: column;
align-items: center;
@@ -577,13 +629,13 @@ onMounted(async () => {
cursor: pointer;
font-size: 11px;
}
.tab-btn.active { color: var(--accent); }
.tab-btn.active { color: var(--accent); font-weight: 500; }
.sheet-mask {
position: fixed;
inset: 0;
z-index: 70;
background: rgba(29, 33, 41, 0.35);
background: rgba(29, 33, 41, 0.4);
}
.more-sheet {
position: absolute;
@@ -591,7 +643,7 @@ onMounted(async () => {
right: 0;
bottom: 0;
background: var(--bg-page);
border-radius: 14px 14px 0 0;
border-radius: 16px 16px 0 0;
padding: 10px 14px calc(14px + env(safe-area-inset-bottom));
display: flex;
flex-direction: column;
@@ -623,17 +675,8 @@ onMounted(async () => {
.sheet-divider { height: 1px; background: var(--border); margin: 6px 0; }
.sheet-danger { color: var(--danger); }
.msw-mask {
position: fixed;
inset: 0;
z-index: 70;
background: var(--bg-page);
}
.msw {
height: 100%;
display: flex;
flex-direction: column;
}
.msw-mask { position: fixed; inset: 0; z-index: 70; background: var(--bg-page); }
.msw { height: 100%; display: flex; flex-direction: column; }
.msw-bar {
height: 52px;
flex: none;
@@ -668,8 +711,19 @@ onMounted(async () => {
}
.msw-item:hover, .msw-item:active { background: var(--bg-hover); }
/* ---------- 平板(721-1000px):侧栏折叠为图标 ---------- */
@media (max-width: 1000px) and (min-width: 721px) {
.side { width: var(--side-w-collapsed); }
.brand { justify-content: center; padding: 0; }
.brand-name, .nav-label, .nav-group { display: none; }
.nav-btn { justify-content: center; padding: 0; width: 40px; align-self: center; }
.nav-btn.active::before { display: none; }
.side-foot { flex-direction: column; gap: 6px; }
.foot-name { display: none; }
}
/* ---------- 手机端(<=720px) ---------- */
@media (max-width: 720px) {
/* 布局骨架:顶栏一行 + 内容 + 底部标签栏 */
.app { display: block; height: 100dvh; }
.side { display: none; }
.main { display: flex; flex-direction: column; height: 100dvh; min-width: 0; }
@@ -685,29 +739,17 @@ onMounted(async () => {
.gsearch .gs-panel { display: none; }
.mob-search-btn { display: flex; }
.topbar-actions { margin-left: auto; gap: 6px; }
.mobile-hide { display: none !important; }
.week-tip { height: 28px; max-width: 118px; padding: 0 8px; white-space: nowrap; overflow: hidden; }
.topbar-actions .btn { padding: 0 10px; }
.topbar-actions .btn { padding: 0 10px; height: 32px; }
.topbar-actions .btn .num { font-size: 12px; }
.tabbar { display: flex; }
.content { flex: 1; min-height: 0; }
.content-inner {
max-width: none;
padding: 12px 12px calc(66px + env(safe-area-inset-bottom));
padding: 12px 12px calc(70px + env(safe-area-inset-bottom));
}
/* 卡片与表单:统一左右贴边、内距收紧 */
.card { border-radius: var(--radius-md); }
input.input, select.select, textarea.textarea { width: 100%; box-sizing: border-box; }
.btn { touch-action: manipulation; }
/* 打卡按钮更醒目 */
.topbar-actions .btn-primary {
height: 32px;
}
/* 提示信息对齐左上,避免遮挡底部标签栏 */
.toast-wrap {
top: 6px;
right: 10px;
+2 -24
View File
@@ -41,29 +41,7 @@ onUnmounted(() => window.removeEventListener('keydown', onKey))
</template>
<style scoped>
.mask {
position: fixed;
inset: 0;
background: rgba(29, 33, 41, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.dialog {
width: 380px;
max-width: calc(100vw - 32px);
background: var(--bg-page);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-float);
padding: 20px;
}
.dialog-title { font-size: 14px; font-weight: 600; color: var(--text-1); }
.dialog { width: 380px; }
.dialog-msg { margin-top: 8px; color: var(--text-2); font-size: 13px; line-height: 1.7; word-break: break-all; }
.dialog-btns { margin-top: 20px; display: flex; justify-content: flex-end; gap: 8px; }
.btn-danger-solid { background: var(--danger); border-color: var(--danger); color: #FFFFFF; }
.btn-danger-solid:hover { background: #D13A3F; }
.modal-enter-active, .modal-leave-active { transition: opacity 0.15s ease; }
.modal-enter-from, .modal-leave-to { opacity: 0; }
.dialog-btns { margin-top: 20px; }
</style>
+1 -38
View File
@@ -112,44 +112,7 @@ function save() {
</template>
<style scoped>
.mask {
position: fixed;
inset: 0;
background: rgba(29, 33, 41, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 90;
}
.dialog {
width: 480px;
max-width: calc(100vw - 32px);
background: var(--bg-page);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-float);
}
.dialog-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 12px;
border-bottom: 1px solid var(--border);
}
.dialog-title { font-size: 14px; font-weight: 600; color: var(--text-1); }
.dialog-body { padding: 16px 20px; }
.dialog-btns {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 20px 16px;
border-top: 1px solid var(--border);
}
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 16px; }
.field.span2 { grid-column: span 2; }
.field label { display: block; font-size: 12px; color: var(--text-2); margin-bottom: 4px; }
.dialog { width: 480px; }
.pwd-row { display: flex; gap: 8px; }
.pwd-row .input { flex: 1; }
.form-error { color: var(--danger); font-size: 12px; margin-top: 8px; }
.modal-enter-active, .modal-leave-active { transition: opacity 0.15s ease; }
.modal-enter-from, .modal-leave-to { opacity: 0; }
</style>
@@ -48,8 +48,10 @@ watch(() => props.open, async v => {
})
onUnmounted(() => window.removeEventListener('keydown', onKey))
watch(fType, t => {
fCategory.value = t === 'expense' ? EXPENSE_CATEGORIES[0] : INCOME_CATEGORIES[0]
// 切换收支类型后:重新拉取对应分类列表,并回落到第一项
watch(fType, async () => {
await loadCats()
fCategory.value = categoryOptions.value[0] || ''
})
const categoryOptions = ref<string[]>([])
@@ -158,57 +160,7 @@ function save() {
</template>
<style scoped>
.mask {
position: fixed;
inset: 0;
background: rgba(29, 33, 41, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 90;
}
.dialog {
width: 480px;
max-width: calc(100vw - 32px);
background: var(--bg-page);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-float);
}
.dialog-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 12px;
border-bottom: 1px solid var(--border);
}
.dialog-title { font-size: 14px; font-weight: 600; color: var(--text-1); }
.dialog { width: 480px; }
.add-cat { display: flex; gap: 6px; margin-top: 6px; }
.add-cat-input { flex: 1; height: 28px; font-size: 12px; }
.dialog-body { padding: 16px 20px; }
.dialog-btns {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 20px 16px;
border-top: 1px solid var(--border);
}
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 16px; }
.field.span2 { grid-column: span 2; }
.field label { display: block; font-size: 12px; color: var(--text-2); margin-bottom: 4px; }
.seg { display: inline-flex; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; }
.seg-btn {
height: 30px;
padding: 0 14px;
border: none;
background: var(--bg-page);
color: var(--text-2);
cursor: pointer;
font-size: 13px;
}
.seg-btn + .seg-btn { border-left: 1px solid var(--border); }
.seg-btn.active { background: var(--accent-bg); color: var(--accent); }
.form-error { color: var(--danger); font-size: 12px; margin-top: 8px; }
.input.invalid, .input.invalid:focus { border-color: var(--danger); }
.modal-enter-active, .modal-leave-active { transition: opacity 0.15s ease; }
.modal-enter-from, .modal-leave-to { opacity: 0; }
</style>
+21 -20
View File
@@ -163,12 +163,16 @@ async function saveMakeupForm() {
// ---- 条目操作 ----
// 弹窗开关与编辑目标分离:添加模式 editTarget 为 null,但弹窗也要打开
const editTarget = ref<LogItem | null>(null)
const dialogOpen = ref(false)
function openEdit(it: LogItem) {
editTarget.value = it
dialogOpen.value = true
}
function openAdd() {
editTarget.value = null
dialogOpen.value = true
}
async function onEditSave(payload: {
id: number
@@ -196,6 +200,7 @@ async function onEditSave(payload: {
showToast('记录已添加')
}
editTarget.value = null
dialogOpen.value = false
await load()
}
async function toggleMilestone(it: LogItem) {
@@ -398,11 +403,11 @@ async function confirmDelete() {
/>
<EditLogDialog
:open="!!editTarget"
:open="dialogOpen"
:item="editTarget"
:projects="projectsAll"
:date="date"
@cancel="editTarget = null"
@cancel="dialogOpen = false"
@save="onEditSave"
/>
</div>
@@ -412,34 +417,32 @@ async function confirmDelete() {
.dp-head { margin-bottom: 12px; }
.dp-date { font-size: 14px; font-weight: 600; }
.sec { padding: 14px 16px; margin-bottom: 12px; }
.sec-title { font-size: 13px; font-weight: 600; margin-bottom: 10px; color: var(--text-1); }
.sec-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
.sec-head .sec-title { margin-bottom: 0; }
.form-hint { font-size: 12px; }
.att-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 16px; }
.att-cell label, .field label { display: block; font-size: 12px; color: var(--text-2); margin-bottom: 4px; }
.att-cell label { display: block; font-size: 12px; color: var(--text-2); margin-bottom: 4px; }
.att-val { display: flex; align-items: center; gap: 6px; min-height: 32px; font-size: 13px; }
.status-val { min-height: 32px; }
.val-empty { color: var(--text-3); }
.edit-row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.time-input { width: 104px; }
.att-note { margin-top: 10px; font-size: 12px; }
.att-actions { display: flex; gap: 8px; margin-top: 12px; }
.att-actions { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 16px; }
.field.span2 { grid-column: span 2; }
.form-footer {
display: flex; justify-content: flex-end; gap: 8px;
margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border);
}
.form-error { color: var(--danger); font-size: 12px; margin-top: 4px; }
.input.invalid { border-color: var(--danger); }
.input.invalid:focus { border-color: var(--danger); }
.list-sec { margin-bottom: 4px; }
.item { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 8px 10px; margin-bottom: 6px; background: var(--bg-page); }
.sec-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
.sec-head .sec-title { margin-bottom: 0; }
.item {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 9px 12px;
margin-bottom: 6px;
background: var(--bg-subtle);
transition: border-color 0.12s, background 0.12s;
}
.item:hover { border-color: var(--border-strong); background: var(--bg-page); }
.item-main { display: flex; align-items: center; gap: 8px; min-height: 26px; flex-wrap: wrap; }
.item-content { cursor: text; word-break: break-all; }
.item-meta { font-size: 12px; }
@@ -447,11 +450,9 @@ async function confirmDelete() {
margin-top: 6px; padding-top: 6px; border-top: 1px dashed var(--border);
font-size: 12px; display: flex; flex-direction: column; gap: 2px;
}
.empty { text-align: center; padding: 24px 0; }
.dp-foot { margin-top: 10px; font-size: 12px; text-align: right; }
@media (max-width: 560px) {
.att-grid, .form-grid { grid-template-columns: 1fr; }
.field.span2 { grid-column: auto; }
.att-grid { grid-template-columns: 1fr; }
}
</style>
@@ -113,7 +113,7 @@ function save() {
<div class="dialog" role="dialog" aria-label="编辑工作记录">
<div class="dialog-head">
<div>
<div class="dialog-title">编辑工作记录</div>
<div class="dialog-title">{{ item ? '编辑工作记录' : '添加工作记录' }}</div>
<div v-if="date" class="muted dialog-sub">{{ date }}</div>
</div>
<button class="icon-btn" title="关闭" @click="emit('cancel')">
@@ -173,7 +173,7 @@ function save() {
</div>
<div class="dialog-btns">
<button class="btn btn-sm" @click="emit('cancel')">取消</button>
<button class="btn btn-primary btn-sm" @click="save">保存修改</button>
<button class="btn btn-primary btn-sm" @click="save">{{ item ? '保存修改' : '添加' }}</button>
</div>
</div>
</div>
@@ -182,65 +182,5 @@ function save() {
</template>
<style scoped>
.mask {
position: fixed;
inset: 0;
background: rgba(29, 33, 41, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 90;
}
.dialog {
width: 560px;
max-width: calc(100vw - 32px);
max-height: 85vh;
background: var(--bg-page);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-float);
display: flex;
flex-direction: column;
}
.dialog-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 16px 20px 12px;
border-bottom: 1px solid var(--border);
flex: none;
}
.dialog-title { font-size: 14px; font-weight: 600; color: var(--text-1); }
.dialog-sub { font-size: 12px; margin-top: 2px; }
.dialog-body { padding: 16px 20px; overflow: auto; }
.dialog-btns {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 20px 16px;
border-top: 1px solid var(--border);
flex: none;
}
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 16px; }
.field.span2 { grid-column: span 2; }
.field label { display: block; font-size: 12px; color: var(--text-2); margin-bottom: 4px; }
.form-error { color: var(--danger); font-size: 12px; margin-top: 4px; }
.input.invalid, .input.invalid:focus { border-color: var(--danger); }
.toggle-btn {
height: 32px;
width: 100%;
border: 1px solid var(--border-strong);
background: var(--bg-page);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 13px;
color: var(--text-2);
}
.toggle-btn:hover { background: var(--bg-hover); }
.toggle-btn.on { color: var(--accent); border-color: var(--accent); background: var(--accent-bg); }
.toggle-btn.onwarn { color: var(--warn); border-color: var(--warn); }
.modal-enter-active, .modal-leave-active { transition: opacity 0.15s ease; }
.modal-enter-from, .modal-leave-to { opacity: 0; }
.dialog { width: 560px; }
</style>
+3
View File
@@ -2,6 +2,9 @@
// 约定:每个写函数修改本地缓存后立即调用 commit() —— 推送差异给服务器,
// 并以服务器返回的权威快照重建缓存;断网时抛出错误(纯在线模式)。
import { db, DEFAULT_LOCATIONS, SCHEMA_VERSION, type Attendance, type Credential, type LogItem, type Memo, type Goal, type PlanItem, type Project, type Transaction, type WeeklyNote, type WorkLog } from './schema'
// 视图直接从 repo 导入这些类型:在此统一再导出
export type { Attendance, Credential, LogItem, Memo, Goal, PlanItem, Project, Transaction, WeeklyNote, WorkLog }
import { addDays, nowLocalISO, toDateStr } from '../lib/date'
import { flushAfterWrite } from '../sync/engine'
+3
View File
@@ -0,0 +1,3 @@
/// <reference types="vite/client" />
declare const __APP_VERSION__: string
+20 -13
View File
@@ -3,8 +3,8 @@
(App.vue 的壳层样式是 scoped,跨组件规则统一放这里,便于集中管理)
============================================================ */
/* 通用的白底卡片在窄屏贴边 */
@media (max-width: 720px) {
/* 卡片在窄屏贴边、圆角略收 */
.card { border-radius: 12px; }
input.input, select.select, textarea.textarea {
@@ -13,18 +13,18 @@
}
.btn { touch-action: manipulation; }
/* ---------- 弹窗:窄屏一体布局,覆盖组件内固定宽度 ---------- */
/* ---------- 弹窗:窄屏近全屏 ---------- */
.dialog {
width: calc(100vw - 20px) !important;
max-width: calc(100vw - 20px) !important;
max-height: 86vh;
max-height: 86dvh;
overflow: auto;
max-height: 88vh;
max-height: 88dvh;
}
.dialog-body { padding: 14px !important; }
.form-grid { grid-template-columns: 1fr !important; }
.dialog-btns { gap: 8px; }
.dialog-head { padding: 14px 16px 10px !important; }
.dialog-body { padding: 14px 16px !important; }
.dialog-btns { padding: 10px 16px 14px !important; }
.dialog-btns .btn { flex: 1; }
.form-grid { grid-template-columns: 1fr !important; }
/* ---------- 日历当日抽屉:改为底部抽屉 ---------- */
.drawer {
@@ -35,6 +35,7 @@
border-left: none !important;
border-top: 1px solid var(--border);
border-radius: 14px 14px 0 0 !important;
box-shadow: 0 -8px 24px rgba(16, 24, 40, 0.10);
}
.drawer-enter-active, .drawer-leave-active { transition: transform 0.2s ease, opacity 0.2s ease; }
.drawer-enter-from, .drawer-leave-to { transform: translateY(28px); opacity: 0; }
@@ -66,7 +67,7 @@
.tx-table td.empty { justify-content: center; }
.tx-table td.empty::before { content: none !important; }
/* ---------- 密码库等其余宽表:横向滚动,不再挤压折行 ---------- */
/* ---------- 密码库等其余宽表:横向滚动 ---------- */
.table-card, .tx-card { overflow-x: auto; }
.cred-table { min-width: 560px !important; }
.th-ops { width: auto !important; }
@@ -75,12 +76,18 @@
.vault-toolbar .flex1 { display: none; }
.vault-toolbar .btn { flex: none; }
/* ---------- 统计卡两列 ---------- */
.cards { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; gap: 10px !important; }
.stat-card { padding: 12px 14px !important; }
.stat-num { font-size: 19px !important; }
/* ---------- 分区与栅格收窄 ---------- */
.section, .sec { padding: 12px 12px !important; }
.rule-grid { grid-template-columns: 1fr !important; }
.two-col { grid-template-columns: 1fr !important; }
/* ---------- 日历格子收窄 ---------- */
.cal-toolbar { margin-bottom: 8px; }
.cal-cell { min-height: 54px !important; padding: 4px 5px !important; }
.cal-marks { margin-top: 3px !important; height: 12px !important; gap: 3px !important; }
/* ---------- 各种桌面习惯型小间距收敛 ---------- */
.section, .sec { padding: 12px 12px !important; }
.rule-grid { grid-template-columns: 1fr !important; }
}
+209 -42
View File
@@ -1,50 +1,72 @@
/* ============================================================
设计基座 ·「高级白」企业级
层次模型:内容区浅灰底(--bg-app) → 白色卡片(--bg-page) →
浅灰内嵌块(--bg-subtle)。所有视图共用此处组件类,禁止各自造轮子。
============================================================ */
:root {
--bg-page: #FFFFFF;
--bg-subtle: #F7F8FA;
/* 表面 */
--bg-app: #F5F6F8; /* 应用内容区底色 */
--bg-page: #FFFFFF; /* 卡片 / 面板 */
--bg-subtle: #F7F8FA; /* 表头 / 内嵌块 */
--bg-hover: #F2F3F5;
--border: #EBEDF0;
--border-strong: #DCDEE3;
/* 文字 */
--text-1: #1D2129;
--text-2: #4E5969;
--text-3: #86909C;
/* 线 */
--border: #E8EAEE;
--border-strong: #DCDFE4;
/* 品牌与语义色 */
--accent: #3370FF;
--accent-bg: #F0F5FF;
--accent-deep: #2B5FD9;
--accent-bg: #EEF4FF;
--ok: #23A757;
--warn: #E8842C;
--danger: #E5484D;
--dot-empty: #C9CDD4;
/* 几何 */
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--shadow-float: 0 4px 16px rgba(29, 33, 41, 0.08);
--radius-md: 10px;
--radius-lg: 14px;
/* 阴影 */
--shadow-card: 0 1px 2px rgba(16, 24, 40, 0.04);
--shadow-float: 0 12px 32px -8px rgba(16, 24, 40, 0.14), 0 4px 12px rgba(16, 24, 40, 0.06);
/* 布局 */
--side-w: 216px;
--side-w-collapsed: 60px;
--topbar-h: 56px;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg-page);
background: var(--bg-app);
color: var(--text-1);
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
font-size: 13px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
button, input, select, textarea { font: inherit; color: inherit; }
/* ---------- 通用工具类 ---------- */
.num { font-variant-numeric: tabular-nums; }
.flex1 { flex: 1; }
.flex1 { flex: 1; min-width: 0; }
.muted { color: var(--text-3); }
.muted2 { color: var(--text-2); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* ---------- 按钮 ---------- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 0 12px;
padding: 0 14px;
height: 32px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-strong);
@@ -52,15 +74,39 @@ button, input, select, textarea { font: inherit; color: inherit; }
color: var(--text-1);
cursor: pointer;
white-space: nowrap;
font-weight: 500;
transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.btn:hover { background: var(--bg-hover); }
.btn:hover { background: var(--bg-hover); border-color: var(--border-strong); }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #FFFFFF; }
.btn-primary:hover { background: #2B5FD9; }
.btn-danger { color: var(--danger); border-color: var(--danger); }
.btn-sm { height: 26px; padding: 0 10px; font-size: 12px; }
.btn-xs { height: 22px; padding: 0 8px; font-size: 12px; }
.btn-primary:hover { background: var(--accent-deep); border-color: var(--accent-deep); }
.btn-danger { color: var(--danger); border-color: var(--danger); background: var(--bg-page); }
.btn-danger:hover { background: #FDF0F0; }
.btn-danger-solid { background: var(--danger); border-color: var(--danger); color: #FFFFFF; }
.btn-danger-solid:hover { background: #D13A3F; }
.btn-sm { height: 28px; padding: 0 12px; font-size: 12px; }
.btn-xs { height: 24px; padding: 0 10px; font-size: 12px; }
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
flex: none;
border: none;
background: transparent;
border-radius: var(--radius-sm);
color: var(--text-3);
cursor: pointer;
transition: background 0.12s, color 0.12s;
}
.icon-btn:hover { background: var(--bg-hover); color: var(--text-1); }
.icon-btn.active { color: var(--accent); }
.icon-btn.active-warn { color: var(--warn); }
/* ---------- 表单 ---------- */
.input, .select, .textarea {
height: 32px;
padding: 0 10px;
@@ -69,30 +115,30 @@ button, input, select, textarea { font: inherit; color: inherit; }
background: var(--bg-page);
outline: none;
width: 100%;
transition: border-color 0.12s, box-shadow 0.12s;
}
.textarea { height: auto; min-height: 64px; padding: 8px 10px; resize: vertical; line-height: 1.7; }
.input:focus, .select:focus, .textarea:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(51, 112, 255, 0.10);
}
.textarea { height: auto; min-height: 56px; padding: 8px 10px; resize: vertical; }
.input:focus, .select:focus, .textarea:focus { border-color: var(--accent); }
.input::placeholder, .textarea::placeholder { color: var(--text-3); }
.input.invalid, .input.invalid:focus { border-color: var(--danger); box-shadow: 0 0 0 3px rgba(229, 72, 77, 0.10); }
.card { border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg-page); }
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
flex: none;
border: none;
background: transparent;
border-radius: var(--radius-sm);
color: var(--text-3);
cursor: pointer;
/* ---------- 卡片与分区 ---------- */
.card {
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg-page);
box-shadow: var(--shadow-card);
}
.icon-btn:hover { background: var(--bg-hover); color: var(--text-1); }
.icon-btn.active { color: var(--accent); }
.icon-btn.active-warn { color: var(--warn); }
.section { padding: 16px 18px; }
.section-title, .sec-title { font-weight: 600; margin-bottom: 12px; font-size: 13px; color: var(--text-1); }
.sec { padding: 16px 18px; }
.empty { text-align: center; padding: 28px 0; }
/* ---------- 标签与状态点 ---------- */
.tag {
display: inline-block;
padding: 0 8px;
@@ -105,19 +151,140 @@ button, input, select, textarea { font: inherit; color: inherit; }
border: 1px solid var(--border);
white-space: nowrap;
}
.dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; flex: none; }
.dot.normal { background: var(--ok); }
.dot.late, .dot.early, .dot.incomplete { background: var(--warn); }
.dot.absent { background: var(--danger); }
.dot.none, .dot.leave { background: var(--dot-empty); }
.dot.rest { background: var(--border); }
/* 移动端:避免 iOS 聚焦缩放,输入字号提到 16px */
/* ---------- 金额 ---------- */
.amt-income { color: var(--ok); }
.amt-expense { color: var(--text-1); }
.amt-warn { color: var(--warn); }
/* ---------- 分段选择器(seg) ---------- */
.seg {
display: inline-flex;
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
overflow: hidden;
background: var(--bg-page);
}
.seg-btn {
border: none;
background: var(--bg-page);
color: var(--text-2);
cursor: pointer;
font-size: 12px;
height: 28px;
padding: 0 12px;
border-right: 1px solid var(--border);
transition: background 0.12s, color 0.12s;
}
.seg-btn:last-child { border-right: none; }
.seg-btn:hover { background: var(--bg-hover); }
.seg-btn.active { background: var(--accent-bg); color: var(--accent); font-weight: 500; }
/* ---------- 开关按钮(toggle) ---------- */
.toggle-btn {
height: 32px;
width: 100%;
border: 1px solid var(--border-strong);
background: var(--bg-page);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 13px;
color: var(--text-2);
transition: all 0.12s;
}
.toggle-btn:hover { background: var(--bg-hover); }
.toggle-btn.on { color: var(--accent); border-color: var(--accent); background: var(--accent-bg); }
.toggle-btn.onwarn { color: var(--warn); border-color: var(--warn); background: #FDF4EC; }
/* ---------- 数据表格(记账/密码库/用户管理共用) ---------- */
.tx-table, .cred-table, .admin-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.tx-table th, .cred-table th, .admin-table th {
text-align: left;
font-weight: 500;
color: var(--text-3);
font-size: 12px;
background: var(--bg-subtle);
padding: 9px 14px;
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
.tx-table td, .cred-table td, .admin-table td {
padding: 10px 14px;
border-bottom: 1px solid var(--border);
vertical-align: top;
word-break: break-all;
}
.tx-table tbody tr:hover, .cred-table tbody tr:hover, .admin-table tbody tr:hover { background: #FAFBFC; }
.tx-table tbody tr:last-child td, .cred-table tbody tr:last-child td, .admin-table tbody tr:last-child td { border-bottom: none; }
/* ---------- 弹窗(全部对话框共用) ---------- */
.mask {
position: fixed;
inset: 0;
background: rgba(29, 33, 41, 0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 90;
padding: 16px;
}
.dialog {
width: 480px;
max-width: calc(100vw - 32px);
max-height: 86vh;
max-height: 86dvh;
overflow: auto;
background: var(--bg-page);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-float);
display: flex;
flex-direction: column;
}
.dialog-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 16px 20px 12px;
border-bottom: 1px solid var(--border);
flex: none;
}
.dialog-title { font-size: 14px; font-weight: 600; color: var(--text-1); }
.dialog-sub { font-size: 12px; margin-top: 2px; color: var(--text-3); }
.dialog-body { padding: 16px 20px; overflow: auto; }
.dialog-btns {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 20px 16px;
border-top: 1px solid var(--border);
flex: none;
}
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 16px; }
.form-grid .field.span2, .field.span2 { grid-column: span 2; }
.field label, .form-row label { display: block; font-size: 12px; color: var(--text-2); margin-bottom: 4px; }
.form-error { color: var(--danger); font-size: 12px; margin-top: 8px; }
/* ---------- 统计卡与横向条 ---------- */
.stat-card { padding: 14px 16px; }
.stat-num { font-size: 22px; font-weight: 600; margin-top: 2px; letter-spacing: -0.2px; }
.bar-row { display: flex; align-items: center; gap: 10px; margin: 7px 0; }
.bar-label { flex: none; color: var(--text-2); font-size: 12px; text-align: right; }
.bar-track { flex: 1; height: 8px; border-radius: 4px; background: var(--bg-subtle); overflow: hidden; }
.bar-fill { display: block; height: 100%; background: var(--accent); border-radius: 4px; }
.bar-num { font-size: 12px; flex: none; color: var(--text-3); }
/* ---------- 图标 ---------- */
.icon { display: inline-flex; align-items: center; justify-content: center; flex: none; }
.icon svg { width: 100%; height: 100%; display: block; }
/* ---------- 移动端:避免 iOS 聚焦缩放 ---------- */
@media (max-width: 720px) {
input, select, textarea { font-size: 16px !important; }
input[type="checkbox"] { font-size: initial; }
}
.dot.rest { background: var(--border); }
.icon { display: inline-flex; align-items: center; justify-content: center; flex: none; }
.icon svg { width: 100%; height: 100%; display: block; }
+35 -43
View File
@@ -87,36 +87,37 @@ watch(focusDate, 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>
<button class="icon-btn" title="上一月" @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>
<button class="icon-btn" title="下一月" @click="nextMonth"><Icon name="chevron-right" :size="16" /></button>
<button class="btn btn-sm" :disabled="monthRef === t" @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 class="card cal-card">
<div class="cal-head">
<span v-for="w in WEEK_LABELS" :key="w" class="cal-week">{{ 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>
</div>
<transition name="drawer">
@@ -132,30 +133,23 @@ watch(focusDate, d => {
</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 {
.cal-card { padding: 0; overflow: hidden; }
.cal-head, .cal-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
border-top: 1px solid var(--border);
border-left: 1px solid var(--border);
gap: 1px;
background: var(--border);
}
.cal-week {
background: var(--bg-subtle);
text-align: center;
padding: 6px 0;
padding: 7px 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);
color: var(--text-3);
}
.cal-cell {
min-height: 92px;
@@ -163,16 +157,14 @@ watch(focusDate, d => {
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;
transition: background 0.12s;
}
.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); }
.cal-cell.selected { background: var(--accent-bg); box-shadow: inset 0 0 0 2px var(--accent); }
.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; }
@@ -184,7 +176,7 @@ watch(focusDate, d => {
top: 0;
right: 0;
bottom: 0;
width: min(420px, 100%);
width: min(440px, 100%);
background: var(--bg-page);
border-left: 1px solid var(--border);
box-shadow: var(--shadow-float);
+6 -56
View File
@@ -214,7 +214,6 @@ async function confirmDelete() {
showToast('账目已删除')
}
</script>
<template>
<div class="ledger-page">
<div class="ledger-toolbar">
@@ -293,7 +292,7 @@ async function confirmDelete() {
</table>
</div>
<div class="muted vault-note num">本月 {{ rows.length }} 条 · 当前筛选 {{ filtered.length }} 条</div>
<div class="muted table-note num">本月 {{ rows.length }} 条 · 当前筛选 {{ filtered.length }} 条</div>
<TransactionDialog
:open="dialogOpen"
@@ -320,69 +319,20 @@ async function confirmDelete() {
.w-filter { width: 110px; flex: none; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }
.stat-card { padding: 14px 16px; }
.stat-num { font-size: 20px; font-weight: 600; margin-top: 4px; }
.amt-income { color: var(--ok); }
.amt-expense { color: var(--text-1); }
.amt-warn { color: var(--warn); }
.sec { padding: 14px 16px; margin-top: 12px; }
.sec-title { font-size: 13px; font-weight: 600; margin-bottom: 10px; }
.sec-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
.sec-head .sec-title { margin-bottom: 0; }
.form-hint { font-size: 12px; }
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 16px; }
.field.span2 { grid-column: span 2; }
.field label { display: block; font-size: 12px; color: var(--text-2); margin-bottom: 4px; }
.form-error { color: var(--danger); font-size: 12px; margin-top: 8px; }
.form-footer {
display: flex; justify-content: flex-end; gap: 8px;
margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border);
}
.seg { display: inline-flex; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; }
.seg-btn {
height: 30px; padding: 0 14px; border: none;
background: var(--bg-page); color: var(--text-2); cursor: pointer; font-size: 13px;
}
.seg-btn + .seg-btn { border-left: 1px solid var(--border); }
.seg-btn.active { background: var(--accent-bg); color: var(--accent); }
.toggle-btn {
height: 32px; width: 100%;
border: 1px solid var(--border-strong);
background: var(--bg-page);
border-radius: var(--radius-sm);
cursor: pointer; font-size: 13px; color: var(--text-2);
}
.toggle-btn:hover { background: var(--bg-hover); }
.toggle-btn.on { color: var(--accent); border-color: var(--accent); background: var(--accent-bg); }
.card.sec, .card.section { margin-top: 12px; }
.bar-label { width: 56px; }
.bar-num { width: 118px; }
.bar-row { display: flex; align-items: center; gap: 10px; margin: 6px 0; }
.bar-label { width: 56px; flex: none; color: var(--text-2); font-size: 12px; text-align: right; }
.bar-track { flex: 1; height: 8px; border-radius: 4px; background: var(--bg-subtle); overflow: hidden; }
.bar-fill { display: block; height: 100%; background: var(--accent); border-radius: 4px; }
.bar-num { font-size: 12px; width: 110px; flex: none; }
.table-card { overflow-x: auto; }
.tx-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.tx-table th {
text-align: left; font-weight: 500; color: var(--text-2);
background: var(--bg-subtle); padding: 10px 12px;
border-bottom: 1px solid var(--border); white-space: nowrap;
}
.tx-table td { padding: 10px 12px; border-bottom: 1px solid var(--border); vertical-align: top; word-break: break-all; }
.tx-table tbody tr:hover { background: var(--bg-subtle); }
.tx-table tbody tr:last-child td { border-bottom: none; }
.td-note { color: var(--text-2); }
.tx-pending { color: var(--warn); font-size: 12px; }
.tx-done { color: var(--ok); font-size: 12px; }
.th-ops { width: 220px; }
.ops { display: flex; gap: 4px; flex-wrap: wrap; }
.empty { text-align: center; padding: 24px 0; }
.vault-note { margin-top: 10px; font-size: 12px; text-align: right; }
.table-card { overflow-x: auto; }
.table-note { margin-top: 10px; font-size: 12px; text-align: right; }
@media (max-width: 560px) {
.form-grid { grid-template-columns: 1fr; }
.field.span2 { grid-column: auto; }
.ledger-toolbar .w-filter { flex: 1; min-width: 0; width: auto; }
}
</style>
+9 -31
View File
@@ -77,7 +77,8 @@ async function submit() {
<style scoped>
.login-page {
min-height: 100vh;
background: var(--bg-subtle);
min-height: 100dvh;
background: var(--bg-app);
display: flex;
align-items: center;
justify-content: center;
@@ -86,20 +87,21 @@ async function submit() {
.login-card {
width: 400px;
max-width: 100%;
padding: 28px 26px;
padding: 30px 28px;
display: flex;
flex-direction: column;
gap: 12px;
background: var(--bg-page);
box-shadow: var(--shadow-float);
border-radius: var(--radius-lg);
}
.login-brand { text-align: center; margin-bottom: 6px; }
.brand-icon {
width: 46px;
height: 46px;
margin: 0 auto 10px;
border-radius: var(--radius-md);
background: var(--accent-bg);
color: var(--accent);
border-radius: 12px;
background: var(--accent);
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
@@ -107,31 +109,7 @@ async function submit() {
.login-title { font-size: 18px; font-weight: 600; color: var(--text-1); }
.login-sub { font-size: 12px; margin-top: 4px; line-height: 1.7; }
.seg { display: inline-flex; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; }
.seg-btn {
flex: 1;
height: 30px;
border: none;
background: var(--bg-page);
color: var(--text-2);
cursor: pointer;
font-size: 13px;
}
.seg-btn + .seg-btn { border-left: 1px solid var(--border); }
.seg-btn.active { background: var(--accent-bg); color: var(--accent); }
.mig-row {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 12px;
color: var(--text-2);
cursor: pointer;
line-height: 1.6;
}
.mig-row input { margin-top: 3px; }
.form-error { color: var(--danger); font-size: 12px; }
.login-btn { margin-top: 2px; }
.login-btn { margin-top: 2px; height: 36px; }
.login-note { font-size: 12px; text-align: center; line-height: 1.7; }
</style>
+4 -4
View File
@@ -197,13 +197,13 @@ function fmtTime(s: string): string {
.memo-list {
display: flex;
flex-direction: column;
max-height: calc(100vh - 140px);
max-height: calc(100vh - 150px);
}
.pane-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 12px 8px;
padding: 12px 12px 10px;
border-bottom: 1px solid var(--border);
flex: none;
}
@@ -223,12 +223,12 @@ function fmtTime(s: string): string {
.memo-item-time { font-size: 11px; margin-top: 2px; }
.memo-editor {
padding: 16px;
padding: 16px 18px;
min-height: 420px;
display: flex;
flex-direction: column;
}
.title-input { font-size: 14px; font-weight: 500; margin-bottom: 10px; }
.title-input { font-size: 15px; font-weight: 600; margin-bottom: 10px; }
.content-textarea { flex: 1; min-height: 340px; line-height: 1.8; }
.editor-foot {
display: flex;
+95 -78
View File
@@ -108,12 +108,13 @@ const summary = computed(() => {
const done = items.value.filter(p => p.status === 'done').length
const plannedMin = items.value.reduce((acc, p) => acc + spanMinutes(p), 0)
const actualMin = items.value.reduce((acc, p) => acc + (p.actual_minutes || 0), 0)
let liveExtra = 0
const runningId = running.value
if (runningId) {
const rp = items.value.find(p => p.id === runningId)
if (rp) actualMin + elapsedMinutes(rp)
if (rp) liveExtra = liveElapsed(rp)
}
return { total, done, plannedMin, actualMin }
return { total, done, plannedMin, actualMin: actualMin + liveExtra }
})
function spanMinutes(p: PlanItem): number {
const [sh, sm] = p.start_time.split(':').map(Number)
@@ -211,62 +212,65 @@ function weekday(s: string): string {
return wdNames[parseDateStr(s).getDay()]
}
</script>
<template>
<div class="plan-page">
<div class="card sec plan-head">
<div class="nav-row">
<button class="icon-btn" @click="prevDay"><Icon name="chevron-left" :size="16" /></button>
<div class="date-box">
<span class="date-main num">{{ date.slice(5) }} {{ weekday(date) }}</span>
<span class="muted date-sub num">{{ date }}</span>
<!-- 长期计划 -->
<div class="blk-head">
<div class="blk-head-text">
<div class="blk-title">长期计划</div>
<div class="muted blk-sub">跨越多天的目标,自动落实到每一天</div>
</div>
<button class="btn btn-sm" @click="openGoalCreate"><Icon name="plus" :size="14" /> 新建长期计划</button>
</div>
<div v-if="!goals.length" class="card empty muted">
暂无长期计划。创建一个跨越多天的目标(如「产品文档学习」),系统会自动生成每天的打卡任务。
</div>
<div v-for="g in goals" :key="g.id" class="card goal-card">
<div class="goal-main">
<div class="goal-line">
<span class="tag">{{ g.kind }}</span>
<span class="goal-name">{{ g.title }}</span>
<span class="muted num goal-range">{{ g.start_date.slice(5) }} ~ {{ g.end_date.slice(5) }}</span>
</div>
<button class="icon-btn" @click="nextDay"><Icon name="chevron-right" :size="16" /></button>
<div class="goal-bar">
<span
class="goal-bar-fill"
:style="{ width: (goalProg[String(g.id)] ? Math.round(goalProg[String(g.id)].done / Math.max(1, goalProg[String(g.id)].total) * 100) : 0) + '%' }"
></span>
</div>
<div class="muted num goal-meta">
{{ goalProg[String(g.id)] ? goalProg[String(g.id)].done + '/' + goalProg[String(g.id)].total + ' 天已完成' : '0/' + (goalProg[String(g.id)] ? goalProg[String(g.id)].total : 0) + ' 天' }}
<template v-if="g.note"> · {{ g.note }}</template>
</div>
</div>
<div class="goal-ops">
<button class="icon-btn" title="编辑" @click="openGoalEdit(g)"><Icon name="edit" :size="14" /></button>
<button class="icon-btn" title="删除" @click="delGoal = g"><Icon name="close" :size="14" /></button>
</div>
</div>
<!-- 每日安排 -->
<div class="blk-head day-head">
<div class="blk-head-text">
<div class="blk-title">每日安排</div>
<div class="muted blk-sub num">
共 {{ summary.total }} 项 · 完成 {{ summary.done }} 项 · 计划 {{ fmtMin(summary.plannedMin) }} · 实际投入 {{ fmtMin(summary.actualMin) }}
<template v-if="running"> · <span class="timer-live">计时中</span></template>
</div>
</div>
<div class="day-ops">
<button class="icon-btn" title="前一天" @click="prevDay"><Icon name="chevron-left" :size="16" /></button>
<span class="date-chip num" :class="{ today: isToday }">{{ date.slice(5) }} {{ weekday(date) }}</span>
<button class="icon-btn" title="后一天" @click="nextDay"><Icon name="chevron-right" :size="16" /></button>
<button class="btn btn-sm" :class="{ 'btn-primary': !isToday }" @click="goToday">今天</button>
<span class="flex1"></span>
<button class="btn btn-primary btn-sm" @click="openCreate"><Icon name="plus" :size="14" /> 添加计划</button>
</div>
<div class="muted sum-line">
共 {{ summary.total }} 项 · 完成 {{ summary.done }} 项 · 计划 {{ fmtMin(summary.plannedMin) }} · 实际投入 {{ fmtMin(summary.actualMin) }}
<template v-if="running"> · <span class="timer-live">计时中</span></template>
</div>
</div>
<div class="goals-sec">
<div class="goals-head">
<span class="goals-title">长期计划</span>
<button class="btn btn-sm" @click="openGoalCreate"><Icon name="plus" :size="14" /> 新建长期计划</button>
</div>
<div v-if="!goals.length" class="muted goals-empty">暂无长期计划。可以创建一个跨越多天的目标(如「产品文档学习」),系统会自动生成每天的打卡任务。</div>
<div v-for="g in goals" :key="g.id" class="card sec goal-card">
<div class="goal-main">
<div class="goal-line">
<span class="tag">{{ g.kind }}</span>
<span class="goal-name">{{ g.title }}</span>
<span class="muted num goal-range">{{ g.start_date.slice(5) }} ~ {{ g.end_date.slice(5) }}</span>
</div>
<div class="goal-bar">
<span class="goal-bar-fill" :style="{ width: (goalProg[String(g.id)] ? Math.round(goalProg[String(g.id)].done / Math.max(1, goalProg[String(g.id)].total) * 100) : 0) + '%' }"></span>
</div>
<div class="muted num goal-meta">
{{ goalProg[String(g.id)] ? goalProg[String(g.id)].done + '/' + goalProg[String(g.id)].total + ' 天已完成' : '0/' + (goalProg[String(g.id)] ? goalProg[String(g.id)].total : 0) + ' 天' }}
<template v-if="g.note"> · {{ g.note }}</template>
</div>
</div>
<div class="goal-ops">
<button class="icon-btn" title="编辑" @click="openGoalEdit(g)"><Icon name="edit" :size="14" /></button>
<button class="icon-btn" title="删除" @click="delGoal = g"><Icon name="close" :size="14" /></button>
</div>
</div>
</div>
<div class="plan-list">
<div v-if="!loading && !items.length" class="card sec empty muted">
今天还没有计划,点右上角「添加计划」开始规划:学习、健身、自律……
</div>
<div class="card list-card">
<div v-if="!loading && !items.length" class="empty muted">今天还没有计划,点「添加计划」开始规划。</div>
<div
v-for="p in items" :key="p.id"
class="card sec plan-item"
class="plan-item"
:class="{ done: p.status === 'done', skip: p.status === 'skip', running: !!p.timer_started_at }"
>
<div class="pi-time num">
@@ -304,7 +308,7 @@ function weekday(s: string): string {
</div>
<teleport to="body">
<div v-if="dialogOpen" class="fixed-mask" @click.self="dialogOpen = false">
<div v-if="dialogOpen" class="mask" @click.self="dialogOpen = false">
<div class="dialog" role="dialog" aria-label="计划">
<div class="dialog-head">
<span class="dialog-title">{{ editingId ? '编辑计划' : '添加计划' }}</span>
@@ -345,7 +349,7 @@ function weekday(s: string): string {
</div>
</teleport>
<teleport to="body">
<div v-if="goalOpen" class="fixed-mask" @click.self="goalOpen = false">
<div v-if="goalOpen" class="mask" @click.self="goalOpen = false">
<div class="dialog" role="dialog" aria-label="长期计划">
<div class="dialog-head">
<span class="dialog-title">{{ goalEditing ? '编辑长期计划' : '新建长期计划' }}</span>
@@ -397,52 +401,65 @@ function weekday(s: string): string {
/>
</div>
</template>
<style scoped>
.plan-page { display: flex; flex-direction: column; gap: 10px; max-width: 760px; }
.plan-head { padding: 12px 16px; }
.nav-row { display: flex; align-items: center; gap: 8px; }
.date-box { min-width: 150px; text-align: center; }
.date-main { font-weight: 600; font-size: 14px; }
.date-sub { font-size: 11px; margin-left: 6px; }
.sum-line { font-size: 12px; margin-top: 8px; }
.timer-live { color: var(--accent); font-weight: 600; }
.flex1 { flex: 1; }
.plan-page { display: flex; flex-direction: column; }
.plan-item { display: flex; align-items: flex-start; gap: 12px; padding: 12px 14px; }
.plan-item.done { opacity: 0.62; }
.plan-item.skip { opacity: 0.5; }
.plan-item.running { border-left: 3px solid var(--accent); }
.blk-head {
display: flex; align-items: flex-start; justify-content: space-between;
gap: 12px; flex-wrap: wrap;
margin: 4px 2px 10px;
}
.blk-head-text { min-width: 0; }
.blk-title { font-size: 14px; font-weight: 600; }
.blk-sub { font-size: 12px; margin-top: 1px; }
.day-head { margin-top: 18px; }
.day-ops { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.date-chip { min-width: 74px; text-align: center; font-weight: 600; font-size: 13px; }
.date-chip.today { color: var(--accent); }
.timer-live { color: var(--accent); font-weight: 600; }
.goal-card { display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px; margin-bottom: 10px; }
.goal-main { flex: 1; min-width: 0; }
.goal-line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.goal-name { font-size: 14px; font-weight: 500; word-break: break-all; }
.goal-range { font-size: 12px; }
.goal-bar { height: 6px; border-radius: 3px; background: var(--bg-subtle); overflow: hidden; margin-top: 10px; }
.goal-bar-fill { display: block; height: 100%; background: var(--accent); border-radius: 3px; }
.goal-meta { font-size: 12px; margin-top: 6px; }
.goal-ops { display: flex; gap: 4px; }
.list-card { overflow: hidden; }
.plan-item {
display: flex; align-items: flex-start; gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
.plan-item:last-child { border-bottom: none; }
.plan-item.done { opacity: 0.6; }
.plan-item.skip { opacity: 0.48; }
.plan-item.running { box-shadow: inset 3px 0 0 var(--accent); }
.pi-time { font-size: 13px; font-weight: 600; min-width: 92px; padding-top: 2px; }
.pi-main { flex: 1; min-width: 0; }
.pi-title { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.pi-text { font-size: 14px; font-weight: 500; word-break: break-all; }
.pi-text.strike { text-decoration: line-through; }
.state-done { color: var(--ok, #2e9e5b); font-size: 12px; }
.state-done { color: var(--ok); font-size: 12px; }
.state-skip { color: var(--text-3); font-size: 12px; }
.pi-note { font-size: 12px; margin-top: 3px; }
.pi-meta { font-size: 12px; margin-top: 3px; }
.pi-ops { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.timer-on { color: var(--accent); border-color: var(--accent); }
.empty { text-align: center; padding: 26px 16px; }
.empty { padding: 28px 16px; }
.fixed-mask {
position: fixed; inset: 0; z-index: 80;
background: rgba(29, 33, 41, 0.35);
display: flex; align-items: center; justify-content: center; padding: 16px;
}
.dialog { background: var(--bg-page); border-radius: 12px; width: 420px; max-width: 100%; }
.dialog-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px 0; }
.dialog-title { font-weight: 600; font-size: 14px; }
.dialog-body { padding: 6px 16px 4px; }
.dialog-btns { display: flex; justify-content: flex-end; gap: 10px; padding: 12px 16px 16px; }
.dialog { width: 420px; }
.form-row { display: flex; flex-direction: column; gap: 5px; margin-top: 10px; }
.form-row label { font-size: 12px; color: var(--text-2); }
.form-row.two { flex-direction: row; gap: 10px; }
.form-row.two > div { flex: 1; display: flex; flex-direction: column; gap: 5px; }
.goal-hint { font-size: 12px; margin-top: 8px; }
@media (max-width: 720px) {
.plan-item { flex-wrap: wrap; }
.pi-ops { width: 100%; justify-content: flex-start; }
.day-ops { width: 100%; }
}
</style>
+3 -14
View File
@@ -210,21 +210,10 @@ function undoPolish() {
<style scoped>
.report-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }
.ai-error { color: var(--danger); font-size: 12px; margin: 0 0 8px; }
.seg { display: inline-flex; border: 1px solid var(--border-strong); border-radius: 6px; overflow: hidden; }
.seg-btn {
height: 28px; padding: 0 12px; border: none;
background: var(--bg-page); color: var(--text-2);
cursor: pointer; font-size: 12px;
border-right: 1px solid var(--border);
}
.seg-btn:last-child { border-right: none; }
.seg-btn:hover { background: var(--bg-hover); }
.seg-btn.active { background: var(--accent-bg); color: var(--accent); }
.w-date { width: 140px; }
.w-date { width: 140px; flex: none; }
.report-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 12px; align-items: start; }
.section { padding: 14px 16px; }
.section-title { font-weight: 600; margin-bottom: 8px; font-size: 13px; }
.section-title { margin-bottom: 10px; }
.section + .section { margin-top: 12px; }
.note-label { display: block; font-size: 12px; margin: 8px 0 4px; }
@@ -232,7 +221,7 @@ function undoPolish() {
.md-editor { min-height: 420px; line-height: 1.7; }
.sum-row { display: flex; justify-content: space-between; padding: 3px 0; font-size: 12px; }
.section-sub { font-weight: 600; margin: 10px 0 4px; font-size: 12px; }
.section-sub { font-weight: 600; margin: 12px 0 4px; font-size: 12px; color: var(--text-2); }
.sum-item { font-size: 12px; padding: 2px 0; word-break: break-all; }
.sum-empty { font-size: 12px; }
+7 -12
View File
@@ -7,7 +7,7 @@ import {
listAttendanceBetween, listLogItemsBetween, listProjects,
renameProject, saveLocationPresets, setProjectArchived, setSetting, type WorkRule
} from '../db/repo'
import { type Project } from '../db/schema'
import { categoryLabel, type Project } from '../db/schema'
import { getLogCategories, setLogCategories, getLedgerCategories, setLedgerCategories } from '../db/categories'
import { downloadText, } from '../lib/download'
import { downloadCsv } from '../lib/csv'
@@ -494,18 +494,16 @@ async function doImport() {
<style scoped>
.settings { display: flex; flex-direction: column; gap: 12px; max-width: 640px; }
.acc-line { font-size: 12px; margin-bottom: 10px; line-height: 1.7; }
.section { padding: 14px 16px; }
.section-title { font-weight: 600; margin-bottom: 10px; font-size: 13px; }
.rule-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 16px; }
.rule-item label { display: block; font-size: 12px; color: var(--text-2); margin-bottom: 4px; }
.rule-save { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
.rule-save { display: flex; align-items: center; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
.proj-add { display: flex; gap: 8px; }
.proj-tip { font-size: 12px; margin-top: 6px; }
.proj-row {
display: flex; align-items: center; gap: 8px;
padding: 6px 0; border-bottom: 1px solid var(--border);
padding: 7px 0; border-bottom: 1px solid var(--border);
}
.proj-row:last-child { border-bottom: none; }
.proj-name { flex: 1; word-break: break-all; }
@@ -513,22 +511,19 @@ async function doImport() {
.loc-note { font-size: 12px; }
.ai-grid { display: flex; flex-direction: column; gap: 10px; }
.backup-row { display: flex; gap: 8px; }
.cat-block { margin-top: 10px; }
.backup-row { display: flex; gap: 8px; flex-wrap: wrap; }
.cat-block { margin-top: 12px; }
.cat-block:first-of-type { margin-top: 0; }
.cat-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; font-size: 12px; }
.cat-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.cat-chip {
display: inline-flex; align-items: center; gap: 4px;
border: 1px solid var(--border); border-radius: 999px;
padding: 2px 8px; font-size: 12px;
background: var(--bg-subtle);
}
.chip-btn { border: none; background: transparent; color: var(--text-3); cursor: pointer; font-size: 11px; padding: 0 1px; }
.chip-btn:hover { color: var(--accent); }
.exp-dates { display: flex; align-items: center; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.exp-dates .input { width: 150px; }
.seg { display: inline-flex; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; }
.seg-btn { border: none; background: var(--bg-page); color: var(--text-2); cursor: pointer; font-size: 12px; padding: 5px 12px; }
.seg-btn.active { background: var(--accent-bg); color: var(--accent); }
.import-error { color: var(--danger); font-size: 12px; margin-top: 8px; }
.backup-note { font-size: 12px; margin-top: 10px; }
</style>
+11 -15
View File
@@ -180,32 +180,28 @@ function heatTitle(date: string, count: number): string {
<style scoped>
.cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.stat-card { padding: 14px 16px; }
.stat-num { font-size: 20px; font-weight: 600; margin-top: 4px; }
.section { padding: 14px 16px; margin-top: 12px; }
.section-title { font-weight: 600; margin-bottom: 10px; font-size: 13px; }
.section { margin-top: 12px; }
.heat { display: flex; gap: 3px; overflow-x: auto; padding-bottom: 4px; }
.heat-col { display: flex; flex-direction: column; gap: 3px; }
.heat-cell { width: 12px; height: 12px; border-radius: 2px; display: inline-block; }
.heat-cell { width: 12px; height: 12px; border-radius: 3px; display: inline-block; }
.lv0 { background: var(--bg-subtle); border: 1px solid var(--border); }
.lv1 { background: #D8ECDF; }
.lv2 { background: #A9D8BC; }
.lv3 { background: #6FBF92; }
.lv4 { background: var(--ok); }
.heat-legend { display: flex; align-items: center; gap: 3px; margin-top: 8px; font-size: 12px; }
.lv1 { background: #E4EEFF; }
.lv2 { background: #BFD6FF; }
.lv3 { background: #7FA8F5; }
.lv4 { background: var(--accent); }
.heat-legend { display: flex; align-items: center; gap: 3px; margin-top: 10px; font-size: 12px; }
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.bar-row { display: flex; align-items: center; gap: 10px; margin: 6px 0; }
.bar-label { width: 72px; flex: none; color: var(--text-2); font-size: 12px; text-align: right; }
.bar-track { flex: 1; height: 8px; border-radius: 4px; background: var(--bg-subtle); overflow: hidden; }
.bar-fill { display: block; height: 100%; background: var(--accent); border-radius: 4px; }
.bar-num { font-size: 12px; width: 92px; flex: none; }
.two-col .section { margin-top: 0; }
.bar-label { width: 72px; }
.bar-num { width: 92px; }
.loc-label { width: 110px; text-align: left; }
@media (max-width: 720px) {
.cards { grid-template-columns: repeat(2, 1fr); }
.two-col { grid-template-columns: 1fr; }
.two-col .section { margin-top: 12px; }
}
</style>
+32 -37
View File
@@ -88,58 +88,53 @@ function onDateInput(e: Event) {
if (v) date.value = v
}
</script>
<template>
<div class="detail-page">
<div class="detail-toolbar">
<button class="icon-btn" title="前一天" @click="prevDay"><Icon name="chevron-left" :size="16" /></button>
<span class="date-title num" :class="{ 'is-today': isToday }">{{ formatDateCN(date) }}</span>
<button class="icon-btn" title="后一天" @click="nextDay"><Icon name="chevron-right" :size="16" /></button>
<input type="date" class="input w-date" :value="date" @change="onDateInput" />
<button class="btn btn-sm" :disabled="isToday" @click="goToday">回到今天</button>
</div>
<div class="export-row">
<span class="muted exp-label">导出工作详情</span>
<div class="seg">
<button class="seg-btn" :class="{ active: expRange === 'day' }" @click="expRange = 'day'">当日</button>
<button class="seg-btn" :class="{ active: expRange === 'week' }" @click="expRange = 'week'">当周</button>
<button class="seg-btn" :class="{ active: expRange === 'month' }" @click="expRange = 'month'">当月</button>
<button class="seg-btn" :class="{ active: expRange === 'custom' }" @click="expRange = 'custom'">自定义</button>
<div class="card toolbar">
<div class="nav-row">
<button class="icon-btn" title="前一天" @click="prevDay"><Icon name="chevron-left" :size="16" /></button>
<span class="date-title num" :class="{ 'is-today': isToday }">{{ formatDateCN(date) }}</span>
<button class="icon-btn" title="后一天" @click="nextDay"><Icon name="chevron-right" :size="16" /></button>
<input type="date" class="input w-date" :value="date" @change="onDateInput" />
<button class="btn btn-sm" :disabled="isToday" @click="goToday">回到今天</button>
</div>
<div class="tool-sep"></div>
<div class="exp-row">
<span class="muted exp-label">导出</span>
<div class="seg">
<button class="seg-btn" :class="{ active: expRange === 'day' }" @click="expRange = 'day'">当日</button>
<button class="seg-btn" :class="{ active: expRange === 'week' }" @click="expRange = 'week'">当周</button>
<button class="seg-btn" :class="{ active: expRange === 'month' }" @click="expRange = 'month'">当月</button>
<button class="seg-btn" :class="{ active: expRange === 'custom' }" @click="expRange = 'custom'">自定义</button>
</div>
<input v-if="expRange !== 'day'" class="input w-exp" type="date" v-model="expStart" />
<input v-if="expRange === 'custom'" class="input w-exp" type="date" v-model="expEnd" />
<button class="btn btn-sm" :disabled="exporting" @click="doExportMd">导出 Markdown</button>
</div>
<input v-if="expRange !== 'day'" class="input w-exp" type="date" v-model="expStart" />
<input v-if="expRange === 'custom'" class="input w-exp" type="date" v-model="expEnd" />
<button class="btn btn-sm" :disabled="exporting" @click="doExportMd">导出 Markdown</button>
</div>
<DayPanel :date="date" />
</div>
</template>
<style scoped>
.detail-toolbar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
.detail-page { display: flex; flex-direction: column; }
.toolbar {
padding: 10px 16px;
display: flex; align-items: center; gap: 14px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.nav-row { display: flex; align-items: center; gap: 6px; }
.date-title { font-size: 14px; font-weight: 600; min-width: 170px; text-align: center; }
.date-title.is-today { color: var(--accent); }
.w-date { width: 150px; }
.export-row {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
margin-bottom: 12px; padding: 8px 12px;
border: 1px solid var(--border); border-radius: var(--radius-md);
background: var(--bg-page);
}
.w-date { width: 150px; flex: none; }
.tool-sep { width: 1px; align-self: stretch; background: var(--border); }
.exp-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.exp-label { font-size: 12px; }
.seg { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
.seg-btn { border: none; background: var(--bg-page); color: var(--text-2); cursor: pointer; font-size: 12px; padding: 4px 10px; }
.seg-btn.active { background: var(--accent-bg); color: var(--accent); }
.w-exp { width: 140px; }
.w-exp { width: 140px; flex: none; }
@media (max-width: 720px) {
.detail-toolbar { gap: 8px; }
.tool-sep { display: none; }
.nav-row { flex: 1; }
.date-title { min-width: 0; flex: 1; text-align: left; }
.w-date { flex: 1; min-width: 120px; width: auto; }
}
+27 -45
View File
@@ -150,24 +150,20 @@ async function doDelete() {
onMounted(load)
</script>
<template>
<div class="admin-page">
<div class="card sec">
<div class="card table-card">
<div class="toolbar">
<div class="head-wrap">
<div class="sec-title">用户管理</div>
<div class="muted head-sub">云端账号统一管理 · 管理员:xionglin</div>
<div class="tb-title">用户管理</div>
<div class="muted tb-sub">云端账号统一管理 · 管理员:xionglin</div>
</div>
<button class="btn btn-primary btn-sm" @click="openCreate">
<Icon name="plus" :size="14" />
新增用户
</button>
</div>
<div v-if="error" class="form-error">{{ error }}</div>
</div>
<div class="card table-card admin-table-card">
<div v-if="error" class="form-error tb-error">{{ error }}</div>
<table class="admin-table">
<thead>
<tr>
@@ -202,8 +198,8 @@ onMounted(load)
<!-- 新增用户弹窗 -->
<teleport to="body">
<div v-if="createOpen" class="fixed-mask" @click.self="createOpen = false">
<div class="dialog create-dialog" role="dialog" aria-label="新增用户">
<div v-if="createOpen" class="mask" @click.self="createOpen = false">
<div class="dialog" role="dialog" aria-label="新增用户">
<div class="dialog-head">
<span class="dialog-title">新增用户</span>
<button class="icon-btn" @click="createOpen = false"><Icon name="close" :size="16" /></button>
@@ -233,8 +229,8 @@ onMounted(load)
<!-- 重置密码弹窗 -->
<teleport to="body">
<div v-if="resetTarget" class="fixed-mask" @click.self="resetTarget = null">
<div class="dialog create-dialog" role="dialog" aria-label="重置密码">
<div v-if="resetTarget" class="mask" @click.self="resetTarget = null">
<div class="dialog" role="dialog" aria-label="重置密码">
<div class="dialog-head">
<span class="dialog-title">重置「{{ resetTarget.username }}」的登录密码</span>
<button class="icon-btn" @click="resetTarget = null"><Icon name="close" :size="16" /></button>
@@ -264,60 +260,46 @@ onMounted(load)
/>
</div>
</template>
<style scoped>
.admin-page { display: flex; flex-direction: column; gap: 12px; max-width: 860px; }
.toolbar { display: flex; align-items: center; gap: 12px; }
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
flex-wrap: wrap;
}
.head-wrap { flex: 1; min-width: 0; }
.sec-title { font-weight: 600; font-size: 13px; }
.head-sub { font-size: 12px; margin-top: 2px; }
.sec { padding: 14px 16px; }
.tb-title { font-weight: 600; font-size: 14px; }
.tb-sub { font-size: 12px; margin-top: 2px; }
.tb-error { padding: 8px 16px 0; }
.admin-table-card { overflow-x: auto; }
.admin-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.admin-table th { text-align: left; font-weight: 500; color: var(--text-3); font-size: 12px; padding: 8px 12px; border-bottom: 1px solid var(--border); }
.admin-table td { padding: 9px 12px; border-bottom: 1px solid var(--border); white-space: nowrap; }
.admin-table td:last-child { border-bottom: none; }
.table-card { overflow-x: auto; }
.admin-table td { white-space: nowrap; }
.td-name { font-weight: 500; }
.role-badge {
display: inline-flex;
align-items: center;
border: 1px solid var(--accent);
background: var(--accent-bg);
color: var(--accent);
border-radius: 999px;
font-size: 12px;
line-height: 16px;
padding: 1px 8px;
line-height: 18px;
padding: 1px 9px;
}
.load-line { padding: 12px; font-size: 12px; }
.load-line { padding: 12px 16px; font-size: 12px; }
.admin-note { font-size: 12px; padding: 0 2px; }
.ops { display: inline-flex; gap: 6px; }
.btn-danger { color: var(--danger); }
.form-row { display: flex; flex-direction: column; gap: 5px; margin-top: 10px; }
.form-row label { font-size: 12px; color: var(--text-2); }
.fixed-mask {
position: fixed;
inset: 0;
z-index: 80;
background: rgba(29, 33, 41, 0.35);
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.dialog { background: var(--bg-page); border-radius: 12px; width: 420px; max-width: 100%; }
.dialog-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px 0; }
.dialog-title { font-weight: 600; font-size: 14px; }
.dialog-body { padding: 6px 16px 4px; }
.dialog-btns { display: flex; justify-content: flex-end; gap: 10px; padding: 12px 16px 16px; }
.btn-danger { color: var(--danger); }
.dialog { width: 420px; }
.empty { text-align: center; }
@media (max-width: 720px) {
.toolbar { flex-wrap: wrap; row-gap: 8px; }
.admin-table td, .admin-table th { white-space: normal; }
}
</style>
+6 -56
View File
@@ -284,7 +284,7 @@ async function doChangePwd() {
</tbody>
</table>
</div>
<div class="muted vault-note num">共 {{ rows.length }} 条账号记录 · 密码以 AES-256-GCM 加密,服务器仅存密文</div>
<div class="muted table-note num">共 {{ rows.length }} 条账号记录 · 密码以 AES-256-GCM 加密,服务器仅存密文</div>
</template>
<CredentialDialog
@@ -356,18 +356,15 @@ async function doChangePwd() {
height: 52px;
margin: 0 auto;
border-radius: 50%;
background: var(--bg-subtle);
border: 1px solid var(--border);
background: var(--accent-bg);
color: var(--accent);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-2);
}
.lock-title { font-size: 14px; font-weight: 600; }
.lock-title { font-size: 15px; font-weight: 600; }
.lock-tip { font-size: 12px; line-height: 1.7; }
.lock-btn { margin-top: 4px; }
.form-error { color: var(--danger); font-size: 12px; }
.empty { text-align: center; padding: 24px 0; }
.vault-toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
.search-wrap { position: relative; width: 280px; max-width: 100%; }
@@ -375,19 +372,6 @@ async function doChangePwd() {
.search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--text-3); }
.table-card { overflow-x: auto; }
.cred-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.cred-table th {
text-align: left;
font-weight: 500;
color: var(--text-2);
background: var(--bg-subtle);
padding: 10px 12px;
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
.cred-table td { padding: 10px 12px; border-bottom: 1px solid var(--border); vertical-align: top; word-break: break-all; }
.cred-table tbody tr:hover { background: var(--bg-subtle); }
.cred-table tbody tr:last-child td { border-bottom: none; }
.td-system { font-weight: 500; }
.th-ops { width: 150px; }
.pwd-cell { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
@@ -395,43 +379,9 @@ async function doChangePwd() {
.copyable { cursor: pointer; }
.copyable:hover { color: var(--accent); }
.ops { display: flex; gap: 4px; flex-wrap: wrap; }
.vault-note { margin-top: 10px; font-size: 12px; text-align: right; }
.table-note { margin-top: 10px; font-size: 12px; text-align: right; }
.mask {
position: fixed;
inset: 0;
background: rgba(29, 33, 41, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 90;
}
.dialog {
width: 440px;
max-width: calc(100vw - 32px);
background: var(--bg-page);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-float);
}
.dialog-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 12px;
border-bottom: 1px solid var(--border);
}
.dialog-title { font-size: 14px; font-weight: 600; }
.dialog-body { padding: 16px 20px; }
.dialog-btns {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 20px 16px;
border-top: 1px solid var(--border);
}
.dialog { width: 440px; }
.vp-field { margin-bottom: 10px; }
.vp-field label { display: block; font-size: 12px; color: var(--text-2); margin-bottom: 4px; }
.vp-tip { font-size: 12px; margin-top: 8px; }
.modal-enter-active, .modal-leave-active { transition: opacity 0.15s ease; }
.modal-enter-from, .modal-leave-to { opacity: 0; }
</style>