ui: 手机端布局重设计——底部带文字标签栏(5个主入口)+更多弹层、全屏搜索、打卡日历底部抽屉、弹窗贴底、记账卡片行、密码库横滑表格

This commit is contained in:
root
2026-10-16 16:30:45 +08:00
parent e5f99d46d8
commit cfc8e3c734
12 changed files with 390 additions and 71 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./web-DCXp8Rvi.js","./index-BztxiTek.js","./index-CkA9n_Ns.css"])))=>i.map(i=>d[i]);
import{r as s,_ as i}from"./index-BztxiTek.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-DCXp8Rvi.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-XtLEyZTQ.js","./index-BMAz2lN3.js","./index-DUPSAEmu.css"])))=>i.map(i=>d[i]);
import{r as s,_ as i}from"./index-BMAz2lN3.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-XtLEyZTQ.js"),__vite__mapDeps([0,1,2]),import.meta.url).then(e=>new e.FilesystemWeb)});export{t as Directory,a as Encoding,E as Filesystem};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -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-BztxiTek.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-CkA9n_Ns.css">
<script type="module" crossorigin src="./assets/index-BMAz2lN3.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DUPSAEmu.css">
</head>
<body>
<div id="app"></div>
+261 -30
View File
@@ -42,6 +42,8 @@ async function onLogin(s: Session) {
}
}
async function doLogout() {
mobileSearch.value = false
moreOpen.value = false
stopRealtime()
if (syncTimer !== null) { window.clearInterval(syncTimer); syncTimer = null }
await logoutUser()
@@ -126,6 +128,22 @@ window.addEventListener('hashchange', () => { route.value = readRoute() })
function nav(key: string) { location.hash = '#/' + key }
const current = computed(() => VIEWS.find(v => v.key === route.value) || VIEWS[0])
// 手机端底部标签页:4 个主功能 + 「更多」弹层(统计/总结/密码库/设置/退出)
const TAB_KEYS = ['calendar', 'today', 'ledger', 'memo']
const TABS = VIEWS.filter(v => TAB_KEYS.includes(v.key))
const MORE_VIEWS = VIEWS.filter(v => !TAB_KEYS.includes(v.key))
const isMoreActive = computed(() => MORE_VIEWS.some(v => v.key === route.value))
const moreOpen = ref(false)
function navFromSheet(key: string) { moreOpen.value = false; nav(key) }
// 手机端全屏搜索
const mobileSearch = ref(false)
const msInput = ref<HTMLInputElement | null>(null)
function openMobileSearch() { mobileSearch.value = true; window.setTimeout(() => msInput.value?.focus(), 60) }
function pickLog(item: LogItem & { date: string }) { openLog(item); mobileSearch.value = false }
function pickMemo(m: Memo) { openMemo(m); mobileSearch.value = false }
// ---- 工具栏打卡 ----
const today = ref(todayStr())
const att = ref<Attendance | undefined>(undefined)
@@ -212,6 +230,9 @@ onMounted(async () => {
<div class="main">
<header class="topbar">
<span class="title">{{ current.name }}</span>
<button class="mob-search-btn" title="搜索" @click="openMobileSearch">
<Icon name="search" :size="16" />
</button>
<div class="gsearch">
<Icon name="search" :size="14" class="gs-icon" />
<input
@@ -242,8 +263,8 @@ onMounted(async () => {
<span class="dot" style="background: var(--warn)"></span>
填写本周周记
</button>
<span class="user-chip num">{{ session?.username }}</span>
<button class="btn btn-sm" @click="doLogout">退出</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">
<Icon :name="clockDone ? 'check-circle' : 'clock'" :size="14" />
<span class="num">{{ clockLabel }}</span>
@@ -262,6 +283,72 @@ onMounted(async () => {
<SettingsView v-else />
</div>
</main>
<!-- 手机端底部标签栏 -->
<nav class="tabbar">
<button
v-for="v in TABS" :key="v.key"
class="tab-btn" :class="{ active: route === v.key }"
@click="nav(v.key)"
>
<Icon :name="v.icon" :size="20" />
<span>{{ v.name }}</span>
</button>
<button class="tab-btn" :class="{ active: isMoreActive }" @click="moreOpen = true">
<Icon name="more" :size="20" />
<span>更多</span>
</button>
</nav>
<!-- 手机端「更多」底部弹层 -->
<div v-if="moreOpen" class="sheet-mask" @click="moreOpen = false">
<div class="more-sheet" @click.stop>
<div class="sheet-user num">{{ session?.username }}</div>
<button v-for="v in MORE_VIEWS" :key="v.key" class="sheet-row" @click="navFromSheet(v.key)">
<Icon :name="v.icon" :size="18" />
<span class="sheet-name">{{ v.name }}</span>
<Icon name="chevron-right" :size="14" class="sheet-go" />
</button>
<div class="sheet-divider"></div>
<button class="sheet-row sheet-danger" @click="moreOpen = false; doLogout()">
<Icon name="close" :size="18" />
<span class="sheet-name">退出登录</span>
</button>
</div>
</div>
<!-- 手机端全屏搜索 -->
<div v-if="mobileSearch" class="msw-mask" @click.self="mobileSearch = false">
<div class="msw">
<div class="msw-bar">
<Icon name="search" :size="16" class="msw-icon" />
<input
ref="msInput" class="msw-input" v-model="searchQ"
placeholder="搜索工作记录与备忘录"
@input="onSearchInput" @keydown.esc="mobileSearch = false"
/>
<button class="icon-btn" @click="clearSearch(); mobileSearch = false">
<Icon name="close" :size="16" />
</button>
</div>
<div class="msw-results">
<template v-if="hasResults">
<div v-if="results.logs.length" class="gs-group muted">工作记录</div>
<div v-for="r in results.logs" :key="'l' + r.id" class="msw-item" @mousedown.prevent="pickLog(r)">
<span class="tag">{{ categoryLabel(r.category) }}</span>
<span class="gs-text">{{ r.content }}</span>
<span class="muted num gs-date">{{ r.date }}</span>
</div>
<div v-if="results.memos.length" class="gs-group muted">备忘录</div>
<div v-for="m in results.memos" :key="'m' + m.id" class="msw-item" @mousedown.prevent="pickMemo(m)">
<span class="gs-text">{{ m.title || '无标题' }}</span>
<span class="muted gs-sub">{{ m.content }}</span>
</div>
</template>
<div v-else-if="searchQ.trim()" class="gs-empty muted">无匹配结果</div>
</div>
</div>
</div>
</div>
<div class="toast-wrap">
<transition-group name="toast">
@@ -419,38 +506,182 @@ 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;
border: none;
background: transparent;
border-radius: var(--radius-sm);
color: var(--text-2);
cursor: pointer;
align-items: center;
justify-content: center;
}
.mobile-hide { display: initial; }
.tabbar {
display: none;
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 30;
background: var(--bg-page);
border-top: 1px solid var(--border);
padding-bottom: env(safe-area-inset-bottom);
}
.tab-btn {
flex: 1;
height: calc(52px - 6px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
border: none;
background: transparent;
color: var(--text-3);
cursor: pointer;
font-size: 11px;
}
.tab-btn.active { color: var(--accent); }
.sheet-mask {
position: fixed;
inset: 0;
z-index: 70;
background: rgba(29, 33, 41, 0.35);
}
.more-sheet {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: var(--bg-page);
border-radius: 14px 14px 0 0;
padding: 10px 14px calc(14px + env(safe-area-inset-bottom));
display: flex;
flex-direction: column;
}
.sheet-user {
font-size: 12px;
color: var(--text-3);
padding: 4px 6px 10px;
border-bottom: 1px solid var(--border);
margin-bottom: 4px;
}
.sheet-row {
display: flex;
align-items: center;
gap: 12px;
height: 46px;
border: none;
background: transparent;
color: var(--text-1);
font-size: 14px;
cursor: pointer;
border-radius: var(--radius-sm);
text-align: left;
padding: 0 6px;
}
.sheet-row:hover, .sheet-row:active { background: var(--bg-hover); }
.sheet-name { flex: 1; }
.sheet-go { color: var(--text-3); }
.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-bar {
height: 52px;
flex: none;
display: flex;
align-items: center;
gap: 8px;
padding: 0 12px;
border-bottom: 1px solid var(--border);
}
.msw-icon { color: var(--text-3); }
.msw-input {
flex: 1;
height: 34px;
border: none;
outline: none;
font-size: 14px;
background: transparent;
color: var(--text-1);
}
.msw-results {
flex: 1;
overflow: auto;
padding: 10px 4px calc(20px + env(safe-area-inset-bottom));
}
.msw-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 10px;
border-radius: var(--radius-sm);
cursor: pointer;
}
.msw-item:hover, .msw-item:active { background: var(--bg-hover); }
@media (max-width: 720px) {
.app { flex-direction: column; height: 100dvh; }
.side {
order: 2;
width: 100%;
height: calc(54px + env(safe-area-inset-bottom));
padding-bottom: env(safe-area-inset-bottom);
flex-direction: row;
justify-content: space-around;
border-right: none;
border-top: 1px solid var(--border);
padding-top: 0;
}
.main { order: 1; flex: 1; min-height: 0; }
.nav-btn { width: 44px; height: 42px; }
.nav-btn::after { display: none; }
/* 布局骨架:顶栏一行 + 内容 + 底部标签栏 */
.app { display: block; height: 100dvh; }
.side { display: none; }
.main { display: flex; flex-direction: column; height: 100dvh; min-width: 0; }
.topbar {
flex-wrap: wrap;
height: auto;
min-height: 52px;
padding: 8px 12px;
row-gap: 8px;
height: 50px;
min-height: 50px;
padding: 0 10px;
gap: 6px;
}
.topbar > .flex1 { display: none; }
.gsearch { order: 10; width: 100%; max-width: 100%; }
.gs-input { height: 34px; }
.gs-panel { top: 100%; }
.topbar-actions { margin-left: auto; }
.user-chip { max-width: 72px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.title { font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.gsearch { display: none; }
.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 .num { font-size: 12px; }
.content-inner { padding: 12px; }
.toast-wrap { top: 8px; right: 12px; left: 12px; align-items: flex-end; }
.tabbar { display: flex; }
.content { flex: 1; min-height: 0; }
.content-inner {
max-width: none;
padding: 12px 12px calc(66px + 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;
left: 10px;
align-items: flex-start;
}
}
</style>
+2 -1
View File
@@ -22,5 +22,6 @@ export const ICONS: Record<string, string> = {
settings: svg('<circle cx="12" cy="12" r="3"/><path d="M12 4.5v2.2M12 17.3v2.2M4.5 12h2.2M17.3 12h2.2M6.8 6.8l1.6 1.6M15.6 15.6l1.6 1.6M17.2 6.8l-1.6 1.6M8.4 15.6l-1.6 1.6"/>'),
memo: svg('<path d="M6.5 3.5h7.5L18.5 8v11a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1v-15a1 1 0 0 1 1-1z"/><path d="M14 3.5V8h4.5M9.5 12h5M9.5 15.5h5"/>'),
lock: svg('<rect x="5.5" y="10.5" width="13" height="9.5" rx="1.5"/><path d="M8.5 10.5V8a3.5 3.5 0 0 1 7 0v2.5"/>'),
wallet: svg('<rect x="3.5" y="6" width="17" height="12.5" rx="2"/><path d="M3.5 10h17"/><circle cx="16.4" cy="14.2" r="1.1"/>')
wallet: svg('<rect x="3.5" y="6" width="17" height="12.5" rx="2"/><path d="M3.5 10h17"/><circle cx="16.4" cy="14.2" r="1.1"/>'),
more: svg('<circle cx="5.4" cy="12" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="18.6" cy="12" r="1.7"/>')
}
+1
View File
@@ -1,6 +1,7 @@
import { createApp } from 'vue'
import App from './App.vue'
import './styles/tokens.css'
import './styles/mobile.css'
import { showFatalError } from './lib/errOverlay'
import { bootstrapSession, openUserDatabase } from './db/auth'
+86
View File
@@ -0,0 +1,86 @@
/* ============================================================
手机端(<=720px)全局适配:作用于各视图与弹窗组件
(App.vue 的壳层样式是 scoped,跨组件规则统一放这里,便于集中管理)
============================================================ */
/* 通用的白底卡片在窄屏贴边 */
@media (max-width: 720px) {
.card { border-radius: 12px; }
input.input, select.select, textarea.textarea {
width: 100%;
box-sizing: border-box;
}
.btn { touch-action: manipulation; }
/* ---------- 弹窗:窄屏一体布局,覆盖组件内固定宽度 ---------- */
.dialog {
width: calc(100vw - 20px) !important;
max-width: calc(100vw - 20px) !important;
max-height: 86vh;
max-height: 86dvh;
overflow: auto;
}
.dialog-body { padding: 14px !important; }
.form-grid { grid-template-columns: 1fr !important; }
.dialog-btns { gap: 8px; }
.dialog-btns .btn { flex: 1; }
/* ---------- 日历当日抽屉:改为底部抽屉 ---------- */
.drawer {
top: auto !important;
left: 0 !important;
width: 100% !important;
height: min(86vh, 86dvh) !important;
border-left: none !important;
border-top: 1px solid var(--border);
border-radius: 14px 14px 0 0 !important;
}
.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; }
.drawer-head { border-radius: 14px 14px 0 0; }
.drawer-body { padding: 12px !important; }
/* ---------- 记账表格:卡片行(带行内标签),手机上不横滚 ---------- */
.tx-table { min-width: 0 !important; }
.tx-table thead { display: none !important; }
.tx-table tr { display: block; padding: 10px 12px; border-bottom: 1px solid var(--border); background: transparent; }
.tx-table tbody tr:last-child { border-bottom: none; }
.tx-table td {
display: flex !important;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 3px 0 !important;
border: none !important;
white-space: normal !important;
}
.tx-table td::before {
content: attr(data-label);
color: var(--text-3);
font-size: 12px;
flex: none;
}
.tx-table td.ops-cell::before { content: none; }
.tx-table td.ops-cell { justify-content: flex-end; }
.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; }
.vault-toolbar { flex-wrap: wrap; row-gap: 8px; }
.vault-toolbar .search-wrap { width: 100%; max-width: none !important; }
.vault-toolbar .flex1 { display: none; }
.vault-toolbar .btn { flex: none; }
/* ---------- 日历格子收窄 ---------- */
.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; }
}
+6 -6
View File
@@ -293,16 +293,16 @@ async function confirmDelete() {
</thead>
<tbody>
<tr v-for="r in filtered" :key="r.id">
<td class="num">{{ r.date }}</td>
<td><span class="tag">{{ r.category }}</span></td>
<td class="num" :class="r.type === 'income' ? 'amt-income' : 'amt-expense'">{{ amountText(r) }}</td>
<td class="td-note">{{ r.note || '--' }}</td>
<td>
<td class="num" data-label="日期">{{ r.date }}</td>
<td data-label="分类"><span class="tag">{{ r.category }}</span></td>
<td class="num" data-label="金额" :class="r.type === 'income' ? 'amt-income' : 'amt-expense'">{{ amountText(r) }}</td>
<td class="td-note" data-label="备注">{{ r.note || '--' }}</td>
<td data-label="报销">
<span v-if="r.type !== 'expense' || r.reimbursable !== 1" class="muted">--</span>
<span v-else-if="r.reimbursed === 1" class="tx-done">已报销</span>
<span v-else class="tx-pending">待报销</span>
</td>
<td>
<td class="ops-cell">
<div class="ops">
<button
v-if="r.type === 'expense' && r.reimbursable === 1 && r.reimbursed !== 1"
+2 -2
View File
@@ -271,7 +271,7 @@ async function doChangePwd() {
</div>
</td>
<td class="td-remark">{{ row.remark || '--' }}</td>
<td>
<td class="td-ops">
<div class="ops">
<button class="btn btn-xs" @click="openEdit(row)">编辑</button>
<button class="btn btn-xs" @click="deleteTarget = row">删除</button>
@@ -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 vault-note num">共 {{ rows.length }} 条账号记录 · 密码以 AES-256-GCM 加密,服务器仅存密文</div>
</template>
<CredentialDialog