Compare commits

..

1 Commits

Author SHA1 Message Date
SaladDay 0b989bd681 fix(usage): keep date-range picker calendar on-screen in narrow popovers
The custom date-range picker switched to its two-column layout (date
fields | calendar) based on the viewport width via Tailwind `sm:`
(640px). But the popover is clamped to `100vw - 2rem` and anchored to
its trigger with `align="end"`, so its actual available width is not the
viewport width. On narrow windows the wide two-column layout could
activate while the popover only had room for one column, pushing the
calendar column off the right edge of the window where it was clipped
and unusable (month header and 4 of 7 weekday columns cut off).

Gate the two-column layout on the popover's own inline size via a CSS
container query instead of the viewport, and let the popover width
shrink to fit the viewport. When the popover is genuinely wide it shows
two columns; when it is clamped narrow it stacks vertically so the
calendar stays fully visible. The base stacked layout is the safe
fallback for engines without container-query support.

Closes #4669
2026-07-01 08:23:14 +00:00
12 changed files with 40 additions and 501 deletions
+2 -41
View File
@@ -1,15 +1,8 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use tauri::{AppHandle, Emitter}; use tauri::AppHandle;
use tauri_plugin_updater::UpdaterExt; use tauri_plugin_updater::UpdaterExt;
/// 应用更新下载进度(通过 `update-download-progress` 事件发给前端)。
#[derive(Clone, serde::Serialize)]
struct UpdateDownloadProgress {
downloaded: u64,
total: Option<u64>,
}
fn merge_settings_for_save( fn merge_settings_for_save(
mut incoming: crate::settings::AppSettings, mut incoming: crate::settings::AppSettings,
existing: &crate::settings::AppSettings, existing: &crate::settings::AppSettings,
@@ -210,22 +203,8 @@ pub async fn install_update_and_restart(app: AppHandle) -> Result<bool, String>
}; };
log::info!("开始下载应用更新: {}", update.version); log::info!("开始下载应用更新: {}", update.version);
let progress_handle = app.clone();
let mut downloaded: u64 = 0;
let bytes = update let bytes = update
.download( .download(|_, _| {}, || {})
move |chunk_len, content_len| {
downloaded = downloaded.saturating_add(chunk_len as u64);
let _ = progress_handle.emit(
"update-download-progress",
UpdateDownloadProgress {
downloaded,
total: content_len,
},
);
},
|| {},
)
.await .await
.map_err(|e| format!("下载更新失败: {e}"))?; .map_err(|e| format!("下载更新失败: {e}"))?;
@@ -266,24 +245,6 @@ pub async fn install_update_and_restart(app: AppHandle) -> Result<bool, String>
} }
} }
/// 检查是否有可用的应用更新,返回可用的新版本号(无更新时返回 None)。
///
/// 数据库版本过新的恢复界面用它判断:升级应用能否解决问题。若返回 None,说明
/// 已是最新版本,但数据库仍不兼容(通常由第三方客户端或更高版本创建),应提示用户
/// 升级无法解决,而不是让其反复尝试。
#[tauri::command]
pub async fn check_app_update_available(app: AppHandle) -> Result<Option<String>, String> {
let updater = app
.updater_builder()
.build()
.map_err(|e| format!("初始化更新器失败: {e}"))?;
let update = updater
.check()
.await
.map_err(|e| format!("检查更新失败: {e}"))?;
Ok(update.map(|u| u.version))
}
/// 获取 app_config_dir 覆盖配置 (从 Store) /// 获取 app_config_dir 覆盖配置 (从 Store)
#[tauri::command] #[tauri::command]
pub async fn get_app_config_dir_override(app: AppHandle) -> Result<Option<String>, String> { pub async fn get_app_config_dir_override(app: AppHandle) -> Result<Option<String>, String> {
-16
View File
@@ -159,22 +159,6 @@ impl Database {
Ok(db) Ok(db)
} }
/// 读取磁盘上数据库的 `user_version`;仅当它比应用支持的 [`SCHEMA_VERSION`]
/// 更新时返回 `Some(version)`。
///
/// 用于初始化失败后判断是否为「数据库版本过新(应用过旧,需升级应用)」的可恢复
/// 场景——此时不应反复弹出无效的重试对话框,而应引导用户在应用内升级。
pub fn stored_user_version_exceeds_supported(
db_path: &std::path::Path,
) -> Result<Option<i32>, AppError> {
if !db_path.exists() {
return Ok(None);
}
let conn = Connection::open(db_path).map_err(|e| AppError::Database(e.to_string()))?;
let version = Self::get_user_version(&conn)?;
Ok((version > SCHEMA_VERSION).then_some(version))
}
/// 创建内存数据库(用于测试) /// 创建内存数据库(用于测试)
pub fn memory() -> Result<Self, AppError> { pub fn memory() -> Result<Self, AppError> {
let conn = Connection::open_in_memory().map_err(|e| AppError::Database(e.to_string()))?; let conn = Connection::open_in_memory().map_err(|e| AppError::Database(e.to_string()))?;
+1 -14
View File
@@ -5,17 +5,6 @@ use std::sync::{OnceLock, RwLock};
pub struct InitErrorPayload { pub struct InitErrorPayload {
pub path: String, pub path: String,
pub error: String, pub error: String,
/// 错误类别。`Some("db_version_too_new")` 表示数据库版本过新(应用过旧),
/// 前端据此展示「升级应用」恢复界面而非直接退出。
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
/// 磁盘上数据库的 user_version(数据库版本过新时填充)
#[serde(skip_serializing_if = "Option::is_none")]
pub db_version: Option<i32>,
/// 当前应用支持的 SCHEMA_VERSION(数据库版本过新时填充)。
/// 当升级到最新版后 db_version 仍 > supported_version,说明可能由第三方客户端创建。
#[serde(skip_serializing_if = "Option::is_none")]
pub supported_version: Option<i32>,
} }
static INIT_ERROR: OnceLock<RwLock<Option<InitErrorPayload>>> = OnceLock::new(); static INIT_ERROR: OnceLock<RwLock<Option<InitErrorPayload>>> = OnceLock::new();
@@ -24,6 +13,7 @@ fn cell() -> &'static RwLock<Option<InitErrorPayload>> {
INIT_ERROR.get_or_init(|| RwLock::new(None)) INIT_ERROR.get_or_init(|| RwLock::new(None))
} }
#[allow(dead_code)]
pub fn set_init_error(payload: InitErrorPayload) { pub fn set_init_error(payload: InitErrorPayload) {
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
if let Ok(mut guard) = cell().write() { if let Ok(mut guard) = cell().write() {
@@ -112,9 +102,6 @@ mod tests {
let payload = InitErrorPayload { let payload = InitErrorPayload {
path: "/tmp/config.json".into(), path: "/tmp/config.json".into(),
error: "broken json".into(), error: "broken json".into(),
kind: None,
db_version: None,
supported_version: None,
}; };
set_init_error(payload.clone()); set_init_error(payload.clone());
let got = get_init_error().expect("should get payload back"); let got = get_init_error().expect("should get payload back");
-40
View File
@@ -270,16 +270,6 @@ pub fn run() {
// 拦截窗口关闭:根据设置决定是否最小化到托盘 // 拦截窗口关闭:根据设置决定是否最小化到托盘
.on_window_event(|window, event| { .on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event { if let tauri::WindowEvent::CloseRequested { api, .. } = event {
// 数据库版本过新的恢复模式下没有托盘可唤回,关闭即退出,避免应用隐身后台
let in_db_recovery = crate::init_status::get_init_error()
.map(|p| p.kind.as_deref() == Some("db_version_too_new"))
.unwrap_or(false);
if in_db_recovery {
api.prevent_close();
window.app_handle().exit(0);
return;
}
let settings = crate::settings::get_settings(); let settings = crate::settings::get_settings();
if settings.minimize_to_tray_on_close { if settings.minimize_to_tray_on_close {
@@ -413,35 +403,6 @@ pub fn run() {
// 说明:从 v3.8.* 升级的用户通常会走到这里的 SQLite schema 迁移, // 说明:从 v3.8.* 升级的用户通常会走到这里的 SQLite schema 迁移,
// 若迁移失败(数据库损坏/权限不足/user_version 过新等),需要给用户明确提示, // 若迁移失败(数据库损坏/权限不足/user_version 过新等),需要给用户明确提示,
// 否则表现可能只是“应用打不开/闪退”。 // 否则表现可能只是“应用打不开/闪退”。
//
// 预检:数据库版本过新时,必须先于任何 schema 写操作(create_tables 内含
// DROP/ALTER 等 DDL)进入恢复界面,避免旧应用对读不懂的更新版 DB 落写。
match crate::database::Database::stored_user_version_exceeds_supported(&db_path) {
Ok(Some(version)) => {
log::warn!("数据库版本过新(v{version}),引导用户在应用内升级应用");
crate::init_status::set_init_error(crate::init_status::InitErrorPayload {
path: db_path.display().to_string(),
error: format!(
"数据库版本过新({version}),当前应用仅支持 {},请升级应用后再尝试。",
crate::database::SCHEMA_VERSION
),
kind: Some("db_version_too_new".to_string()),
db_version: Some(version),
supported_version: Some(crate::database::SCHEMA_VERSION),
});
// 主窗口默认 visible:false,恢复界面必须强制显示
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
return Ok(());
}
Ok(None) => {}
Err(e) => {
log::warn!("预检数据库版本失败,继续正常初始化流程: {e}");
}
}
let db = loop { let db = loop {
match crate::database::Database::init() { match crate::database::Database::init() {
Ok(db) => break Arc::new(db), Ok(db) => break Arc::new(db),
@@ -1226,7 +1187,6 @@ pub fn run() {
commands::set_log_config, commands::set_log_config,
commands::restart_app, commands::restart_app,
commands::install_update_and_restart, commands::install_update_and_restart,
commands::check_app_update_available,
commands::check_for_updates, commands::check_for_updates,
commands::is_portable_mode, commands::is_portable_mode,
commands::copy_text_to_clipboard, commands::copy_text_to_clipboard,
-303
View File
@@ -1,303 +0,0 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { exit } from "@tauri-apps/plugin-process";
import {
Database,
Download,
RefreshCw,
ExternalLink,
FolderOpen,
Loader2,
AlertTriangle,
} from "lucide-react";
import { Button } from "@/components/ui/button";
const RELEASES_URL = "https://github.com/farion1231/cc-switch/releases";
interface DatabaseUpgradeProps {
payload: {
path?: string;
error?: string;
kind?: string;
db_version?: number;
supported_version?: number;
};
}
// checking: 启动时检查是否有可用更新
// upgradable: 有可用更新,升级应用即可解决
// incompatible: 已是最新版本但数据库仍过新(可能来自第三方客户端),升级无法解决
// updating: 正在下载/安装更新
// error: 升级过程出错
type Phase = "checking" | "upgradable" | "incompatible" | "updating" | "error";
interface DownloadProgress {
downloaded: number;
total: number | null;
}
/**
* 数据库版本过新(应用过旧)时的应用内恢复界面。
*
* 启动时先检查是否有可用更新:
* - 有 → 提供「升级应用」一键下载+安装+重启,并展示下载进度条。
* - 无 → 说明当前已是最新版本但数据库仍不兼容(通常由第三方客户端或更高版本创建),
* 升级无法解决,及时提醒用户备份后改用兼容客户端或等待官方支持。
*/
export function DatabaseUpgrade({ payload }: DatabaseUpgradeProps) {
const { t } = useTranslation();
const [phase, setPhase] = useState<Phase>("checking");
const [availableVersion, setAvailableVersion] = useState<string | null>(null);
const [progress, setProgress] = useState<DownloadProgress | null>(null);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const unlistenRef = useRef<(() => void) | null>(null);
const dbVersion = payload.db_version;
const supportedVersion = payload.supported_version;
// 启动时检查可用更新,决定 upgradable / incompatible
useEffect(() => {
let cancelled = false;
(async () => {
try {
const version = await invoke<string | null>(
"check_app_update_available",
);
if (cancelled) return;
if (version) {
setAvailableVersion(version);
setPhase("upgradable");
} else {
setPhase("incompatible");
}
} catch {
// 检查失败(如离线):仍允许尝试升级,避免完全卡死
if (!cancelled) setPhase("upgradable");
}
})();
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
return () => {
unlistenRef.current?.();
};
}, []);
const startUpgrade = useCallback(async () => {
setPhase("updating");
setProgress(null);
setErrorMsg(null);
try {
unlistenRef.current?.();
unlistenRef.current = await listen<DownloadProgress>(
"update-download-progress",
(e) => setProgress(e.payload),
);
// 成功时后端会下载+安装+重启,不会返回;返回 false 表示无可用更新。
const updating = await invoke<boolean>("install_update_and_restart");
unlistenRef.current?.();
unlistenRef.current = null;
if (!updating) {
// 竞态:检查时有更新、安装时已无 → 按不兼容处理
setPhase("incompatible");
}
// updating === true:应用即将重启,保持 updating 态直到进程退出。
} catch (e) {
unlistenRef.current?.();
unlistenRef.current = null;
setErrorMsg(e instanceof Error ? e.message : String(e));
setPhase("error");
}
}, []);
const percent =
progress && progress.total && progress.total > 0
? Math.min(100, Math.round((progress.downloaded / progress.total) * 100))
: null;
const fmtMB = (n: number) => (n / 1024 / 1024).toFixed(1);
const isIncompatible = phase === "incompatible";
const accent = isIncompatible
? {
chip: "bg-red-100 text-red-600 dark:bg-red-950/50 dark:text-red-400",
Icon: AlertTriangle,
}
: {
chip: "bg-amber-100 text-amber-600 dark:bg-amber-950/50 dark:text-amber-400",
Icon: Database,
};
const AccentIcon = accent.Icon;
return (
<div className="flex min-h-screen items-center justify-center bg-background p-6 text-foreground">
<div className="w-full max-w-lg space-y-5 rounded-2xl border border-border/60 bg-card/80 p-7 shadow-xl">
<div className="flex items-start gap-4">
<div
className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-xl ${accent.chip}`}
>
<AccentIcon className="h-6 w-6" />
</div>
<div className="space-y-1">
<h1 className="text-lg font-semibold">
{t("dbUpgrade.title", "数据库版本过新")}
</h1>
<p className="text-sm text-muted-foreground">
{t(
"dbUpgrade.description",
"当前数据库由更新版本的 CC Switch 创建,需要升级应用后才能继续使用。升级不会删除你的数据。",
)}
</p>
{dbVersion != null && supportedVersion != null && (
<p className="pt-0.5 text-xs text-muted-foreground tabular-nums">
{t("dbUpgrade.versionInfo", {
db: dbVersion,
supported: supportedVersion,
defaultValue: "数据库版本 v{{db}} · 应用支持 v{{supported}}",
})}
</p>
)}
</div>
</div>
{/* 错误详情 / 数据库路径 */}
<div className="space-y-1 rounded-lg border border-border/50 bg-muted/40 p-3 text-xs text-muted-foreground">
{payload.error && (
<p className="break-words font-mono">{payload.error}</p>
)}
{payload.path && (
<p className="break-all">
{t("dbUpgrade.dbPath", "数据库文件")}{payload.path}
</p>
)}
</div>
{phase === "checking" && (
<p className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
{t("dbUpgrade.checking", "正在检查可用更新…")}
</p>
)}
{phase === "upgradable" && availableVersion && (
<p className="text-sm text-muted-foreground">
{t("dbUpgrade.updateAvailable", {
version: availableVersion,
defaultValue: "发现新版本 v{{version}},升级后即可继续使用。",
})}
</p>
)}
{phase === "incompatible" && (
<div className="space-y-2 rounded-lg border border-red-300/60 bg-red-50 p-3 text-sm text-red-700 dark:border-red-500/40 dark:bg-red-950/40 dark:text-red-300">
<p className="font-medium">
{t("dbUpgrade.incompatibleTitle", "升级也无法解决")}
</p>
<p className="leading-relaxed">
{t("dbUpgrade.incompatibleDescription", {
db: dbVersion,
supported: supportedVersion,
defaultValue:
"你已是最新版本,但数据库版本(v{{db}})仍高于本应用支持的版本(v{{supported}})。该数据库可能由第三方客户端或更高版本创建,升级当前官方应用也无法兼容。",
})}
</p>
</div>
)}
{phase === "updating" && (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="flex items-center gap-2 text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
{percent === null
? t("dbUpgrade.preparing", "正在准备更新…")
: t("dbUpgrade.downloading", "正在下载更新…")}
</span>
{percent !== null && (
<span className="tabular-nums text-muted-foreground">
{percent}%
</span>
)}
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-muted">
<div
className={`h-full rounded-full bg-amber-500 transition-all duration-200 ${
percent === null ? "w-1/3 animate-pulse" : ""
}`}
style={percent === null ? undefined : { width: `${percent}%` }}
/>
</div>
{progress && (
<p className="text-right text-xs tabular-nums text-muted-foreground">
{fmtMB(progress.downloaded)} MB
{progress.total ? ` / ${fmtMB(progress.total)} MB` : ""}
</p>
)}
</div>
)}
{phase === "error" && errorMsg && (
<p className="rounded-lg border border-red-300/60 bg-red-50 p-3 text-sm text-red-700 dark:border-red-500/40 dark:bg-red-950/40 dark:text-red-300">
{errorMsg}
</p>
)}
<div className="flex flex-wrap items-center gap-2">
{(phase === "upgradable" || phase === "error") && (
<Button
onClick={startUpgrade}
className="gap-2 bg-amber-500 text-white hover:bg-amber-600"
>
{phase === "error" ? (
<RefreshCw className="h-4 w-4" />
) : (
<Download className="h-4 w-4" />
)}
{phase === "error"
? t("dbUpgrade.retry", "重试升级")
: t("dbUpgrade.upgradeNow", "升级应用")}
</Button>
)}
{(phase === "incompatible" || phase === "error") && (
<Button
variant="outline"
className="gap-2"
onClick={() =>
void invoke("open_external", { url: RELEASES_URL })
}
>
<ExternalLink className="h-4 w-4" />
{t("dbUpgrade.openReleases", "打开发布页")}
</Button>
)}
<Button
variant="outline"
className="gap-2"
onClick={() => void invoke("open_app_config_folder")}
disabled={phase === "updating"}
>
<FolderOpen className="h-4 w-4" />
{t("dbUpgrade.openConfigDir", "打开配置目录")}
</Button>
<Button
variant="ghost"
className="ml-auto text-muted-foreground"
onClick={() => void exit(0)}
disabled={phase === "updating"}
>
{t("dbUpgrade.quit", "退出")}
</Button>
</div>
</div>
</div>
);
}
export default DatabaseUpgrade;
@@ -331,7 +331,7 @@ export function UsageDateRangePicker({
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
className="w-[340px] max-w-[calc(100vw-2rem)] p-3 sm:w-[620px]" className="usage-range-popover w-[620px] max-w-[calc(100vw-2rem)] p-3"
align="end" align="end"
> >
{/* Preset shortcuts */} {/* Preset shortcuts */}
@@ -353,9 +353,9 @@ export function UsageDateRangePicker({
))} ))}
</div> </div>
<div className="flex flex-col gap-3 sm:flex-row"> <div className="usage-range-layout flex flex-col gap-3">
{/* Left: date fields */} {/* Left: date fields */}
<div className="space-y-2 sm:w-[250px] sm:flex-none"> <div className="usage-range-fields space-y-2">
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{t("usage.customRangeHint", "支持日期与时间,最长 30 天")} {t("usage.customRangeHint", "支持日期与时间,最长 30 天")}
</p> </p>
@@ -403,7 +403,7 @@ export function UsageDateRangePicker({
</div> </div>
{/* Right: calendar */} {/* Right: calendar */}
<div className="rounded-lg border border-border/50 bg-muted/30 p-2.5 sm:min-w-0 sm:flex-1"> <div className="usage-range-calendar rounded-lg border border-border/50 bg-muted/30 p-2.5">
{/* Month navigation */} {/* Month navigation */}
<div className="flex items-center justify-between mb-1.5"> <div className="flex items-center justify-between mb-1.5">
<Button <Button
-17
View File
@@ -1610,23 +1610,6 @@
"tip2": "• Extractor function runs in sandbox environment, supports ES2020+ syntax", "tip2": "• Extractor function runs in sandbox environment, supports ES2020+ syntax",
"tip3": "• Entire config must be wrapped in () to form object literal expression" "tip3": "• Entire config must be wrapped in () to form object literal expression"
}, },
"dbUpgrade": {
"title": "Database version is too new",
"description": "This database was created by a newer version of CC Switch. Upgrade the app to continue. Upgrading will not delete your data.",
"versionInfo": "Database v{{db}} · App supports v{{supported}}",
"dbPath": "Database file",
"checking": "Checking for available updates…",
"updateAvailable": "Version v{{version}} is available; upgrading will let you continue.",
"incompatibleTitle": "Upgrading won't fix this",
"incompatibleDescription": "You are already on the latest version, but the database (v{{db}}) is still newer than this app supports (v{{supported}}). It was likely created by a third-party client or a newer build, so upgrading the official app cannot make it compatible.",
"preparing": "Preparing update…",
"downloading": "Downloading update…",
"upgradeNow": "Upgrade app",
"retry": "Retry upgrade",
"openReleases": "Open releases page",
"openConfigDir": "Open config folder",
"quit": "Quit"
},
"errors": { "errors": {
"usage_query_failed": "Usage query failed", "usage_query_failed": "Usage query failed",
"configLoadFailedTitle": "Configuration Load Failed", "configLoadFailedTitle": "Configuration Load Failed",
-17
View File
@@ -1610,23 +1610,6 @@
"tip2": "• 抽出関数はサンドボックスで実行され、ES2020+ の構文を使えます", "tip2": "• 抽出関数はサンドボックスで実行され、ES2020+ の構文を使えます",
"tip3": "• 全体を () で囲み、オブジェクトリテラル式にしてください" "tip3": "• 全体を () で囲み、オブジェクトリテラル式にしてください"
}, },
"dbUpgrade": {
"title": "データベースのバージョンが新しすぎます",
"description": "このデータベースは新しいバージョンの CC Switch で作成されています。続行するにはアプリを更新してください。更新してもデータは削除されません。",
"versionInfo": "データベース v{{db}} · アプリ対応 v{{supported}}",
"dbPath": "データベースファイル",
"checking": "利用可能な更新を確認中…",
"updateAvailable": "新しいバージョン v{{version}} が利用可能です。更新すると続行できます。",
"incompatibleTitle": "更新しても解決できません",
"incompatibleDescription": "すでに最新バージョンですが、データベース(v{{db}})はこのアプリの対応バージョン(v{{supported}})より新しいままです。サードパーティ製クライアントまたはより新しいビルドで作成された可能性があり、公式アプリを更新しても互換性は得られません。",
"preparing": "更新を準備中…",
"downloading": "更新をダウンロード中…",
"upgradeNow": "アプリを更新",
"retry": "更新を再試行",
"openReleases": "リリースページを開く",
"openConfigDir": "設定フォルダを開く",
"quit": "終了"
},
"errors": { "errors": {
"usage_query_failed": "利用状況の取得に失敗しました", "usage_query_failed": "利用状況の取得に失敗しました",
"configLoadFailedTitle": "設定の読み込みに失敗しました", "configLoadFailedTitle": "設定の読み込みに失敗しました",
-17
View File
@@ -1582,23 +1582,6 @@
"tip2": "• extractor 函式在沙盒環境中執行,支援 ES2020+ 語法", "tip2": "• extractor 函式在沙盒環境中執行,支援 ES2020+ 語法",
"tip3": "• 整個設定必須用 () 包覆,形成物件實字運算式" "tip3": "• 整個設定必須用 () 包覆,形成物件實字運算式"
}, },
"dbUpgrade": {
"title": "資料庫版本過新",
"description": "目前資料庫由較新版本的 CC Switch 建立,需要升級應用後才能繼續使用。升級不會刪除你的資料。",
"versionInfo": "資料庫版本 v{{db}} · 應用支援 v{{supported}}",
"dbPath": "資料庫檔案",
"checking": "正在檢查可用更新…",
"updateAvailable": "發現新版本 v{{version}},升級後即可繼續使用。",
"incompatibleTitle": "升級也無法解決",
"incompatibleDescription": "你已是最新版本,但資料庫版本(v{{db}})仍高於本應用支援的版本(v{{supported}})。該資料庫可能由第三方用戶端或更高版本建立,升級目前官方應用也無法相容。",
"preparing": "正在準備更新…",
"downloading": "正在下載更新…",
"upgradeNow": "升級應用",
"retry": "重試升級",
"openReleases": "開啟發佈頁",
"openConfigDir": "開啟設定目錄",
"quit": "結束"
},
"errors": { "errors": {
"usage_query_failed": "用量查詢失敗", "usage_query_failed": "用量查詢失敗",
"configLoadFailedTitle": "設定載入失敗", "configLoadFailedTitle": "設定載入失敗",
-17
View File
@@ -1610,23 +1610,6 @@
"tip2": "• extractor 函数在沙箱环境中执行,支持 ES2020+ 语法", "tip2": "• extractor 函数在沙箱环境中执行,支持 ES2020+ 语法",
"tip3": "• 整个配置必须用 () 包裹,形成对象字面量表达式" "tip3": "• 整个配置必须用 () 包裹,形成对象字面量表达式"
}, },
"dbUpgrade": {
"title": "数据库版本过新",
"description": "当前数据库由更新版本的 CC Switch 创建,需要升级应用后才能继续使用。升级不会删除你的数据。",
"versionInfo": "数据库版本 v{{db}} · 应用支持 v{{supported}}",
"dbPath": "数据库文件",
"checking": "正在检查可用更新…",
"updateAvailable": "发现新版本 v{{version}},升级后即可继续使用。",
"incompatibleTitle": "升级也无法解决",
"incompatibleDescription": "你已是最新版本,但数据库版本(v{{db}})仍高于本应用支持的版本(v{{supported}})。该数据库可能由第三方客户端或更高版本创建,升级当前官方应用也无法兼容。",
"preparing": "正在准备更新…",
"downloading": "正在下载更新…",
"upgradeNow": "升级应用",
"retry": "重试升级",
"openReleases": "打开发布页",
"openConfigDir": "打开配置目录",
"quit": "退出"
},
"errors": { "errors": {
"usage_query_failed": "用量查询失败", "usage_query_failed": "用量查询失败",
"configLoadFailedTitle": "配置加载失败", "configLoadFailedTitle": "配置加载失败",
+33
View File
@@ -191,3 +191,36 @@ input[type="password"]::-ms-reveal,
input[type="password"]::-ms-clear { input[type="password"]::-ms-clear {
display: none; display: none;
} }
/*
* Usage date-range picker popover.
*
* The two-column layout (date fields | calendar) must depend on how much
* width the popover itself actually has — not on the viewport. The popover is
* clamped to `100vw - 2rem` and anchored to its trigger, so a viewport-based
* breakpoint (e.g. Tailwind `sm:`) could enable the wide layout while the
* popover only had room for one column, pushing the calendar off-screen.
*
* A container query keys the layout to the popover's own inline size. The base
* (stacked) layout is the safe fallback when container queries are
* unsupported, keeping the calendar fully visible at any width.
*/
.usage-range-popover {
container-type: inline-size;
}
@container (min-width: 500px) {
.usage-range-layout {
flex-direction: row;
}
.usage-range-fields {
width: 250px;
flex: none;
}
.usage-range-calendar {
min-width: 0;
flex: 1 1 0%;
}
}
-15
View File
@@ -1,7 +1,6 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import App from "./App"; import App from "./App";
import { DatabaseUpgrade } from "./components/DatabaseUpgrade";
import { UpdateProvider } from "./contexts/UpdateContext"; import { UpdateProvider } from "./contexts/UpdateContext";
import "./index.css"; import "./index.css";
// 导入国际化配置 // 导入国际化配置
@@ -31,8 +30,6 @@ try {
interface ConfigLoadErrorPayload { interface ConfigLoadErrorPayload {
path?: string; path?: string;
error?: string; error?: string;
/** "db_version_too_new" 表示数据库版本过新,渲染应用内升级恢复界面 */
kind?: string;
} }
/** /**
@@ -79,18 +76,6 @@ async function bootstrap() {
const initError = (await invoke( const initError = (await invoke(
"get_init_error", "get_init_error",
)) as ConfigLoadErrorPayload | null; )) as ConfigLoadErrorPayload | null;
if (initError && initError.kind === "db_version_too_new") {
// 数据库版本过新:渲染应用内「升级应用」恢复界面,不进入正常 App
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ThemeProvider defaultTheme="system" storageKey="cc-switch-theme">
<DatabaseUpgrade payload={initError} />
<Toaster />
</ThemeProvider>
</React.StrictMode>,
);
return;
}
if (initError && (initError.path || initError.error)) { if (initError && (initError.path || initError.error)) {
await handleConfigLoadError(initError); await handleConfigLoadError(initError);
// 注意:不会执行到这里,因为 exit(1) 会终止进程 // 注意:不会执行到这里,因为 exit(1) 会终止进程