更新若干功能
This commit is contained in:
@@ -1 +1 @@
|
||||
bea799c633664a7311e58ec89934e52
|
||||
e4f8c1296afa8a28d19333bcd28c94b1
|
||||
|
||||
@@ -1 +1 @@
|
||||
b4270031ccf5e17f6edfe9de8d3baa19
|
||||
62c10364921311d089da2985d202f33e
|
||||
|
||||
3
app.go
3
app.go
@@ -111,6 +111,7 @@ func (a *App) startup(ctx context.Context) {
|
||||
}
|
||||
a.store = s
|
||||
a.bootstrap = BootstrapStatus{State: BootstrapReady, DefaultPath: defaultPath, DatabasePath: c.DatabasePath}
|
||||
a.applyPackagedSyncConfig()
|
||||
a.store.Log("info", "系统", "应用程序启动", "")
|
||||
a.store.Log("info", "数据库", "桌面运行时已连接", s.path)
|
||||
}
|
||||
@@ -146,6 +147,7 @@ func (a *App) InitializeDatabase(path string) (BootstrapStatus, error) {
|
||||
return a.bootstrap, e
|
||||
}
|
||||
a.bootstrap = BootstrapStatus{State: BootstrapReady, DefaultPath: a.bootstrap.DefaultPath, DatabasePath: s.path}
|
||||
a.applyPackagedSyncConfig()
|
||||
a.store.Log("info", "数据库", "数据库初始化成功", s.path)
|
||||
return a.bootstrap, nil
|
||||
}
|
||||
@@ -604,6 +606,7 @@ func (a *App) MigrateDatabase(target string) error {
|
||||
return coded("BOOTSTRAP_WRITE_FAILED", e)
|
||||
}
|
||||
a.bootstrap = BootstrapStatus{State: BootstrapReady, DefaultPath: a.bootstrap.DefaultPath, DatabasePath: target}
|
||||
a.applyPackagedSyncConfig()
|
||||
a.store.Log("info", "数据库", "数据库迁移成功", target)
|
||||
return nil
|
||||
}
|
||||
|
||||
7
build/sync.defaults.json
Normal file
7
build/sync.defaults.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"host": "101.43.12.11",
|
||||
"port": 3306,
|
||||
"user": "code_count",
|
||||
"password": "code_count",
|
||||
"database": "code_count"
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
main{position:relative;overflow:visible}
|
||||
main{position:relative;overflow:visible;min-height:0}
|
||||
main::before{content:"";position:fixed;inset:0 0 0 232px;pointer-events:none;background:linear-gradient(90deg,rgba(255,255,255,.035) 1px,transparent 1px),linear-gradient(180deg,rgba(255,255,255,.03) 1px,transparent 1px);background-size:80px 80px;mask-image:linear-gradient(90deg,transparent,black 14%,black 86%,transparent);animation:gridDrift 18s linear infinite;opacity:.55}
|
||||
main::after{content:"";position:fixed;inset:0 0 0 232px;pointer-events:none;background:linear-gradient(115deg,transparent 0 35%,rgba(124,115,255,.1) 45%,transparent 56%);transform:translateX(-35%);animation:ambientSweep 12s ease-in-out infinite}
|
||||
.page{position:relative;z-index:1}
|
||||
/* 主内容区已是滚动容器,sticky 相对 main 顶边,不再叠加标题栏高度 */
|
||||
html{--topbar-h:0px}
|
||||
.sticky-head{position:sticky;top:calc(var(--topbar-h) + 8px);z-index:20;margin:-12px -10px 24px;padding:18px 20px;border:1px solid rgba(255,255,255,.08);border-radius:8px;background:color-mix(in srgb,var(--surface) calc(clamp(.72, var(--glass-user-opacity, .55) + .18, .94) * 100%),transparent);backdrop-filter:blur(24px) saturate(140%);box-shadow:0 16px 38px rgba(0,0,0,.22)}
|
||||
@supports not (backdrop-filter:blur(1px)){.sticky-head{background:color-mix(in srgb,var(--surface) 97%,transparent)}}
|
||||
@@ -498,7 +499,7 @@ html[data-theme=light] .heat-cell.l1{background:#b9efd4}html[data-theme=light] .
|
||||
.calendar-layout{display:grid;grid-template-columns:minmax(0,3fr) minmax(230px,1fr);gap:20px;align-items:start}
|
||||
.calendar-weekdays{display:grid;grid-template-columns:repeat(7,1fr);gap:6px;margin-bottom:8px;color:var(--muted);font-size:12px;text-align:center}
|
||||
/* 面板撑满可用视口高度,格子行高 1fr 平分剩余空间,窗口越大格子越高 */
|
||||
.calendar-grid-panel{display:flex;flex-direction:column;min-height:calc(100vh - var(--topbar-h) - 196px)}
|
||||
.calendar-grid-panel{display:flex;flex-direction:column;min-height:calc(100vh - var(--titlebar-h, 40px) - 196px)}
|
||||
.calendar-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:6px;grid-auto-rows:minmax(86px,1fr);flex:1}
|
||||
.calendar-cell{position:relative;min-height:86px;border:1px solid var(--border);border-radius:7px;background:var(--surface-2);padding:6px;display:flex;flex-direction:column;gap:4px;cursor:pointer;text-align:left;color:var(--text);transition:border-color .15s,background .15s}
|
||||
.calendar-cell:hover{border-color:var(--primary)}
|
||||
@@ -627,7 +628,7 @@ html[data-theme=light] .heat-cell.l1{background:#b9efd4}html[data-theme=light] .
|
||||
.ai-key-notice>div{flex:1;display:grid;gap:3px}
|
||||
.ai-key-notice small{color:var(--muted)}
|
||||
/* AI 页固定视口高度:页面自身不滚动,左历史/右会话各自内部滚动 */
|
||||
.ai-page{height:calc(100vh - var(--topbar-h));display:flex;flex-direction:column;overflow:hidden;padding-bottom:22px}
|
||||
.ai-page{height:calc(100vh - var(--titlebar-h, 40px));display:flex;flex-direction:column;overflow:hidden;padding-bottom:22px}
|
||||
.ai-page .sticky-head{position:static;flex:none;margin:-12px -10px 18px}
|
||||
.ai-page .ai-key-notice{flex:none}
|
||||
.ai-layout{flex:1;min-height:0;display:grid;grid-template-columns:290px minmax(0,1fr);gap:20px;align-items:stretch}
|
||||
@@ -1780,6 +1781,43 @@ html[data-theme=light] .ph-stats{background:rgba(255,255,255,.5)}
|
||||
|
||||
/* ============ 无边框自定义标题栏(logo + 菜单 + 窗控同一行) ============ */
|
||||
:root{--titlebar-h:40px}
|
||||
html,body,#app{height:100%;overflow:hidden}
|
||||
/* 只有主内容区滚动;侧栏不设 overflow:auto,避免滚动条夹在 rail 中间、裁切铃铛/飞出菜单 */
|
||||
.shell{
|
||||
height:100%;
|
||||
min-height:0;
|
||||
overflow:hidden;
|
||||
}
|
||||
.shell main{
|
||||
margin-top:var(--titlebar-h);
|
||||
height:calc(100% - var(--titlebar-h));
|
||||
min-height:0;
|
||||
overflow-x:hidden;
|
||||
overflow-y:auto;
|
||||
}
|
||||
.shell .sidebar{
|
||||
top:var(--titlebar-h);
|
||||
bottom:0;
|
||||
height:auto;
|
||||
overflow:visible;
|
||||
z-index:30;
|
||||
}
|
||||
.shell .side-sub{
|
||||
min-height:0;
|
||||
overflow-x:hidden;
|
||||
overflow-y:auto;
|
||||
}
|
||||
.shell main::before,.shell main::after{top:var(--titlebar-h)}
|
||||
.with-titlebar,.boot-loading.with-titlebar{padding-top:var(--titlebar-h);height:100%;min-height:0;box-sizing:border-box;overflow:auto}
|
||||
.setup-screen.with-titlebar{height:100%;min-height:0;overflow:auto}
|
||||
|
||||
/* 细滚动条,避免 WebView 默认粗条显得像网页 */
|
||||
.shell main,.shell .side-sub,.bell-list{scrollbar-width:thin;scrollbar-color:color-mix(in srgb,var(--muted) 55%,transparent) transparent}
|
||||
.shell main::-webkit-scrollbar,.shell .side-sub::-webkit-scrollbar,.bell-list::-webkit-scrollbar{width:8px;height:8px}
|
||||
.shell main::-webkit-scrollbar-track,.shell .side-sub::-webkit-scrollbar-track,.bell-list::-webkit-scrollbar-track{background:transparent}
|
||||
.shell main::-webkit-scrollbar-thumb,.shell .side-sub::-webkit-scrollbar-thumb,.bell-list::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--muted) 45%,transparent);border-radius:999px;border:2px solid transparent;background-clip:content-box}
|
||||
.shell main::-webkit-scrollbar-thumb:hover,.shell .side-sub::-webkit-scrollbar-thumb:hover,.bell-list::-webkit-scrollbar-thumb:hover{background:color-mix(in srgb,var(--muted) 70%,transparent);border:2px solid transparent;background-clip:content-box}
|
||||
|
||||
.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;
|
||||
@@ -1828,12 +1866,6 @@ html[data-theme=light] .ph-stats{background:rgba(255,255,255,.5)}
|
||||
.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}
|
||||
|
||||
51
sync.go
51
sync.go
@@ -22,33 +22,48 @@ import (
|
||||
|
||||
const syncTimeout = 10 * time.Second
|
||||
|
||||
// 内置默认连接:本地 MySQL(root/root)的 code_count 库。
|
||||
// 用户零配置即可登录同步;meta 中存过自定义配置时仍以 meta 为准。
|
||||
const (
|
||||
defaultSyncHost = "127.0.0.1"
|
||||
defaultSyncUser = "root"
|
||||
defaultSyncPassword = "root"
|
||||
defaultSyncDatabase = "code_count"
|
||||
)
|
||||
|
||||
// ---------- 配置与状态 ----------
|
||||
|
||||
// syncConfig 解析同步服务器连接。
|
||||
// 启动时会把打包配置写入本地 SQLite meta(见 applyPackagedSyncConfig);
|
||||
// 运行期以 meta 为准,meta 为空时回退到打包嵌入配置。
|
||||
func (a *App) syncConfig() SyncConfig {
|
||||
port, _ := strconv.Atoi(a.store.Meta("sync_port"))
|
||||
if port <= 0 || port > 65535 {
|
||||
port = 3306
|
||||
}
|
||||
c := SyncConfig{
|
||||
Host: a.store.Meta("sync_host"),
|
||||
Host: strings.TrimSpace(a.store.Meta("sync_host")),
|
||||
Port: port,
|
||||
User: a.store.Meta("sync_user"),
|
||||
User: strings.TrimSpace(a.store.Meta("sync_user")),
|
||||
Password: a.store.Meta("sync_password"),
|
||||
Database: a.store.Meta("sync_database"),
|
||||
Database: strings.TrimSpace(a.store.Meta("sync_database")),
|
||||
}
|
||||
if c.Host == "" && c.User == "" && c.Database == "" {
|
||||
c.Host, c.User, c.Password, c.Database = defaultSyncHost, defaultSyncUser, defaultSyncPassword, defaultSyncDatabase
|
||||
if syncConfigComplete(c) {
|
||||
if c.Port <= 0 || c.Port > 65535 {
|
||||
c.Port = 3306
|
||||
}
|
||||
return c
|
||||
}
|
||||
return c
|
||||
pkg := packagedSyncDefaults()
|
||||
if syncConfigComplete(pkg) {
|
||||
return pkg
|
||||
}
|
||||
return SyncConfig{Host: "127.0.0.1", Port: 3306, User: "root", Password: "root", Database: "code_count"}
|
||||
}
|
||||
|
||||
// applyPackagedSyncConfig 启动时用打包配置覆盖本地 SQLite 中的同步连接,
|
||||
// 避免旧安装残留的 127.0.0.1 等 meta 继续生效。
|
||||
func (a *App) applyPackagedSyncConfig() {
|
||||
if a.store == nil {
|
||||
return
|
||||
}
|
||||
pkg := packagedSyncDefaults()
|
||||
if !syncConfigComplete(pkg) {
|
||||
return
|
||||
}
|
||||
_ = a.store.SetMeta("sync_host", strings.TrimSpace(pkg.Host))
|
||||
_ = a.store.SetMeta("sync_port", strconv.Itoa(pkg.Port))
|
||||
_ = a.store.SetMeta("sync_user", strings.TrimSpace(pkg.User))
|
||||
_ = a.store.SetMeta("sync_password", pkg.Password)
|
||||
_ = a.store.SetMeta("sync_database", strings.TrimSpace(pkg.Database))
|
||||
}
|
||||
|
||||
func syncConfigComplete(c SyncConfig) bool {
|
||||
|
||||
23
sync_defaults.go
Normal file
23
sync_defaults.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
//go:embed build/sync.defaults.json
|
||||
var syncDefaultsJSON []byte
|
||||
|
||||
// packagedSyncDefaults 读取打包时嵌入的同步服务器配置。
|
||||
// 修改连接地址请编辑 build/sync.defaults.json 后重新 wails3 task windows:build。
|
||||
func packagedSyncDefaults() SyncConfig {
|
||||
var c SyncConfig
|
||||
if e := json.Unmarshal(syncDefaultsJSON, &c); e != nil {
|
||||
// 嵌入文件损坏时回退到安全的空配置,由调用方再兜底。
|
||||
return SyncConfig{Port: 3306}
|
||||
}
|
||||
if c.Port <= 0 || c.Port > 65535 {
|
||||
c.Port = 3306
|
||||
}
|
||||
return c
|
||||
}
|
||||
@@ -67,17 +67,21 @@ func TestSyncConfigDefaults(t *testing.T) {
|
||||
defer s.db.Close()
|
||||
a := NewApp()
|
||||
a.store = s
|
||||
pkg := packagedSyncDefaults()
|
||||
a.applyPackagedSyncConfig()
|
||||
c := a.syncConfig()
|
||||
if c.Host != defaultSyncHost || c.User != defaultSyncUser || c.Password != defaultSyncPassword || c.Database != defaultSyncDatabase || c.Port != 3306 {
|
||||
t.Fatalf("defaults not applied: %+v", c)
|
||||
}
|
||||
// meta 存过任意自定义配置后,以 meta 为准,不再混入默认值。
|
||||
if e := s.SetMeta("sync_host", "db.example.com"); e != nil {
|
||||
t.Fatal(e)
|
||||
if c.Host != pkg.Host || c.User != pkg.User || c.Password != pkg.Password || c.Database != pkg.Database || c.Port != pkg.Port {
|
||||
t.Fatalf("want packaged defaults written to sqlite: %+v got %+v", pkg, c)
|
||||
}
|
||||
// 启动覆盖后,本地残留的旧地址会被打包配置盖掉。
|
||||
_ = s.SetMeta("sync_host", "127.0.0.1")
|
||||
_ = s.SetMeta("sync_user", "root")
|
||||
_ = s.SetMeta("sync_password", "root")
|
||||
_ = s.SetMeta("sync_database", "code_count")
|
||||
a.applyPackagedSyncConfig()
|
||||
c = a.syncConfig()
|
||||
if c.Host != "db.example.com" || c.User != "" || c.Password != "" || c.Database != "" {
|
||||
t.Fatalf("explicit meta should win: %+v", c)
|
||||
if c.Host != pkg.Host || c.User != pkg.User {
|
||||
t.Fatalf("packaged config must overwrite sqlite meta: %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,61 @@
|
||||
// Applies init.sql to the local MySQL using the app's built-in default connection.
|
||||
// Applies init.sql using the packaged sync defaults (build/sync.defaults.json).
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
type syncDefaults struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Database string `json:"database"`
|
||||
}
|
||||
|
||||
func loadDefaults() syncDefaults {
|
||||
path := filepath.Join("build", "sync.defaults.json")
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var c syncDefaults
|
||||
if err := json.Unmarshal(raw, &c); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if c.Port <= 0 {
|
||||
c.Port = 3306
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func dsn(c syncDefaults, withDB bool) string {
|
||||
db := ""
|
||||
if withDB {
|
||||
db = c.Database
|
||||
}
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?multiStatements=false&charset=utf8mb4",
|
||||
c.User, c.Password, c.Host, c.Port, db)
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadDefaults()
|
||||
if len(os.Args) > 1 && os.Args[1] == "inspect" {
|
||||
inspect()
|
||||
inspect(cfg)
|
||||
return
|
||||
}
|
||||
raw, err := os.ReadFile("init.sql")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
db, err := sql.Open("mysql", "code_count:code_count@tcp(101.43.12.11:3306)/?multiStatements=false&charset=utf8mb4")
|
||||
db, err := sql.Open("mysql", dsn(cfg, false))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -45,11 +81,11 @@ func main() {
|
||||
}
|
||||
}
|
||||
var n int
|
||||
if err := db.QueryRow("SELECT COUNT(*) FROM code_count.sync_settings").Scan(&n); err != nil {
|
||||
if err := db.QueryRow("SELECT COUNT(*) FROM " + cfg.Database + ".sync_settings").Scan(&n); err != nil {
|
||||
fmt.Println("verify failed:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("ok, sync_settings rows:", n)
|
||||
fmt.Println("ok, sync_settings rows:", n, "host:", cfg.Host)
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
@@ -59,8 +95,8 @@ func min(a, b int) int {
|
||||
return b
|
||||
}
|
||||
|
||||
func inspect() {
|
||||
db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/code_count?charset=utf8mb4")
|
||||
func inspect(cfg syncDefaults) {
|
||||
db, err := sql.Open("mysql", dsn(cfg, true))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user