快捷入口已经挪到顶部菜单栏了。

顶部在「工具」和「账号」之间多了一个 快捷 菜单:已钉的入口会列在里面(带图标和未读数),点一项就能跳转。底下还有「添加快捷入口」,用来打开原来的选择面板。有未读时,菜单名旁边会有一个小红点。

概览侧栏里的快捷入口和「添加」按钮已经去掉,之前钉过的项会继续出现在顶部菜单里。
This commit is contained in:
李琦
2026-08-17 14:38:36 +08:00
parent e71ce451ff
commit 684a79be6f
8 changed files with 152 additions and 125 deletions

View File

@@ -2,7 +2,7 @@
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { LayoutDashboard, ScrollText, Settings, X, Database, SlidersHorizontal, Home, ListTodo, TicketCheck, CalendarDays, CalendarCheck2, Sparkles, UserRound, ClipboardList, Wrench, Rocket, ChevronDown, Bell, StickyNote, ListFilter, Palette, Bot, Users, NotebookPen, CloudUpload, Power, Image, Activity, Package, Shield, Plus, Pin, PinOff } from 'lucide-vue-next'
import { LayoutDashboard, ScrollText, Settings, X, Database, SlidersHorizontal, Home, ListTodo, TicketCheck, CalendarDays, CalendarCheck2, Sparkles, UserRound, ClipboardList, Wrench, Rocket, ChevronDown, Bell, StickyNote, ListFilter, Palette, Bot, Users, NotebookPen, CloudUpload, Power, Image, Activity, Package, Shield, Plus, Pin } from 'lucide-vue-next'
import { useAppStore } from './store'
import DatabaseSetup from './components/DatabaseSetup.vue'
import BrowserBlocked from './components/BrowserBlocked.vue'
@@ -108,7 +108,7 @@ const navGroups = [
] }
]
// ---- 概览快捷入口:把其它分区菜单钉到「概览」二级导航 ----
// ---- 顶栏快捷入口:把常用菜单钉到 TitleBar「快捷」下拉 ----
const SHORTCUT_KEY = 'cc-nav-shortcuts'
const loadShortcuts = () => {
try {
@@ -128,7 +128,7 @@ const catalogByTo = computed(() => {
}
return map
})
// 可钉到概览的项:非概览内置、且当前用户可见
// 可钉到顶栏的项:非概览内置、且当前用户可见
const pinnableItems = computed(() => {
const overviewTos = new Set(navGroups[0].items.map(it => it.to))
const out = []
@@ -146,7 +146,6 @@ function addShortcut(to) {
if (navShortcuts.value.includes(to)) return
navShortcuts.value = [...navShortcuts.value, to]
saveShortcuts()
shortcutPicker.value = false
}
function removeShortcut(to) {
navShortcuts.value = navShortcuts.value.filter(x => x !== to)
@@ -157,18 +156,19 @@ function toggleShortcut(to) {
else addShortcut(to)
}
// 钉到顶栏「快捷」菜单的项(带徽标数字,供 TitleBar 渲染)
const pinnedShortcuts = computed(() => navShortcuts.value
.map(to => catalogByTo.value.get(to))
.filter(it => it && (!it.adminOnly || store.syncStatus.userId === 1))
.map(it => ({
to: it.to,
label: it.label,
icon: it.icon,
badge: it.badge ? (store.badges[it.badge] || 0) : 0
})))
// adminOnly 项只对云端管理员id=1展示
const visibleItems = g => {
let items = g.items.filter(it => !it.adminOnly || store.syncStatus.userId === 1)
if (g.id === 'overview') {
const extras = navShortcuts.value
.map(to => catalogByTo.value.get(to))
.filter(it => it && (!it.adminOnly || store.syncStatus.userId === 1))
.map(it => ({ ...it, shortcut: true }))
items = [...items, ...extras]
}
return items
}
const visibleItems = g => g.items.filter(it => !it.adminOnly || store.syncStatus.userId === 1)
const itemActive = it => it.match ? it.match(route) : route.path === it.to.split('?')[0]
// 导航徽标badge 显示数字dot 只显示小圆点;一级 rail 在组内任一非零时亮点
const badgeVal = it => it.badge ? (store.badges[it.badge] || 0) : 0
@@ -186,7 +186,6 @@ watch(() => route.fullPath, () => {
const g = groupOfRoute()
if (g) activeGroupId.value = g
railFlyout.value = null
if (g !== 'overview') shortcutPicker.value = false
for (const grp of navGroups) {
for (const it of grp.items) if (it.children && itemActive(it)) expandedParents.value[it.label] = true
}
@@ -288,7 +287,7 @@ watch(activeTask, task => {
<template>
<BrowserBlocked v-if="!native" />
<template v-else>
<TitleBar :app-version="appVersion" @about="aboutOpen = true" />
<TitleBar :app-version="appVersion" :shortcuts="pinnedShortcuts" @about="aboutOpen = true" @manage-shortcuts="shortcutPicker = true" />
<DatabaseSetup v-if="store.bootstrap.state !== 'ready' && store.bootstrap.state !== 'loading'" :status="store.bootstrap" class="with-titlebar" />
<div v-else-if="store.bootstrap.state === 'ready'" class="shell">
<aside class="sidebar">
@@ -324,29 +323,16 @@ watch(activeTask, task => {
<div class="sub-title">{{ t(activeGroup.label) }}</div>
<nav class="sub-nav" :aria-label="t(activeGroup.label)">
<template v-for="it in visibleItems(activeGroup)" :key="it.to">
<RouterLink :to="it.to" :class="{ active: itemActive(it), shortcut: it.shortcut }">
<RouterLink :to="it.to" :class="{ active: itemActive(it) }">
<component :is="it.icon" /><span>{{ t(it.label) }}</span>
<em v-if="badgeVal(it)" class="nav-badge">{{ badgeText(it) }}</em>
<i v-else-if="dotVal(it)" class="nav-dot" aria-hidden="true" />
<button
v-if="it.shortcut"
type="button"
class="nav-unpin"
:title="t('navUnpinShortcut')"
@click.prevent.stop="removeShortcut(it.to)"
><PinOff /></button>
<button v-if="it.children" type="button" class="sub-caret" :class="{ open: expandedParents[it.label] }" :aria-label="t(it.label)" @click="toggleParent(it, $event)"><ChevronDown /></button>
</RouterLink>
<div v-if="it.children && expandedParents[it.label]" class="sub-children">
<RouterLink v-for="c in it.children" :key="c.to" :to="c.to" :class="{ active: childActive(c) }">{{ t(c.label) }}</RouterLink>
</div>
</template>
<button
v-if="activeGroup.id === 'overview'"
type="button"
class="nav-add-shortcut"
@click="shortcutPicker = !shortcutPicker"
><Plus />{{ t('navAddShortcut') }}</button>
</nav>
<div class="sidebar-bottom">
<span class="version"><i />v{{ appVersion }}</span>

View File

@@ -88,7 +88,7 @@ onUnmounted(() => removeEventListener('mousedown', onDocClick))
<template>
<div ref="root" class="dp" :class="{ disabled }">
<div class="dp-box" @click="toggle">
<div class="dp-box" :class="{ open }" @click="toggle">
<CalendarDays class="dp-ico" />
<input v-model="typed" class="dp-input" :placeholder="placeholder || (zh ? '如 0501 / 2026-05-01' : 'e.g. 0501 / 2026-05-01')"
:disabled="disabled" @click.stop="open = true" @keydown.enter.prevent="commitTyped" @blur="commitTyped" />

View File

@@ -3,14 +3,15 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { Window } from '@wailsio/runtime'
import { Minus, Square, Copy, X, ChevronDown } from 'lucide-vue-next'
import { Minus, Square, Copy, X, ChevronDown, Plus } from 'lucide-vue-next'
import { useAppStore } from '../store'
import { call, isNative } from '../api'
defineProps({
appVersion: { type: String, default: '2.0.0' }
const props = defineProps({
appVersion: { type: String, default: '2.0.0' },
shortcuts: { type: Array, default: () => [] }
})
const emit = defineEmits(['about'])
const emit = defineEmits(['about', 'manage-shortcuts'])
const { t, locale } = useI18n()
const router = useRouter()
@@ -53,6 +54,21 @@ const menus = computed(() => [
{ label: t('logs'), nav: '/logs' }
]
},
{
id: 'shortcuts',
label: t('menuShortcuts'),
notify: props.shortcuts.some(s => s.badge > 0),
items: [
...props.shortcuts.map(s => ({
label: t(s.label),
nav: s.to,
icon: s.icon,
badge: s.badge
})),
...(props.shortcuts.length ? [{ sep: true }] : []),
{ label: t('navAddShortcut'), action: 'manageShortcuts', icon: Plus }
]
},
{
id: 'account',
label: t('menuAccount'),
@@ -119,6 +135,9 @@ async function runItem(it) {
case 'about':
emit('about')
break
case 'manageShortcuts':
emit('manage-shortcuts')
break
case 'quit':
if (confirm(t('quitConfirm'))) {
try { await call('QuitApp') } catch { /* ignore */ }
@@ -189,15 +208,17 @@ onUnmounted(() => {
<nav class="tb-menus" aria-label="Application">
<div v-for="m in menus" :key="m.id" class="tb-menu" :class="{ open: openMenu === m.id }">
<button type="button" class="tb-menu-btn" @click.stop="toggleMenu(m.id)">
{{ m.label }}<ChevronDown />
<button type="button" class="tb-menu-btn" :class="{ notify: m.notify }" @click.stop="toggleMenu(m.id)">
{{ m.label }}<i v-if="m.notify" class="tb-menu-dot" aria-hidden="true" /><ChevronDown />
</button>
<div v-if="openMenu === m.id" class="tb-drop popover-glass" @click.stop>
<template v-for="(it, i) in m.items" :key="i">
<hr v-if="it.sep" />
<button v-else type="button" class="tb-item" :class="{ checked: it.check }" @click="runItem(it)">
<component v-if="it.icon" :is="it.icon" class="tb-item-ico" />
<span>{{ it.label }}</span>
<kbd v-if="it.accel">{{ it.accel }}</kbd>
<em v-if="it.badge" class="tb-item-badge">{{ it.badge > 99 ? '99+' : it.badge }}</em>
<kbd v-else-if="it.accel">{{ it.accel }}</kbd>
<i v-else-if="it.check" class="tb-check"></i>
</button>
</template>

View File

@@ -39,8 +39,8 @@ const zh = {
navOverview: '概览',
navAddShortcut: '添加快捷入口',
navShortcutPicker: '选择快捷入口',
navShortcutHint: '把常用菜单钉到概览,方便一键到达',
navUnpinShortcut: '从概览移除',
navShortcutHint: '把常用菜单钉到顶部「快捷」,方便一键到达',
navUnpinShortcut: '从快捷菜单移除',
navProjects: '项目',
navWork: '事务',
navSystem: '系统',
@@ -544,6 +544,7 @@ const zh = {
menuFile: '文件',
menuView: '视图',
menuTools: '工具',
menuShortcuts: '快捷',
menuHelp: '帮助',
menuDatabase: '数据管理',
menuQuit: '退出',
@@ -1121,8 +1122,8 @@ const en = {
navOverview: 'Overview',
navAddShortcut: 'Add shortcut',
navShortcutPicker: 'Pick shortcuts',
navShortcutHint: 'Pin frequent menus to Overview for one-tap access',
navUnpinShortcut: 'Remove from Overview',
navShortcutHint: 'Pin frequent menus to the top Shortcuts menu',
navUnpinShortcut: 'Remove from Shortcuts',
navProjects: 'Projects',
navWork: 'Work',
navSystem: 'System',
@@ -1626,6 +1627,7 @@ const en = {
menuFile: 'File',
menuView: 'View',
menuTools: 'Tools',
menuShortcuts: 'Shortcuts',
menuHelp: 'Help',
menuDatabase: 'Data management',
menuQuit: 'Quit',

View File

@@ -254,8 +254,8 @@ html[data-theme=light] .lg-art{background:#141a2e}
.modal header button:hover{background:var(--surface-2);color:var(--text)}
.modal>label{margin:0;padding:18px 26px 0;font-weight:800;color:var(--text)}
.modal>label:first-of-type{padding-top:24px}
.modal input:not(.pack-cmd-term),.modal textarea,.modal>label select{height:44px;margin-top:10px;border-color:rgba(145,136,255,.26);background:rgba(32,40,56,.92);box-shadow:inset 0 1px 0 rgba(255,255,255,.035);transition:border-color .2s,box-shadow .2s,background .2s}
.modal input:not(.pack-cmd-term):focus,.modal textarea:focus,.modal>label select:focus{border-color:rgba(145,136,255,.72);box-shadow:0 0 0 3px rgba(115,103,245,.18),inset 0 1px 0 rgba(255,255,255,.05)}
.modal input:not(.pack-cmd-term):not(.dp-input),.modal textarea,.modal>label select{height:44px;margin-top:10px;border-color:rgba(145,136,255,.26);background:rgba(32,40,56,.92);box-shadow:inset 0 1px 0 rgba(255,255,255,.035);transition:border-color .2s,box-shadow .2s,background .2s}
.modal input:not(.pack-cmd-term):not(.dp-input):focus,.modal textarea:focus,.modal>label select:focus{border-color:rgba(145,136,255,.72);box-shadow:0 0 0 3px rgba(115,103,245,.18),inset 0 1px 0 rgba(255,255,255,.05)}
.modal textarea{min-height:92px;height:92px}
.modal footer{gap:12px;padding:22px 26px;align-items:center}
.modal footer .btn{min-width:88px;justify-content:center}
@@ -981,30 +981,31 @@ html[data-theme=light] .calendar-cell.has-art::before{opacity:.82}
html{color-scheme:dark}
html[data-theme=light]{color-scheme:light}
.modal input:not([type=checkbox]):not([type=radio]):not(.pack-cmd-term),.modal textarea,.modal select,
.modal input:not([type=checkbox]):not([type=radio]):not(.pack-cmd-term):not(.dp-input),.modal textarea,.modal select,
.quick-form input,.quick-form select,
.rule-add input,.rule-add select,
.form-panel input:not([type=range]):not([type=checkbox]),.form-panel select{
background-color:color-mix(in srgb,var(--surface-2) 86%,transparent);
border:1px solid var(--border);border-radius:11px;color:var(--text);
min-height:40px;padding:9px 14px;outline:0;font-size:13.5px;
box-shadow:inset 0 1.5px 3px rgba(0,0,0,.16),inset 0 -1px 0 rgba(255,255,255,.03);
background-color:color-mix(in srgb,var(--surface-2) 88%,transparent);
border:1px solid color-mix(in srgb,var(--border) 88%,var(--primary));
border-radius:10px;color:var(--text);
min-height:38px;height:38px;padding:0 12px;outline:0;font-size:13px;font-weight:500;
box-shadow:inset 0 1px 0 rgba(255,255,255,.04);
transition:border-color .18s,box-shadow .18s,background-color .18s;
}
.modal input:not([type=checkbox]):not([type=radio]):not(.pack-cmd-term):hover,.modal textarea:hover,.modal select:hover,
.modal input:not([type=checkbox]):not([type=radio]):not(.pack-cmd-term):not(.dp-input):hover,.modal textarea:hover,.modal select:hover,
.quick-form input:hover,.quick-form select:hover,
.rule-add input:hover,.rule-add select:hover,
.form-panel input:not([type=range]):not([type=checkbox]):hover,.form-panel select:hover{
border-color:color-mix(in srgb,var(--primary) 42%,var(--border));
border-color:color-mix(in srgb,var(--primary) 38%,var(--border));
}
.modal input:not([type=checkbox]):not([type=radio]):not(.pack-cmd-term):focus,.modal textarea:focus,.modal select:focus,
.modal input:not([type=checkbox]):not([type=radio]):not(.pack-cmd-term):not(.dp-input):focus,.modal textarea:focus,.modal select:focus,
.quick-form input:focus,.quick-form select:focus,
.rule-add input:focus,.rule-add select:focus,
.form-panel input:not([type=range]):not([type=checkbox]):focus,.form-panel select:focus{
border-color:var(--primary);background-color:var(--surface-2);
box-shadow:0 0 0 3.5px rgba(115,103,245,.16),inset 0 1.5px 3px rgba(0,0,0,.08);
box-shadow:0 0 0 3px color-mix(in srgb,var(--primary) 22%,transparent);
}
.modal textarea{min-height:96px;line-height:1.55;resize:vertical}
.modal textarea{min-height:96px;height:auto;line-height:1.55;resize:vertical;padding:10px 12px}
::placeholder{color:color-mix(in srgb,var(--muted) 72%,transparent)}
/* 所有下拉统一为自绘箭头(去掉系统灰三角,后台感的主要来源之一)。
@@ -1023,16 +1024,29 @@ select::-ms-expand{display:none}
input[type=range]{accent-color:var(--primary)}
input[type=checkbox],input[type=radio]{accent-color:var(--primary)}
/* 模态框:更大的圆角、顶部渐变饰线、无分割线的松弛留白 */
.overlay{background:rgba(7,10,17,.56);backdrop-filter:blur(10px) saturate(120%);-webkit-backdrop-filter:blur(10px) saturate(120%)}
.modal{position:relative;border-radius:18px;overflow:auto;box-shadow:0 42px 96px rgba(0,0,0,.52),inset 0 1px 0 var(--glass-highlight)}
.modal::before{content:"";position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,rgba(139,124,248,.85) 28%,rgba(83,214,162,.6) 72%,transparent);opacity:.9;pointer-events:none}
.modal header{border-bottom:0;padding:22px 26px 4px;align-items:center}
.modal header h2{font-size:17px;letter-spacing:.3px}
.modal header button{width:32px;height:32px;border-radius:10px;display:grid;place-items:center;font-size:20px;line-height:1;transition:background-color .15s,color .15s}
/* 模态框:玻璃底、顶部饰线、入场、页头/页脚细分割 */
.overlay{background:rgba(6,9,16,.58);backdrop-filter:blur(14px) saturate(140%);-webkit-backdrop-filter:blur(14px) saturate(140%)}
.modal{position:relative;border-radius:20px;overflow:auto;border:1px solid color-mix(in srgb,var(--border) 80%,rgba(255,255,255,.16));
background:linear-gradient(180deg,color-mix(in srgb,var(--surface) 92%,#fff),var(--surface));
box-shadow:0 28px 80px rgba(0,0,0,.48),0 0 0 1px rgba(255,255,255,.03),inset 0 1px 0 rgba(255,255,255,.06);
animation:modalIn .22s cubic-bezier(.22,1,.36,1)}
.modal::before{content:"";position:absolute;top:0;left:16px;right:16px;height:1px;border-radius:1px;background:linear-gradient(90deg,transparent,rgba(143,132,250,.7) 22%,rgba(67,201,150,.45) 78%,transparent);opacity:.95;pointer-events:none;z-index:1}
.modal header{border-bottom:1px solid color-mix(in srgb,var(--border) 70%,transparent);padding:16px 22px 14px;align-items:center}
.modal header h2{font-size:15.5px;font-weight:700;letter-spacing:.2px}
.modal header button{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;font-size:18px;line-height:1;color:var(--muted);transition:background-color .15s,color .15s,transform .15s}
.modal header button:hover{background:var(--surface-3);color:var(--text)}
.modal>label{font-size:12px;font-weight:600;letter-spacing:.4px;color:var(--muted);margin:16px 26px 0}
.modal footer{border-top:0;padding:20px 26px 24px;gap:10px}
.modal header button:hover svg{transform:rotate(90deg)}
.modal header button svg{width:16px;height:16px;transition:transform .2s}
.modal>label{font-size:11.5px;font-weight:600;letter-spacing:.35px;color:var(--muted);margin:14px 22px 0}
.modal footer{border-top:1px solid color-mix(in srgb,var(--border) 70%,transparent);padding:14px 22px 16px;gap:10px;background:color-mix(in srgb,var(--surface-2) 42%,transparent)}
@keyframes modalIn{from{opacity:0;transform:translateY(12px) scale(.975)}to{opacity:1;transform:none}}
@media(prefers-reduced-motion:reduce){.modal{animation:none}}
html[data-theme=light] .overlay{background:rgba(15,23,42,.32)}
html[data-theme=light] .modal{box-shadow:0 24px 64px rgba(15,23,42,.16),inset 0 1px 0 #fff}
html[data-theme=light] .split-fields{background:color-mix(in srgb,var(--surface-2) 70%,#fff)}
html[data-theme=light] .split-fields label{background:#fff;border-color:var(--border)}
html[data-theme=light] .dp-panel{background:#fff;box-shadow:0 16px 40px rgba(15,23,42,.14)}
html[data-theme=light] .dq-chip.active{color:#fff}
.form-error{margin:14px 26px 0;padding:9px 13px;border-radius:10px;background:rgba(240,94,104,.12);border:1px solid rgba(240,94,104,.32);color:#ff9aa2;font-size:12.5px}
/* 按钮:主按钮渐变 + 光泽,悬停轻微上浮 */
@@ -1126,37 +1140,39 @@ input[type=checkbox],input[type=radio]{accent-color:var(--primary)}
@media(prefers-reduced-motion:reduce){.daily-card,.daily-hero img{animation:none!important}}
/* 待办/工单编辑:左侧字段列 + 右侧整块 Markdown 编辑区 */
.modal-split{width:min(960px,calc(100vw - 64px));height:min(680px,calc(100dvh - 56px));overflow:hidden}
.modal-split{width:min(980px,calc(100vw - 64px));height:min(700px,calc(100dvh - 56px));overflow:hidden}
.modal-split header h2{display:flex;align-items:baseline;gap:10px}
.modal-split header h2 .quick-date{color:var(--muted);font-size:12px;font-weight:500;letter-spacing:.4px}
.modal-split .split-body{flex:1;display:flex;min-height:0}
.split-fields{width:288px;flex:none;display:flex;flex-direction:column;overflow-y:auto;padding:14px 24px 18px 26px;border-right:1px solid color-mix(in srgb,var(--border) 72%,transparent)}
.split-fields label{display:block;font-size:12px;font-weight:600;letter-spacing:.4px;color:var(--muted);margin-top:14px}
.split-fields>label:first-child{margin-top:2px}
.split-fields input,.split-fields select{width:100%}
.split-fields .field-pair{display:grid;grid-template-columns:1fr 1fr;gap:0 12px}
.split-editor{flex:1;min-width:0;display:flex;flex-direction:column;padding:14px 26px 18px 24px}
.split-editor .md-toolbar{margin-bottom:0}
.md-field-label{font-size:12px;font-weight:600;letter-spacing:.4px;color:var(--muted);margin-right:auto}
.split-editor .md-editor{flex:1;min-height:0}
.split-editor .md-editor textarea{flex:1;height:auto;min-height:0;resize:none}
.split-editor .md-preview-box{flex:1;min-height:0;max-height:none}
.split-fields{width:300px;flex:none;display:flex;flex-direction:column;overflow-y:auto;padding:12px 14px 16px 16px;border-right:1px solid color-mix(in srgb,var(--border) 68%,transparent);background:color-mix(in srgb,var(--surface-2) 28%,transparent)}
.split-fields label{display:flex;flex-direction:column;gap:7px;font-size:11px;font-weight:600;letter-spacing:.45px;color:var(--muted);margin-top:8px;padding:9px 10px 10px;border-radius:12px;border:1px solid color-mix(in srgb,var(--border) 62%,transparent);background:color-mix(in srgb,var(--surface) 70%,transparent)}
.split-fields>label:first-child{margin-top:0}
.split-fields .req{color:#f08a92;font-weight:700;margin-left:1px}
.split-fields input,.split-fields select{width:100%;margin-top:0;height:36px;min-height:36px}
.split-fields .field-pair{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px}
.split-fields .field-pair label{margin-top:0}
.split-editor{flex:1;min-width:0;display:flex;flex-direction:column;padding:12px 18px 16px 16px}
.split-editor .md-toolbar{margin-bottom:0;padding:8px 10px;border-radius:12px 12px 0 0;border:1px solid color-mix(in srgb,var(--border) 70%,transparent);border-bottom:0;background:color-mix(in srgb,var(--surface-2) 55%,transparent)}
.md-field-label{font-size:11px;font-weight:600;letter-spacing:.45px;color:var(--muted);margin-right:auto}
.split-editor .md-editor{flex:1;min-height:0;margin-top:0;border:1px solid color-mix(in srgb,var(--border) 70%,transparent);border-radius:0 0 12px 12px;overflow:hidden;background:color-mix(in srgb,var(--surface-2) 40%,transparent)}
.split-editor .md-editor textarea{flex:1;height:auto;min-height:0;resize:none;margin:0;border:0;border-radius:0;box-shadow:none;background:transparent;padding:14px 16px}
.split-editor .md-preview-box{flex:1;min-height:0;max-height:none;padding:14px 16px;border:0;border-radius:0;box-shadow:none;background:transparent}
.modal-split footer{align-items:center}
.modal-split footer .form-error{margin:0 auto 0 0;padding:7px 12px}
/* 截止日期快捷标签:今天/明天/N天后可增删 */
.due-quick{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
.dq-chip{position:relative;display:inline-flex;align-items:center;gap:2px;height:24px;padding:0 10px;border-radius:999px;border:1px solid var(--border);background:color-mix(in srgb,var(--surface-2) 78%,transparent);color:var(--muted);font-size:11.5px;font-weight:600;cursor:pointer;transition:border-color .15s,color .15s,background .15s;user-select:none}
.due-quick{display:flex;flex-wrap:wrap;gap:5px;margin-top:2px}
.dq-chip{position:relative;display:inline-flex;align-items:center;gap:2px;height:22px;padding:0 9px;border-radius:999px;border:1px solid color-mix(in srgb,var(--border) 80%,transparent);background:color-mix(in srgb,var(--surface-3) 55%,transparent);color:var(--muted);font-size:11px;font-weight:600;cursor:pointer;transition:border-color .15s,color .15s,background .15s;user-select:none}
.dq-chip:hover:not(:disabled){border-color:color-mix(in srgb,var(--primary) 45%,var(--border));color:var(--text)}
.dq-chip.active{background:rgba(115,103,245,.16);border-color:color-mix(in srgb,var(--primary) 62%,var(--border));color:#a99ffb}
.dq-chip.active{background:linear-gradient(140deg,rgba(143,131,249,.95),rgba(110,95,242,.95));border-color:transparent;color:#fff;box-shadow:0 4px 12px rgba(101,87,232,.28)}
.dq-chip:disabled{opacity:.5;cursor:not-allowed}
/* 删除角标绝对定位悬浮出现不占布局空间chip 宽度恒定,避免 hover 重排导致点击目标跳动 */
.dq-x{position:absolute;top:-5px;right:-4px;z-index:1;display:grid;place-items:center;width:15px;height:15px;border-radius:50%;background:var(--surface-3);border:1px solid var(--border);color:var(--muted);opacity:0;transform:scale(.6);transition:opacity .15s,transform .15s;cursor:pointer;pointer-events:none}
.dq-chip:hover .dq-x{opacity:1;transform:scale(1);pointer-events:auto}
.dq-x:hover{color:#fff;background:var(--red);border-color:var(--red)}
.dq-x svg{width:9px;height:9px}
.dq-add{width:26px;padding:0;justify-content:center}
.dq-add svg{width:12px}
.dq-add{width:22px;height:22px;padding:0;justify-content:center;border-radius:999px}
.dq-add svg{width:11px}
.dq-input{padding:0 9px;gap:4px;cursor:default}
.due-quick .dq-input input[type=number]{width:38px;height:20px;min-height:0;border:0;outline:0;background:transparent;color:var(--text);font-size:11.5px;padding:0;margin:0;box-shadow:none;border-radius:0;text-align:center;-moz-appearance:textfield;appearance:textfield}
.dq-input input::-webkit-outer-spin-button,.dq-input input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
@@ -1164,9 +1180,9 @@ input[type=checkbox],input[type=radio]{accent-color:var(--primary)}
/* 编辑器:编辑/预览切换 + 插图按钮 */
.md-editor{display:flex;flex-direction:column;gap:0;margin-top:10px}
.md-toolbar{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}
.md-toolbar .tabs{margin:0;padding:3px;background:var(--surface-2);border:1px solid var(--border)}
.md-toolbar .tabs button{height:26px;padding:0 12px;font-size:12px}
.md-toolbar .btn.md-img-btn{height:30px;padding:0 12px;font-size:12px;border-radius:9px;gap:6px}
.md-toolbar .tabs{margin:0;padding:3px;background:color-mix(in srgb,var(--surface-3) 70%,transparent);border:1px solid color-mix(in srgb,var(--border) 70%,transparent);border-radius:9px}
.md-toolbar .tabs button{height:26px;padding:0 11px;font-size:12px;border-radius:7px}
.md-toolbar .btn.md-img-btn{height:28px;padding:0 11px;font-size:12px;border-radius:8px;gap:6px}
.md-toolbar .btn.md-img-btn svg{width:14px}
.md-editor textarea{margin-top:0;min-height:132px}
.md-preview-box{min-height:132px;max-height:320px;overflow:auto;border:1px solid var(--border);border-radius:11px;padding:11px 14px;background:color-mix(in srgb,var(--surface-2) 60%,transparent);box-shadow:inset 0 1.5px 3px rgba(0,0,0,.12)}
@@ -1335,37 +1351,40 @@ html[data-theme=light] .ph-stats{background:rgba(255,255,255,.5)}
.stats-mini .stats-toggle{margin-left:auto}
/* ============ DatePicker 日期选择器 ============ */
.dp{position:relative;margin-top:7px}
.dp-box{display:flex;align-items:center;gap:8px;background:var(--surface-2);border:1px solid var(--border);border-radius:7px;padding:0 10px;height:41px;cursor:pointer;transition:border-color .15s}
.dp-box:focus-within{border-color:var(--primary)}
.dp-ico{width:16px;color:var(--muted);flex:none}
.dp .dp-input{flex:1;min-width:0;border:0;background:transparent;padding:0;margin:0;outline:none;color:var(--text);font:inherit;font-size:13px;height:100%}
.dp-clear{border:0;background:transparent;color:var(--muted);cursor:pointer;padding:2px;display:grid;place-items:center;flex:none}
.dp-clear svg{width:14px}
.dp-clear:hover{color:var(--red)}
.dp{position:relative;margin-top:0}
.split-fields .dp,.modal>label .dp{margin-top:0}
.dp-box{display:flex;align-items:center;gap:8px;background:color-mix(in srgb,var(--surface-2) 88%,transparent);border:1px solid color-mix(in srgb,var(--border) 88%,var(--primary));border-radius:10px;padding:0 10px;height:36px;cursor:pointer;transition:border-color .18s,box-shadow .18s,background-color .18s}
.dp-box:hover{border-color:color-mix(in srgb,var(--primary) 38%,var(--border))}
.dp-box:focus-within,.dp-box.open{border-color:var(--primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--primary) 22%,transparent);background:var(--surface-2)}
.dp-ico{width:15px;height:15px;color:color-mix(in srgb,var(--primary) 70%,var(--muted));flex:none}
.dp .dp-input{flex:1;min-width:0;border:0!important;background:transparent!important;padding:0!important;margin:0!important;outline:none;color:var(--text);font:inherit;font-size:13px;font-weight:600;font-variant-numeric:tabular-nums;height:auto!important;min-height:0!important;box-shadow:none!important}
.dp-clear{border:0;background:transparent;color:var(--muted);cursor:pointer;padding:2px;display:grid;place-items:center;flex:none;border-radius:6px}
.dp-clear svg{width:13px}
.dp-clear:hover{color:var(--red);background:color-mix(in srgb,var(--red) 14%,transparent)}
.dp.disabled .dp-box{opacity:.55;cursor:not-allowed}
.dp-panel{position:absolute;top:calc(100% + 6px);left:0;z-index:70;width:264px;padding:10px;border:1px solid var(--border);border-radius:10px;background:color-mix(in srgb,var(--surface-2) 92%,transparent);box-shadow:0 18px 42px rgba(0,0,0,.38);backdrop-filter:blur(24px) saturate(150%);animation:popoverIn .16s ease-out}
.dp-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
.dp-nav{width:28px;height:28px;border:1px solid var(--border);background:var(--surface-2);border-radius:7px;color:var(--muted);cursor:pointer;display:grid;place-items:center;transition:color .15s,border-color .15s}
.dp-nav:hover{color:var(--text);border-color:var(--primary)}
.dp-panel{position:absolute;top:calc(100% + 8px);left:0;z-index:90;width:272px;padding:12px;border:1px solid color-mix(in srgb,var(--border) 75%,rgba(255,255,255,.12));border-radius:14px;background:color-mix(in srgb,var(--surface) 88%,var(--surface-2));box-shadow:0 18px 48px rgba(0,0,0,.42),0 0 0 1px rgba(255,255,255,.04);backdrop-filter:blur(22px) saturate(150%);animation:popoverIn .16s ease-out}
.dp-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.dp-nav{width:28px;height:28px;border:1px solid var(--border);background:color-mix(in srgb,var(--surface-2) 80%,transparent);border-radius:8px;color:var(--muted);cursor:pointer;display:grid;place-items:center;transition:color .15s,border-color .15s,background .15s}
.dp-nav:hover{color:var(--text);border-color:var(--primary);background:color-mix(in srgb,var(--primary) 12%,var(--surface-2))}
.dp-nav svg{width:15px}
.dp-ym{display:flex;gap:4px}
.dp-ym button{border:0;background:transparent;color:var(--text);font-weight:700;font-size:13.5px;padding:4px 8px;border-radius:6px;cursor:pointer}
.dp-ym button:hover,.dp-ym button.on{background:var(--surface-3);color:var(--primary)}
.dp-week{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;color:var(--muted);font-size:11px;margin-bottom:4px}
.dp-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:2px}
.dp-ym button{border:0;background:transparent;color:var(--text);font-weight:700;font-size:13px;padding:4px 8px;border-radius:7px;cursor:pointer}
.dp-ym button:hover,.dp-ym button.on{background:color-mix(in srgb,var(--primary) 14%,var(--surface-3));color:var(--primary)}
.dp-week{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;color:var(--muted);font-size:10.5px;font-weight:600;letter-spacing:.3px;margin-bottom:6px}
.dp-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:3px}
.dp-grid.months{grid-template-columns:repeat(3,1fr);gap:6px}
.dp-day{height:30px;border:0;background:transparent;color:var(--text);border-radius:6px;cursor:pointer;font-size:12.5px;font:inherit}
.dp-day:hover{background:var(--surface-3)}
.dp-day.dim{color:var(--muted);opacity:.45}
.dp-day.today{box-shadow:inset 0 0 0 1px var(--primary)}
.dp-day.active{background:var(--primary);color:#fff}
.dp-cell{height:40px;border:1px solid var(--border);background:var(--surface-2);color:var(--text);border-radius:7px;cursor:pointer;font:inherit;font-size:12.5px}
.dp-day{height:30px;border:0;background:transparent;color:var(--text);border-radius:8px;cursor:pointer;font-size:12.5px;font:inherit;transition:background .12s,color .12s}
.dp-day:hover{background:color-mix(in srgb,var(--primary) 16%,var(--surface-3))}
.dp-day.dim{color:var(--muted);opacity:.4}
.dp-day.today{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--primary) 70%,transparent);color:var(--primary);font-weight:700}
.dp-day.active{background:linear-gradient(140deg,#8f83f9,#6e5ff2);color:#fff;box-shadow:0 4px 10px rgba(101,87,232,.32);font-weight:700}
.dp-day.active.today{box-shadow:0 4px 10px rgba(101,87,232,.32)}
.dp-cell{height:38px;border:1px solid var(--border);background:color-mix(in srgb,var(--surface-2) 80%,transparent);color:var(--text);border-radius:9px;cursor:pointer;font:inherit;font-size:12.5px}
.dp-cell:hover{border-color:var(--primary)}
.dp-cell.active{background:var(--primary);border-color:var(--primary);color:#fff}
.dp-foot{display:flex;justify-content:space-between;margin-top:8px;border-top:1px solid var(--border);padding-top:8px}
.dp-foot button{border:0;background:transparent;color:var(--primary);cursor:pointer;font-size:12.5px;padding:4px 6px;font:inherit}
.dp-foot button:hover{text-decoration:underline}
.dp-cell.active{background:linear-gradient(140deg,#8f83f9,#6e5ff2);border-color:transparent;color:#fff}
.dp-foot{display:flex;justify-content:space-between;margin-top:10px;border-top:1px solid color-mix(in srgb,var(--border) 75%,transparent);padding-top:8px}
.dp-foot button{border:0;background:transparent;color:var(--primary);cursor:pointer;font-size:12px;font-weight:600;padding:4px 6px;font:inherit;border-radius:6px}
.dp-foot button:hover{background:color-mix(in srgb,var(--primary) 12%,transparent);text-decoration:none}
/* 页头日报导航里的紧凑形态 */
.team-date-nav .dp{margin-top:0;width:170px}
.team-date-nav .dp-box{height:34px;border-radius:9px}
@@ -1636,17 +1655,11 @@ html[data-theme=light] .lp-log-line{color:#1f2937}
.sub-brand b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sub-title{margin:12px 7px 5px;font-size:10px;font-weight:700;letter-spacing:1.2px;color:var(--muted);text-transform:uppercase}
.sidebar .sub-nav{display:flex;flex-direction:column;gap:3px;margin-top:2px}
.sidebar .sub-nav a{height:38px;display:flex;align-items:center;gap:9px;padding:0 10px;border-radius:8px;color:var(--muted);font-size:12.8px}
.sidebar .sub-nav a{height:38px;display:flex;align-items:center;gap:9px;padding:0 10px;border-radius:8px;color:var(--muted);font-size:12.8px;overflow:hidden}
.sidebar .sub-nav a>span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sidebar .sub-nav a svg{width:16px;height:16px;flex:none}
.sidebar .sub-nav a:hover{background:var(--surface-3);color:var(--text)}
.sidebar .sub-nav a.active{background:color-mix(in srgb,var(--primary) 15%,transparent);color:#a9a2ff}
.sidebar .sub-nav a.shortcut{border:1px dashed color-mix(in srgb,var(--primary) 28%,transparent)}
.nav-unpin{margin-left:4px;display:grid;place-items:center;width:22px;height:22px;border:0;border-radius:6px;background:transparent;color:var(--muted);cursor:pointer;padding:0;flex:none}
.nav-unpin svg{width:12px!important;height:12px!important}
.nav-unpin:hover{background:rgba(240,84,84,.12);color:var(--red)}
.nav-add-shortcut{display:flex;align-items:center;gap:8px;height:34px;margin:6px 0 0;padding:0 10px;border:1px dashed var(--border);border-radius:8px;background:transparent;color:var(--muted);font:inherit;font-size:12px;cursor:pointer;white-space:nowrap;flex-shrink:0}
.nav-add-shortcut svg{width:14px;height:14px;flex:none}
.nav-add-shortcut:hover{border-color:var(--primary);color:var(--primary);background:color-mix(in srgb,var(--primary) 8%,transparent)}
.nav-shortcut-modal{width:min(420px,92vw);max-height:min(560px,80vh);padding:0;overflow:hidden;display:flex;flex-direction:column}
.nav-shortcut-modal .modal-head{flex:none}
.nav-shortcut-modal .modal-head h2{display:flex;align-items:center;gap:8px;margin:0;font-size:15px}
@@ -1671,8 +1684,9 @@ html[data-theme=light] .lp-log-line{color:#1f2937}
/* 窄屏:只留 rail点分类弹浮层二级菜单 */
.rail-flyout{position:fixed;left:78px;width:196px;padding:10px;border:1px solid var(--border);border-radius:12px;background:color-mix(in srgb,var(--surface-2) 90%,transparent);box-shadow:0 18px 42px rgba(0,0,0,.4);backdrop-filter:blur(24px) saturate(150%);animation:popoverIn .16s ease-out;z-index:60;display:flex;flex-direction:column;gap:3px}
.fly-title{font-size:10px;font-weight:700;letter-spacing:1.2px;color:var(--muted);padding:2px 10px 6px;text-transform:uppercase}
.rail-flyout a{height:36px;display:flex;align-items:center;gap:9px;padding:0 10px;border-radius:8px;color:var(--muted);text-decoration:none;font-size:12.8px}
.rail-flyout a svg{width:16px;height:16px}
.rail-flyout a{height:36px;display:flex;align-items:center;gap:9px;padding:0 10px;border-radius:8px;color:var(--muted);text-decoration:none;font-size:12.8px;overflow:hidden}
.rail-flyout a>span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rail-flyout a svg{width:16px;height:16px;flex:none}
.rail-flyout a:hover{background:var(--surface-3);color:var(--text)}
.rail-flyout a.active{background:color-mix(in srgb,var(--primary) 15%,transparent);color:#a9a2ff}
.rail-flyout a.fly-child{padding-left:35px}
@@ -2073,6 +2087,10 @@ html,body,#app{height:100%;overflow:hidden}
.tb-item:hover{background:var(--surface-3)}
.tb-item span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tb-item kbd{font:inherit;font-size:10.5px;color:var(--muted)}
.tb-item-ico{width:14px;height:14px;flex:none;color:var(--muted)}
.tb-item-badge{flex:none;min-width:18px;height:16px;padding:0 5px;border-radius:999px;display:inline-grid;place-items:center;font-style:normal;font-size:10px;font-weight:800;color:#fff;background:linear-gradient(135deg,#f05e68,#e0447f);font-variant-numeric:tabular-nums;line-height:1}
.tb-menu-btn.notify{position:relative}
.tb-menu-dot{width:6px;height:6px;border-radius:50%;background:#f05e68;flex:none;margin:0 1px 6px 0;box-shadow:0 0 0 2px color-mix(in srgb,var(--surface-2) 80%,transparent)}
.tb-check{font-style:normal;color:#a9a2ff;font-size:12px}
.tb-spacer{flex:1;min-width:12px;height:100%}
.tb-controls{display:flex;align-items:stretch;height:100%;flex:none;--wails-draggable:no-drag}

View File

@@ -160,10 +160,10 @@ onUnmounted(() => offTasks())
<div class="split-body">
<div class="split-fields">
<label>{{ t('ticketTitle') }}<input v-model="form.title" :disabled="saving" required /></label>
<label>{{ t('relatedProject') }} *<select v-model.number="form.projectId" :disabled="saving" required><option :value="0" disabled>{{ t('selectProject') }}</option><option v-for="p in store.projects" :key="p.id" :value="p.id">{{ p.name }}</option></select></label>
<label>{{ t('relatedProject') }} <span class="req">*</span><select v-model.number="form.projectId" :disabled="saving" required><option :value="0" disabled>{{ t('selectProject') }}</option><option v-for="p in store.projects" :key="p.id" :value="p.id">{{ p.name }}</option></select></label>
<label>{{ t('ticketTypeLabel') }}<select v-model="form.type" :disabled="saving"><option v-for="k in ['feature', 'bug', 'task', 'improvement']" :key="k" :value="k">{{ t('ticketType.' + k) }}</option></select></label>
<label>{{ t('startDate') }} *<DatePicker v-model="form.startAt" :disabled="saving" :clearable="false" /></label>
<label>{{ t('dueDate') }} *<DatePicker v-model="form.dueAt" :disabled="saving" :clearable="false" />
<label>{{ t('startDate') }} <span class="req">*</span><DatePicker v-model="form.startAt" :disabled="saving" :clearable="false" /></label>
<label>{{ t('dueDate') }} <span class="req">*</span><DatePicker v-model="form.dueAt" :disabled="saving" :clearable="false" />
<DueQuickPick v-model="form.dueAt" :disabled="saving" />
</label>
<div class="field-pair">