ui: 全站界面重设计(文字侧栏+灰底白卡层次+组件统一) fix: 添加记录弹窗不显示/计时汇总崩溃/CSV导出崩溃/记账切换类型崩溃
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# v0.6.0 发布说明(2026-10-26)
|
||||
|
||||
## 界面整体重设计
|
||||
|
||||
本版本对全部界面做了系统性改版,统一为「高级白」企业级视觉,信息层次更清晰:
|
||||
|
||||
- **桌面侧栏重做**:由纯图标改为「图标 + 文字」导航,按 记录 / 工具 / 分析 / 系统 分组;新增品牌区与当前用户区(头像、用户名、退出)。
|
||||
- **顶栏精简**:只保留页面标题、全局搜索与打卡按钮;用户信息与退出移入侧栏底部。
|
||||
- **内容层次**:内容区改为浅灰底 + 白色卡片,卡片带细边框与轻投影,主次分明。
|
||||
- **组件统一**:按钮、输入框、表格、弹窗、分段选择器、进度条等全部收敛为全局统一样式,各页面不再各自为政。
|
||||
- **逐页重排**:日历(网格线 + 选中描边)、详情(导航与导出合并为一条工具栏)、计划(长期计划与每日安排分区块、列表合并为单卡)、记账、统计(热力图改品牌蓝)、总结、备忘录、密码库、设置、用户管理。
|
||||
- **登录页升级**:品牌标识、卡片投影。
|
||||
- **平板适配**:721–1000px 侧栏自动折叠为图标栏。
|
||||
- **移动端**:底部标签栏与「更多」弹层沿用,并随新基座精修间距与弹窗。
|
||||
|
||||
## 问题修复
|
||||
|
||||
- 修复:详情页「添加记录」按钮点击无反应(弹窗开关与编辑目标状态混用所致)。
|
||||
- 修复:计划页有任务正在计时时,汇总行会触发未定义函数导致整页报错。
|
||||
- 修复:设置页导出工作记录 CSV 缺少分类标签导入,触发即报错。
|
||||
- 修复:记账弹窗切换收支类型时引用了不存在的分类常量,触发即报错。
|
||||
- 记录弹窗标题与按钮文案随「新增 / 编辑」模式正确切换。
|
||||
|
||||
## 升级与部署
|
||||
|
||||
- 桌面端:安装 InternLog-Setup-0.6.0.exe。
|
||||
- 安卓端:安装 InternLog-0.6.0-debug.apk。
|
||||
- 服务器:`cd /opt/daywork && git pull && cd server && docker compose up -d --build`(网页端 bundle 随仓库更新)。
|
||||
- 数据结构无变更,v0.5.0 及之前版本可直接升级。
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./web-e9jKyg3_.js","./index-DLDMtYxl.js","./index-ieoBMDLY.css"])))=>i.map(i=>d[i]);
|
||||
import{r as s,_ as i}from"./index-DLDMtYxl.js";var t;(function(e){e.Documents="DOCUMENTS",e.Data="DATA",e.Library="LIBRARY",e.Cache="CACHE",e.External="EXTERNAL",e.ExternalStorage="EXTERNAL_STORAGE"})(t||(t={}));var a;(function(e){e.UTF8="utf8",e.ASCII="ascii",e.UTF16="utf16"})(a||(a={}));const E=s("Filesystem",{web:()=>i(()=>import("./web-e9jKyg3_.js"),__vite__mapDeps([0,1,2]),import.meta.url).then(e=>new e.FilesystemWeb)});export{t as Directory,a as Encoding,E as Filesystem};
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./web-BbcS1QA8.js","./index-CtHE6m7N.js","./index-C8-KlPfD.css"])))=>i.map(i=>d[i]);
|
||||
import{r as s,_ as i}from"./index-CtHE6m7N.js";var t;(function(e){e.Documents="DOCUMENTS",e.Data="DATA",e.Library="LIBRARY",e.Cache="CACHE",e.External="EXTERNAL",e.ExternalStorage="EXTERNAL_STORAGE"})(t||(t={}));var a;(function(e){e.UTF8="utf8",e.ASCII="ascii",e.UTF16="utf16"})(a||(a={}));const E=s("Filesystem",{web:()=>i(()=>import("./web-BbcS1QA8.js"),__vite__mapDeps([0,1,2]),import.meta.url).then(e=>new e.FilesystemWeb)});export{t as Directory,a as Encoding,E as Filesystem};
|
||||
+1
File diff suppressed because one or more lines are too long
+27
File diff suppressed because one or more lines are too long
-27
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -8,8 +8,8 @@
|
||||
<link rel="icon" href="./icons/icon.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="./icons/icon-192.png" />
|
||||
<title>实习工作日志</title>
|
||||
<script type="module" crossorigin src="./assets/index-DLDMtYxl.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-ieoBMDLY.css">
|
||||
<script type="module" crossorigin src="./assets/index-CtHE6m7N.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-C8-KlPfD.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "intern-log",
|
||||
"private": true,
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
+132
-90
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare const __APP_VERSION__: string
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user