From ce00c9d19393e6fe3cb1ab9608815af9c1e92abc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Fri, 14 Aug 2026 17:54:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=8B=A5=E5=B9=B2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../checksum/build-frontend--DEV--RUNNER-npm- | 2 +- .../checksum/windows-common-generate-bindings | 2 +- app.go | 2 + frontend/src/App.vue | 24 +- frontend/src/api.js | 20 ++ frontend/src/components/AIDayPanel.vue | 14 +- frontend/src/components/TitleBar.vue | 218 +++++++++++++++++ frontend/src/main.js | 28 ++- frontend/src/polish.css | 74 +++++- frontend/src/views/Settings.vue | 2 +- frontend/src/views/TeamReports.vue | 19 +- init.sql | 220 +++++++++--------- main.go | 14 ++ shell.go | 143 +++++++++++- sync.go | 3 +- tools/applyinit/main.go | 2 +- tray_status.go | 139 +++++++++++ workbench.go | 11 + 18 files changed, 779 insertions(+), 158 deletions(-) create mode 100644 frontend/src/components/TitleBar.vue create mode 100644 tray_status.go diff --git a/.task/checksum/build-frontend--DEV--RUNNER-npm- b/.task/checksum/build-frontend--DEV--RUNNER-npm- index 3394be1..f3b6c1d 100644 --- a/.task/checksum/build-frontend--DEV--RUNNER-npm- +++ b/.task/checksum/build-frontend--DEV--RUNNER-npm- @@ -1 +1 @@ -cedbecabd994aac478c9127719ea7b09 +bea799c633664a7311e58ec89934e52 diff --git a/.task/checksum/windows-common-generate-bindings b/.task/checksum/windows-common-generate-bindings index d5014ce..dbe463c 100644 --- a/.task/checksum/windows-common-generate-bindings +++ b/.task/checksum/windows-common-generate-bindings @@ -1 +1 @@ -4fa5aa6193227681772cfc8412c2ad90 +b4270031ccf5e17f6edfe9de8d3baa19 diff --git a/app.go b/app.go index 8fbea11..381d204 100644 --- a/app.go +++ b/app.go @@ -543,6 +543,8 @@ func (a *App) ClearData(mode string, projectID int64) error { return e } +// MigrateDatabase 仅把本机 SQLite 文件搬到新路径并切换连接。 +// 不做任何 schema DDL;云端 MySQL 建表/升级只允许通过仓库根目录 init.sql(或 tools/applyinit)执行。 func (a *App) MigrateDatabase(target string) error { if e := a.ready(); e != nil { return e diff --git a/frontend/src/App.vue b/frontend/src/App.vue index f461df6..cf732a1 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -15,6 +15,7 @@ import MessageBell from './components/MessageBell.vue' import TaskCenter from './components/TaskCenter.vue' import NoteCenter from './components/NoteCenter.vue' import TeamSwitcher from './components/TeamSwitcher.vue' +import TitleBar from './components/TitleBar.vue' import { call, isNative, on } from './api' const route = useRoute() @@ -192,24 +193,12 @@ watch(activeTask, task => { diff --git a/frontend/src/api.js b/frontend/src/api.js index 099b4e5..3859f20 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -32,3 +32,23 @@ export function onTasksChanged(cb) { // 调试/自动化钩子:桌面环境内页面 JS 本就能访问 Wails 桥,这里只是给控制台一个入口。 if (typeof window !== 'undefined') window.__cc = { call } + +/** 复制纯文本到剪贴板;失败时抛错供调用方 toast。 */ +export async function copyText(text) { + const v = String(text ?? '') + if (!v) throw new Error('EMPTY') + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(v) + return + } + // 无 clipboard API 时的降级(极少数环境) + const ta = document.createElement('textarea') + ta.value = v + ta.setAttribute('readonly', '') + ta.style.cssText = 'position:fixed;left:-9999px;top:0' + document.body.appendChild(ta) + ta.select() + const ok = document.execCommand('copy') + document.body.removeChild(ta) + if (!ok) throw new Error('COPY_FAILED') +} diff --git a/frontend/src/components/AIDayPanel.vue b/frontend/src/components/AIDayPanel.vue index 8103cba..60032ef 100644 --- a/frontend/src/components/AIDayPanel.vue +++ b/frontend/src/components/AIDayPanel.vue @@ -2,8 +2,8 @@ import { computed, onMounted, onUnmounted, ref } from 'vue' import { useRouter } from 'vue-router' import { useI18n } from 'vue-i18n' -import { Sparkles, RefreshCw, NotebookPen, Settings } from 'lucide-vue-next' -import { call, on } from '../api' +import { Sparkles, RefreshCw, NotebookPen, Settings, Copy } from 'lucide-vue-next' +import { call, on, copyText } from '../api' import { useAppStore } from '../store' import MarkdownView from './MarkdownView.vue' @@ -34,6 +34,14 @@ async function gen(kind) { store.showToast({ type: 'error', text: t('errors.' + code) !== 'errors.' + code ? t('errors.' + code) : String(e) }) } } +async function copyContent(text) { + try { + await copyText(text) + store.showToast({ type: 'success', key: 'copied' }) + } catch { + store.showToast({ type: 'error', key: 'copyFailed' }) + } +} const fmtTime = at => { const d = new Date(at) if (isNaN(d)) return '' @@ -69,6 +77,7 @@ onUnmounted(() => offSummary?.())
{{ t('wbAiPlan') }} + @@ -80,6 +89,7 @@ onUnmounted(() => offSummary?.())
{{ t('wbReport') }} + diff --git a/frontend/src/components/TitleBar.vue b/frontend/src/components/TitleBar.vue new file mode 100644 index 0000000..f74534a --- /dev/null +++ b/frontend/src/components/TitleBar.vue @@ -0,0 +1,218 @@ + + + diff --git a/frontend/src/main.js b/frontend/src/main.js index 694f35a..a019558 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -399,7 +399,12 @@ const zh = { menuDatabase: '数据管理', menuQuit: '退出', menuAnalyzeNow: '立即分析全部', + menuNewProject: '新建项目', menuAbout: '关于', + winMin: '最小化', + winMax: '最大化', + winRestore: '还原', + winClose: '关闭', aiChat: 'AI 分析', aiSubtitle: '基于项目数据的智能分析与问答', aiSparkLite: '星火 Lite', @@ -442,6 +447,8 @@ const zh = { copyContent: '复制内容', copied: '已复制到剪贴板', copyFailed: '复制失败', + copyReport: '复制', + copyDigest: '复制摘要', settingsSubtitle: '管理排除规则、界面和数据库配置', tabRules: '排除规则', tabAppearance: '界面设置', @@ -741,11 +748,12 @@ const zh = { dbCurrentLoc: '当前位置', dbNoPath: '未获取到数据库路径', copyPathTitle: '复制路径', - dbMigratedMsg: '数据库已迁移并切换到新位置', - dbMigrateFail: '数据库迁移失败', + dbMigratedMsg: '本地数据库文件已移动到新位置', + dbMigrateFail: '移动数据库文件失败', dbPathCopied: '数据库路径已复制', dbCopyFail: '无法复制路径', - migrateBtn: '选择新位置并迁移', + migrateBtn: '更换数据库文件位置', + dbRelocateHint: '仅移动本机 SQLite 文件;云端 MySQL 表结构请用仓库根目录 init.sql 初始化,应用与前端不会执行建表/迁移。', dangerZone: '清空数据', dangerDesc: '选择要清空的数据类型,此操作不可恢复。', clearStats: '清空统计数据', @@ -1238,7 +1246,12 @@ const en = { menuDatabase: 'Data management', menuQuit: 'Quit', menuAnalyzeNow: 'Analyze all now', + menuNewProject: 'New project', menuAbout: 'About', + winMin: 'Minimize', + winMax: 'Maximize', + winRestore: 'Restore', + winClose: 'Close', aiChat: 'AI Analysis', aiSubtitle: 'Project-aware analysis and chat', aiSparkLite: 'Spark Lite', @@ -1281,6 +1294,8 @@ const en = { copyContent: 'Copy content', copied: 'Copied to clipboard', copyFailed: 'Copy failed', + copyReport: 'Copy', + copyDigest: 'Copy digest', settingsSubtitle: 'Manage exclusion rules, appearance and database', tabRules: 'Exclusion rules', tabAppearance: 'Appearance', @@ -1580,11 +1595,12 @@ const en = { dbCurrentLoc: 'Current location', dbNoPath: 'Database path unavailable', copyPathTitle: 'Copy path', - dbMigratedMsg: 'Database migrated to the new location', - dbMigrateFail: 'Database migration failed', + dbMigratedMsg: 'Local SQLite file moved to the new location', + dbMigrateFail: 'Failed to move the database file', dbPathCopied: 'Database path copied', dbCopyFail: 'Could not copy the path', - migrateBtn: 'Choose a new location and migrate', + migrateBtn: 'Change database file location', + dbRelocateHint: 'Only moves the local SQLite file. Cloud MySQL schema must be created with init.sql; the app and frontend never run DDL migrations.', dangerZone: 'Clear data', dangerDesc: 'Choose what to clear. This cannot be undone.', clearStats: 'Clear statistics', diff --git a/frontend/src/polish.css b/frontend/src/polish.css index 849100f..d52b12e 100644 --- a/frontend/src/polish.css +++ b/frontend/src/polish.css @@ -1328,9 +1328,9 @@ html[data-theme=light] .ph-stats{background:rgba(255,255,255,.5)} .ai-day-block>header{display:flex;align-items:center;gap:10px;margin-bottom:4px} .ai-day-block>header b{display:flex;align-items:center;gap:8px;font-size:14px} .ai-day-block>header b svg{width:16px;height:16px;color:var(--primary)} -.ai-day-block>header time{font-size:11.5px;color:var(--muted)} +.ai-day-block>header time{font-size:11.5px;color:var(--muted);margin-right:auto} .ai-day-block>header time.stale{color:var(--yellow)} -.ai-day-block>header .btn{margin-left:auto;height:32px;padding:0 13px;font-size:12.5px;flex:none} +.ai-day-block>header .btn{height:32px;padding:0 13px;font-size:12.5px;flex:none;margin-left:0} .ai-day-body{font-size:13px;line-height:1.7;color:var(--text);overflow-wrap:anywhere} .ai-day-body :is(h1,h2,h3){font-size:13.5px;margin:10px 0 4px} .ai-day-body ul,.ai-day-body ol{margin:6px 0;padding-left:20px} @@ -1755,6 +1755,7 @@ html[data-theme=light] .ph-stats{background:rgba(255,255,255,.5)} .team-digest{padding:18px 20px;margin-bottom:16px} .team-digest .pc-head{margin-bottom:0;align-items:center} .team-digest .pc-head .spacer{flex:1} +.db-relocate-hint{margin:10px 0 0;font-size:12px;line-height:1.55;color:var(--muted)} .team-digest .md-body{margin-top:14px;padding-top:14px;border-top:1px solid var(--border)} .team-report-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:13px} .team-report-card{padding:14px 16px;margin:0} @@ -1762,7 +1763,9 @@ html[data-theme=light] .ph-stats{background:rgba(255,255,255,.5)} .team-report-card header b{flex:1;min-width:0;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .team-report-card header time{color:var(--muted);font-size:11px;flex:none} .team-report-card header em{font-style:normal;color:var(--yellow);font-size:11px;font-weight:800;flex:none} -.team-report-card header .btn.sm{height:26px;padding:0 9px;font-size:11px;flex:none} +.team-report-card header .icon-btn{width:28px;height:28px;flex:none;border:0;border-radius:8px;background:transparent;color:var(--muted);display:grid;place-items:center;cursor:pointer} +.team-report-card header .icon-btn:hover{background:var(--surface-3);color:var(--text)} +.team-report-card header .icon-btn svg{width:14px;height:14px} .team-report-card header .btn.sm svg{width:12px} .trc-ava{width:26px;height:26px;flex:none;border-radius:50%;display:grid;place-items:center;background:rgba(67,201,150,.14);color:var(--green)} .trc-ava.miss{background:rgba(231,189,53,.13);color:var(--yellow)} @@ -1774,3 +1777,68 @@ html[data-theme=light] .ph-stats{background:rgba(255,255,255,.5)} .share-team{display:inline-flex;align-items:center;gap:6px;margin-left:auto} .share-team svg{width:13px;color:var(--muted)} .share-team select{height:26px;border:1px solid var(--border);border-radius:8px;background:var(--surface-2);color:var(--text);padding:0 7px;outline:none;font:inherit;font-size:11.5px;max-width:150px} + +/* ============ 无边框自定义标题栏(logo + 菜单 + 窗控同一行) ============ */ +:root{--titlebar-h:40px} +.titlebar{ + position:fixed;inset:0 0 auto 0;height:var(--titlebar-h);z-index:40; + display:flex;align-items:center;gap:4px;padding:0 0 0 10px; + background:linear-gradient(180deg,color-mix(in srgb,var(--side) 92%,#fff 4%),var(--side)); + border-bottom:1px solid var(--border); + /* 整条顶栏可拖拽窗口;交互控件用 no-drag 退出 */ + --wails-draggable:drag; + -webkit-user-select:none;user-select:none; +} +.tb-brand{display:flex;align-items:center;gap:8px;flex:none;min-width:0;padding-right:6px} +.tb-logo{width:26px;height:26px;flex:none} +.tb-logo svg{width:20px;height:20px} +.tb-name{font-size:12.5px;font-weight:700;letter-spacing:.2px;white-space:nowrap} +.tb-ver{font-style:normal;font-size:10.5px;color:var(--muted);padding:1px 6px;border-radius:999px;border:1px solid var(--border);background:color-mix(in srgb,var(--surface-3) 60%,transparent)} +.tb-menus{display:flex;align-items:center;gap:1px;flex:none;--wails-draggable:no-drag} +.tb-menu{position:relative} +.tb-menu-btn{ + height:28px;border:0;border-radius:7px;background:transparent;color:var(--muted); + padding:0 9px;display:inline-flex;align-items:center;gap:3px;cursor:pointer;font:inherit;font-size:12.2px;font-weight:600; +} +.tb-menu-btn svg{width:12px;height:12px;opacity:.7} +.tb-menu-btn:hover,.tb-menu.open .tb-menu-btn{background:var(--surface-3);color:var(--text)} +.tb-drop{ + position:absolute;top:calc(100% + 4px);left:0;min-width:196px;padding:6px; + border:1px solid var(--border);border-radius:11px;z-index:50; + background:color-mix(in srgb,var(--surface-2) 94%,transparent); + box-shadow:0 18px 40px rgba(0,0,0,.42);backdrop-filter:blur(20px) saturate(150%); + display:flex;flex-direction:column;gap:2px;animation:popoverIn .14s ease-out; +} +.tb-drop hr{border:0;border-top:1px solid var(--border);margin:4px 2px} +.tb-item{ + height:32px;border:0;border-radius:8px;background:transparent;color:var(--text); + padding:0 10px;display:flex;align-items:center;gap:10px;cursor:pointer;font:inherit;font-size:12.4px;text-align:left;width:100%; +} +.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-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} +.tb-win{ + width:46px;border:0;background:transparent;color:var(--muted);cursor:pointer; + display:grid;place-items:center;transition:background .15s,color .15s; +} +.tb-win svg{width:14px;height:14px} +.tb-win:hover{background:var(--surface-3);color:var(--text)} +.tb-win.close:hover{background:#e04852;color:#fff} + +/* 侧栏/主区为标题栏让出顶部空间 */ +.shell .sidebar{top:var(--titlebar-h)} +.shell main{margin-top:var(--titlebar-h);min-height:calc(100vh - var(--titlebar-h))} +.shell main::before,.shell main::after{top:var(--titlebar-h)} +.with-titlebar,.boot-loading.with-titlebar{padding-top:var(--titlebar-h);min-height:100vh;box-sizing:border-box} +.setup-screen.with-titlebar,.boot-loading.with-titlebar{min-height:100vh} +.rail-flyout{top:auto} +.side-rail{padding-top:10px} +.side-sub{padding-top:10px} +.sub-brand{display:none} +@media(max-width:1150px){ + .tb-name,.tb-ver{display:none} + .tb-menus .tb-menu-btn{padding:0 7px;font-size:11.8px} +} diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index bc332e5..9d87760 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -150,7 +150,7 @@ onUnmounted(()=>{clearTimeout(aiKeyTimer)})
{{t('fsAdminOnlyTitle')}}{{t('fsAdminOnlyDesc')}}
- diff --git a/frontend/src/views/TeamReports.vue b/frontend/src/views/TeamReports.vue index 4a5277e..66b62ad 100644 --- a/frontend/src/views/TeamReports.vue +++ b/frontend/src/views/TeamReports.vue @@ -1,8 +1,8 @@