itismyskillmarket 1.3.44 → 1.3.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ALNUNP4E.js → chunk-ARRJETWL.js} +120 -39
- package/dist/electron-entry.js +1 -1
- package/dist/index.js +8 -10
- package/gui/app.js +42 -0
- package/gui/index.html +19 -6
- package/gui/style.css +175 -3
- package/package.json +1 -1
|
@@ -42,29 +42,63 @@ var fileScopes = fileConfig.npmScopes ? fileConfig.npmScopes.split(",").map((s)
|
|
|
42
42
|
var SKILL_SCOPES = process.env.SKM_NPM_SCOPES ? process.env.SKM_NPM_SCOPES.split(",").map((s) => s.trim()).filter(Boolean) : fileScopes || DEFAULT_SCOPES;
|
|
43
43
|
var SKM_URL = process.env.SKM_URL || fileConfig.skmUrl || `https://www.npmjs.com/package/${NPM_SCOPE}`;
|
|
44
44
|
|
|
45
|
-
// src/
|
|
46
|
-
var
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
// src/utils/cache.ts
|
|
46
|
+
var TtlCache = class {
|
|
47
|
+
store = /* @__PURE__ */ new Map();
|
|
48
|
+
/**
|
|
49
|
+
* 获取缓存值。如果 key 不存在或已过期,返回 null。
|
|
50
|
+
* 过期条目会被惰性删除。
|
|
51
|
+
*/
|
|
52
|
+
get(key) {
|
|
53
|
+
const entry = this.store.get(key);
|
|
54
|
+
if (!entry) return null;
|
|
55
|
+
if (Date.now() > entry.expiry) {
|
|
56
|
+
this.store.delete(key);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return entry.data;
|
|
53
60
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
/**
|
|
62
|
+
* 设置缓存值
|
|
63
|
+
*
|
|
64
|
+
* @param key - 缓存键
|
|
65
|
+
* @param data - 缓存数据
|
|
66
|
+
* @param ttlMs - 生存时间(毫秒),默认 30 秒
|
|
67
|
+
*/
|
|
68
|
+
set(key, data, ttlMs = 3e4) {
|
|
69
|
+
this.store.set(key, { data, expiry: Date.now() + ttlMs });
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 删除指定的缓存条目
|
|
73
|
+
*/
|
|
74
|
+
delete(key) {
|
|
75
|
+
this.store.delete(key);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 清空所有缓存
|
|
79
|
+
*/
|
|
80
|
+
clear() {
|
|
81
|
+
this.store.clear();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 当前缓存条目数量
|
|
85
|
+
*/
|
|
86
|
+
get size() {
|
|
87
|
+
return this.store.size;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var cache = new TtlCache();
|
|
91
|
+
|
|
92
|
+
// src/commands/npm.ts
|
|
59
93
|
async function fetchNpmPackage(packageName, retries = 1) {
|
|
60
94
|
const cacheKey = `pkg:${packageName}`;
|
|
61
|
-
const cached =
|
|
95
|
+
const cached = cache.get(cacheKey);
|
|
62
96
|
if (cached) return cached;
|
|
63
97
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
64
98
|
try {
|
|
65
99
|
const result = await fetchNpmPackageOnce(packageName);
|
|
66
100
|
if (result !== null) {
|
|
67
|
-
|
|
101
|
+
cache.set(cacheKey, result);
|
|
68
102
|
return result;
|
|
69
103
|
}
|
|
70
104
|
} catch {
|
|
@@ -311,8 +345,7 @@ var BaseAdapter = class {
|
|
|
311
345
|
}
|
|
312
346
|
async isAvailable() {
|
|
313
347
|
try {
|
|
314
|
-
|
|
315
|
-
return true;
|
|
348
|
+
return fs3.pathExists(this.skillDir);
|
|
316
349
|
} catch {
|
|
317
350
|
return false;
|
|
318
351
|
}
|
|
@@ -687,6 +720,54 @@ import { exec } from "child_process";
|
|
|
687
720
|
import { promisify } from "util";
|
|
688
721
|
import * as tar from "tar";
|
|
689
722
|
var execAsync = promisify(exec);
|
|
723
|
+
var CACHE_MAX_ENTRIES = 50;
|
|
724
|
+
var CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
725
|
+
async function cleanupCache() {
|
|
726
|
+
try {
|
|
727
|
+
const cacheDir = getCacheDir();
|
|
728
|
+
if (!await fs11.pathExists(cacheDir)) return;
|
|
729
|
+
const entries = await fs11.readdir(cacheDir, { withFileTypes: true });
|
|
730
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => ({ name: e.name, path: path10.join(cacheDir, e.name) }));
|
|
731
|
+
if (dirs.length <= CACHE_MAX_ENTRIES) {
|
|
732
|
+
const now = Date.now();
|
|
733
|
+
let removed = 0;
|
|
734
|
+
for (const dir of dirs) {
|
|
735
|
+
try {
|
|
736
|
+
const stat = await fs11.stat(dir.path);
|
|
737
|
+
if (now - stat.mtimeMs > CACHE_MAX_AGE_MS) {
|
|
738
|
+
await fs11.remove(dir.path);
|
|
739
|
+
removed++;
|
|
740
|
+
}
|
|
741
|
+
} catch {
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (removed > 0) {
|
|
745
|
+
console.log(` Cache cleanup: removed ${removed} expired entr${removed > 1 ? "ies" : "y"}`);
|
|
746
|
+
}
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const withMtime = await Promise.all(
|
|
750
|
+
dirs.map(async (dir) => {
|
|
751
|
+
try {
|
|
752
|
+
const stat = await fs11.stat(dir.path);
|
|
753
|
+
return { ...dir, mtime: stat.mtimeMs };
|
|
754
|
+
} catch {
|
|
755
|
+
return { ...dir, mtime: 0 };
|
|
756
|
+
}
|
|
757
|
+
})
|
|
758
|
+
);
|
|
759
|
+
withMtime.sort((a, b) => b.mtime - a.mtime);
|
|
760
|
+
const toRemove = withMtime.slice(CACHE_MAX_ENTRIES);
|
|
761
|
+
for (const dir of toRemove) {
|
|
762
|
+
try {
|
|
763
|
+
await fs11.remove(dir.path);
|
|
764
|
+
} catch {
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
console.log(` Cache cleanup: removed ${toRemove.length} old entr${toRemove.length > 1 ? "ies" : "y"}`);
|
|
768
|
+
} catch {
|
|
769
|
+
}
|
|
770
|
+
}
|
|
690
771
|
async function installSkill(skillId, version, options) {
|
|
691
772
|
await ensureMarketDirs();
|
|
692
773
|
let targetVersion;
|
|
@@ -733,6 +814,7 @@ async function installSkill(skillId, version, options) {
|
|
|
733
814
|
} catch (err) {
|
|
734
815
|
throw new Error(`Failed to download package: ${err}`);
|
|
735
816
|
}
|
|
817
|
+
await cleanupCache();
|
|
736
818
|
}
|
|
737
819
|
pkgRoot = targetDir;
|
|
738
820
|
}
|
|
@@ -1872,18 +1954,16 @@ var MAX_UPLOAD_SIZE = 50 * 1024 * 1024;
|
|
|
1872
1954
|
var __filename = fileURLToPath2(import.meta.url);
|
|
1873
1955
|
var __dirname = dirname(__filename);
|
|
1874
1956
|
var guiDir = join3(__dirname, "..", "gui");
|
|
1875
|
-
var
|
|
1876
|
-
function
|
|
1877
|
-
|
|
1878
|
-
if (
|
|
1879
|
-
|
|
1880
|
-
cache.delete(key);
|
|
1881
|
-
return null;
|
|
1882
|
-
}
|
|
1883
|
-
return entry.data;
|
|
1957
|
+
var uiCache = new TtlCache();
|
|
1958
|
+
function getAuthorName(author) {
|
|
1959
|
+
if (!author) return "";
|
|
1960
|
+
if (typeof author === "string") return author.replace(/<[^>]*>/g, "").trim();
|
|
1961
|
+
return author.name || "";
|
|
1884
1962
|
}
|
|
1885
|
-
function
|
|
1886
|
-
|
|
1963
|
+
function getRepoUrl(repo) {
|
|
1964
|
+
if (!repo) return "";
|
|
1965
|
+
if (typeof repo === "string") return repo;
|
|
1966
|
+
return repo.url || "";
|
|
1887
1967
|
}
|
|
1888
1968
|
async function throttledMap(items, fn, concurrency = 3) {
|
|
1889
1969
|
const results = [];
|
|
@@ -1943,7 +2023,7 @@ API_ROUTES.GET["/api/skills"] = async (_req, res, url) => {
|
|
|
1943
2023
|
const sort = url.searchParams.get("sort") || "name";
|
|
1944
2024
|
const platform = url.searchParams.get("platform") || "";
|
|
1945
2025
|
const cacheKey = `search:${search}:limit:${limit}`;
|
|
1946
|
-
let searchResult =
|
|
2026
|
+
let searchResult = uiCache.get(cacheKey);
|
|
1947
2027
|
if (!searchResult) {
|
|
1948
2028
|
searchResult = await searchSkillmarketPackages({
|
|
1949
2029
|
from: 0,
|
|
@@ -1951,17 +2031,17 @@ API_ROUTES.GET["/api/skills"] = async (_req, res, url) => {
|
|
|
1951
2031
|
// 一次拉取更多,避免分页
|
|
1952
2032
|
keyword: search || void 0
|
|
1953
2033
|
});
|
|
1954
|
-
|
|
2034
|
+
uiCache.set(cacheKey, searchResult, 3e4);
|
|
1955
2035
|
}
|
|
1956
2036
|
const { packages, total } = searchResult;
|
|
1957
2037
|
let fetchErrors = 0;
|
|
1958
2038
|
const skillDetails = await throttledMap(packages, async (pkgName) => {
|
|
1959
2039
|
try {
|
|
1960
2040
|
const pkgCacheKey = `pkg:${pkgName}`;
|
|
1961
|
-
let info =
|
|
2041
|
+
let info = uiCache.get(pkgCacheKey);
|
|
1962
2042
|
if (!info) {
|
|
1963
2043
|
info = await fetchNpmPackage(pkgName);
|
|
1964
|
-
if (info)
|
|
2044
|
+
if (info) uiCache.set(pkgCacheKey, info, 3e4);
|
|
1965
2045
|
}
|
|
1966
2046
|
if (!info) {
|
|
1967
2047
|
fetchErrors++;
|
|
@@ -1977,9 +2057,9 @@ API_ROUTES.GET["/api/skills"] = async (_req, res, url) => {
|
|
|
1977
2057
|
version: latestVersion,
|
|
1978
2058
|
description: pkg?.description || "",
|
|
1979
2059
|
platforms: meta?.platforms || [],
|
|
1980
|
-
author: info.author
|
|
2060
|
+
author: getAuthorName(info.author) || getAuthorName(pkg?.author),
|
|
1981
2061
|
homepage: pkg?.homepage || "",
|
|
1982
|
-
repository: pkg?.repository
|
|
2062
|
+
repository: getRepoUrl(pkg?.repository),
|
|
1983
2063
|
updated: info.time?.[latestVersion] || info.time?.modified || ""
|
|
1984
2064
|
};
|
|
1985
2065
|
} catch {
|
|
@@ -2095,10 +2175,10 @@ API_ROUTES.GET["/api/skill-info"] = async (_req, res, url) => {
|
|
|
2095
2175
|
return;
|
|
2096
2176
|
}
|
|
2097
2177
|
const cacheKey = `skill-info:${skillName}`;
|
|
2098
|
-
let info =
|
|
2178
|
+
let info = uiCache.get(cacheKey);
|
|
2099
2179
|
if (!info) {
|
|
2100
2180
|
info = await fetchSkillPackage(skillName);
|
|
2101
|
-
if (info)
|
|
2181
|
+
if (info) uiCache.set(cacheKey, info, 3e4);
|
|
2102
2182
|
}
|
|
2103
2183
|
if (!info) {
|
|
2104
2184
|
jsonResponse(res, 404, { error: `Skill "${skillName}" not found` });
|
|
@@ -2117,10 +2197,10 @@ API_ROUTES.GET["/api/skill-info"] = async (_req, res, url) => {
|
|
|
2117
2197
|
version: latestVersion,
|
|
2118
2198
|
platforms: meta?.platforms || [],
|
|
2119
2199
|
versions: recentVersions,
|
|
2120
|
-
author: info.author
|
|
2200
|
+
author: getAuthorName(info.author) || getAuthorName(pkg?.author),
|
|
2121
2201
|
license: info.license || "",
|
|
2122
2202
|
homepage: pkg?.homepage || "",
|
|
2123
|
-
repository: pkg?.repository
|
|
2203
|
+
repository: getRepoUrl(pkg?.repository),
|
|
2124
2204
|
readme: info.readme || ""
|
|
2125
2205
|
});
|
|
2126
2206
|
} catch (err) {
|
|
@@ -2228,7 +2308,7 @@ API_ROUTES.GET["/api/admin/stats"] = async (_req, res, _url) => {
|
|
|
2228
2308
|
return null;
|
|
2229
2309
|
}
|
|
2230
2310
|
})
|
|
2231
|
-
)).filter(
|
|
2311
|
+
)).filter((item) => item !== null);
|
|
2232
2312
|
let totalVersions = 0;
|
|
2233
2313
|
let totalSize = 0;
|
|
2234
2314
|
const platformSet = /* @__PURE__ */ new Set();
|
|
@@ -2625,6 +2705,7 @@ export {
|
|
|
2625
2705
|
LATEST_LINK,
|
|
2626
2706
|
getSkillsDir,
|
|
2627
2707
|
getPlatformLinksDir,
|
|
2708
|
+
getRegistryPath,
|
|
2628
2709
|
ensureMarketDirs,
|
|
2629
2710
|
loadRegistry,
|
|
2630
2711
|
saveRegistry,
|
package/dist/electron-entry.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
getConfigValue,
|
|
25
25
|
getInstalledSkills,
|
|
26
26
|
getPlatformLinksDir,
|
|
27
|
+
getRegistryPath,
|
|
27
28
|
getSkillsDir,
|
|
28
29
|
installSkill,
|
|
29
30
|
isSkillInstalled,
|
|
@@ -38,12 +39,12 @@ import {
|
|
|
38
39
|
uninstallAll,
|
|
39
40
|
uninstallSkill,
|
|
40
41
|
updateSkill
|
|
41
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-ARRJETWL.js";
|
|
42
43
|
|
|
43
44
|
// src/cli.ts
|
|
44
45
|
import { Command } from "commander";
|
|
45
46
|
import { readFileSync } from "fs";
|
|
46
|
-
import { fileURLToPath
|
|
47
|
+
import { fileURLToPath } from "url";
|
|
47
48
|
import { dirname, resolve } from "path";
|
|
48
49
|
import { execSync } from "child_process";
|
|
49
50
|
|
|
@@ -554,15 +555,12 @@ Installing to ${targetPlatforms.length} platform(s)...
|
|
|
554
555
|
// src/commands/verify.ts
|
|
555
556
|
import fs3 from "fs-extra";
|
|
556
557
|
import path3 from "path";
|
|
557
|
-
import { fileURLToPath } from "url";
|
|
558
|
-
var __filename = fileURLToPath(import.meta.url);
|
|
559
|
-
var __dirname = path3.dirname(__filename);
|
|
560
558
|
async function verifySkill(skillName) {
|
|
561
559
|
try {
|
|
562
560
|
console.log(`
|
|
563
561
|
\u{1F50D} Verifying skill: ${skillName}
|
|
564
562
|
`);
|
|
565
|
-
const skillDir = path3.join(
|
|
563
|
+
const skillDir = path3.join(getSkillsDir(), skillName);
|
|
566
564
|
if (!await fs3.pathExists(skillDir)) {
|
|
567
565
|
console.error(`\u274C Skill "${skillName}" not found locally.`);
|
|
568
566
|
console.log(` Try: skm install ${skillName}`);
|
|
@@ -609,7 +607,7 @@ async function verifySkill(skillName) {
|
|
|
609
607
|
} else {
|
|
610
608
|
console.log(`\u26A0\uFE0F package.json not found (optional for basic skills)`);
|
|
611
609
|
}
|
|
612
|
-
const registryPath =
|
|
610
|
+
const registryPath = getRegistryPath();
|
|
613
611
|
if (await fs3.pathExists(registryPath)) {
|
|
614
612
|
try {
|
|
615
613
|
const registry = await fs3.readJson(registryPath);
|
|
@@ -646,9 +644,9 @@ async function verifySkill(skillName) {
|
|
|
646
644
|
}
|
|
647
645
|
|
|
648
646
|
// src/cli.ts
|
|
649
|
-
var
|
|
650
|
-
var
|
|
651
|
-
var packageJson = JSON.parse(readFileSync(resolve(
|
|
647
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
648
|
+
var __dirname = dirname(__filename);
|
|
649
|
+
var packageJson = JSON.parse(readFileSync(resolve(__dirname, "../package.json"), "utf-8"));
|
|
652
650
|
var VERSION = packageJson.version || "1.3.1";
|
|
653
651
|
var program = new Command();
|
|
654
652
|
program.name("skm").description("SkillMarket - Cross-platform skill manager for AI coding tools").version(VERSION);
|
package/gui/app.js
CHANGED
|
@@ -693,6 +693,34 @@ function applyI18nToStaticElements() {
|
|
|
693
693
|
}
|
|
694
694
|
}
|
|
695
695
|
|
|
696
|
+
// -----------------------------------------------------------------------------
|
|
697
|
+
// 主题切换
|
|
698
|
+
// -----------------------------------------------------------------------------
|
|
699
|
+
|
|
700
|
+
const THEMES = ['ocean', 'purple', 'forest', 'amber', 'sepia', 'gray', 'teal', 'dusk', 'light'];
|
|
701
|
+
|
|
702
|
+
function detectTheme() {
|
|
703
|
+
const saved = localStorage.getItem('skm-theme');
|
|
704
|
+
if (saved && THEMES.includes(saved)) return saved;
|
|
705
|
+
return 'ocean';
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function applyTheme(theme) {
|
|
709
|
+
// Remove all theme classes from body
|
|
710
|
+
THEMES.forEach(t => document.body.classList.remove(`theme-${t}`));
|
|
711
|
+
// Add the selected theme class
|
|
712
|
+
document.body.classList.add(`theme-${theme}`);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function setTheme(theme) {
|
|
716
|
+
if (!THEMES.includes(theme)) return;
|
|
717
|
+
applyTheme(theme);
|
|
718
|
+
localStorage.setItem('skm-theme', theme);
|
|
719
|
+
|
|
720
|
+
const themeSelect = document.getElementById('theme-select');
|
|
721
|
+
if (themeSelect) themeSelect.value = theme;
|
|
722
|
+
}
|
|
723
|
+
|
|
696
724
|
// -----------------------------------------------------------------------------
|
|
697
725
|
// 版本号加载
|
|
698
726
|
// -----------------------------------------------------------------------------
|
|
@@ -748,6 +776,12 @@ function reRenderCurrentView() {
|
|
|
748
776
|
// -----------------------------------------------------------------------------
|
|
749
777
|
|
|
750
778
|
document.addEventListener('DOMContentLoaded', () => {
|
|
779
|
+
// 初始化主题
|
|
780
|
+
const initialTheme = detectTheme();
|
|
781
|
+
applyTheme(initialTheme);
|
|
782
|
+
const themeSelect = document.getElementById('theme-select');
|
|
783
|
+
if (themeSelect) themeSelect.value = initialTheme;
|
|
784
|
+
|
|
751
785
|
initializeNavigation();
|
|
752
786
|
initializeControls();
|
|
753
787
|
initializeCollapsibleSections();
|
|
@@ -907,6 +941,14 @@ function initializeControls() {
|
|
|
907
941
|
});
|
|
908
942
|
}
|
|
909
943
|
|
|
944
|
+
// 主题切换
|
|
945
|
+
const themeSelect = document.getElementById('theme-select');
|
|
946
|
+
if (themeSelect) {
|
|
947
|
+
themeSelect.addEventListener('change', () => {
|
|
948
|
+
setTheme(themeSelect.value);
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
|
|
910
952
|
// 语言切换
|
|
911
953
|
const langSelect = document.getElementById('lang-select');
|
|
912
954
|
if (langSelect) {
|
package/gui/index.html
CHANGED
|
@@ -22,12 +22,25 @@
|
|
|
22
22
|
<button class="nav-btn" data-view="admin">⚙️ Admin</button>
|
|
23
23
|
<button class="nav-btn" data-view="help">📖 Help</button>
|
|
24
24
|
</nav>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
<div class="theme-switch">
|
|
26
|
+
<select id="theme-select">
|
|
27
|
+
<option value="ocean">🌊 海洋</option>
|
|
28
|
+
<option value="purple">🔮 深紫</option>
|
|
29
|
+
<option value="forest">🌿 森林</option>
|
|
30
|
+
<option value="amber">🔥 琥珀</option>
|
|
31
|
+
<option value="sepia">🕯 暖棕</option>
|
|
32
|
+
<option value="gray">🌫 柔灰</option>
|
|
33
|
+
<option value="teal">🦆 墨青</option>
|
|
34
|
+
<option value="dusk">🌆 暮紫</option>
|
|
35
|
+
<option value="light">☀ 纯白</option>
|
|
36
|
+
</select>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="lang-switch">
|
|
39
|
+
<select id="lang-select">
|
|
40
|
+
<option value="en">EN</option>
|
|
41
|
+
<option value="zh">中</option>
|
|
42
|
+
</select>
|
|
43
|
+
</div>
|
|
31
44
|
</header>
|
|
32
45
|
|
|
33
46
|
<!-- 主内容区 -->
|
package/gui/style.css
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
SkillMarket GUI - Styles
|
|
3
3
|
============================================================================= */
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/* =============================================================================
|
|
6
|
+
主题系统 - 通过 body.theme-xxx 切换
|
|
7
|
+
============================================================================= */
|
|
8
|
+
|
|
9
|
+
/* 海洋蓝(默认) */
|
|
10
|
+
:root,
|
|
11
|
+
body.theme-ocean {
|
|
6
12
|
--bg-primary: #1a1a2e;
|
|
7
13
|
--bg-secondary: #16213e;
|
|
8
14
|
--bg-card: #0f3460;
|
|
@@ -19,6 +25,142 @@
|
|
|
19
25
|
--topbar-height: 52px;
|
|
20
26
|
}
|
|
21
27
|
|
|
28
|
+
/* 深空紫 */
|
|
29
|
+
body.theme-purple {
|
|
30
|
+
--bg-primary: #1a0a2e;
|
|
31
|
+
--bg-secondary: #2d1b4e;
|
|
32
|
+
--bg-card: #3d2a6a;
|
|
33
|
+
--bg-hover: #4d3a7a;
|
|
34
|
+
--text-primary: #d4a5ff;
|
|
35
|
+
--text-secondary: #ffffff;
|
|
36
|
+
--text-muted: #a080b0;
|
|
37
|
+
--border-color: #3d2a6a;
|
|
38
|
+
--accent: #b388ff;
|
|
39
|
+
--accent-hover: #c9a8ff;
|
|
40
|
+
--success: #4caf50;
|
|
41
|
+
--warning: #ff9800;
|
|
42
|
+
--danger: #f44336;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* 森林绿 */
|
|
46
|
+
body.theme-forest {
|
|
47
|
+
--bg-primary: #0d2818;
|
|
48
|
+
--bg-secondary: #1a3d2a;
|
|
49
|
+
--bg-card: #2d5a3a;
|
|
50
|
+
--bg-hover: #3a7a4a;
|
|
51
|
+
--text-primary: #8bc34a;
|
|
52
|
+
--text-secondary: #ffffff;
|
|
53
|
+
--text-muted: #8a9a8a;
|
|
54
|
+
--border-color: #2d5a3a;
|
|
55
|
+
--accent: #66bb6a;
|
|
56
|
+
--accent-hover: #81c784;
|
|
57
|
+
--success: #4caf50;
|
|
58
|
+
--warning: #ff9800;
|
|
59
|
+
--danger: #f44336;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* 暖琥珀 */
|
|
63
|
+
body.theme-amber {
|
|
64
|
+
--bg-primary: #2a1f0a;
|
|
65
|
+
--bg-secondary: #3d301a;
|
|
66
|
+
--bg-card: #5a452a;
|
|
67
|
+
--bg-hover: #7a5a3a;
|
|
68
|
+
--text-primary: #ffb74d;
|
|
69
|
+
--text-secondary: #ffffff;
|
|
70
|
+
--text-muted: #b0a080;
|
|
71
|
+
--border-color: #5a452a;
|
|
72
|
+
--accent: #ffa726;
|
|
73
|
+
--accent-hover: #ffb74d;
|
|
74
|
+
--success: #4caf50;
|
|
75
|
+
--warning: #ff9800;
|
|
76
|
+
--danger: #f44336;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* 暖棕 - 仿纸质感,低蓝光护眼 */
|
|
80
|
+
body.theme-sepia {
|
|
81
|
+
--bg-primary: #2d2416;
|
|
82
|
+
--bg-secondary: #352b1c;
|
|
83
|
+
--bg-card: #3d3223;
|
|
84
|
+
--bg-hover: #4a3d2c;
|
|
85
|
+
--text-primary: #e6c9a8;
|
|
86
|
+
--text-secondary: #f5ede0;
|
|
87
|
+
--text-muted: #a09080;
|
|
88
|
+
--border-color: #4a3d2c;
|
|
89
|
+
--accent: #d4a76a;
|
|
90
|
+
--accent-hover: #e0b87a;
|
|
91
|
+
--success: #6b8e23;
|
|
92
|
+
--warning: #cd853f;
|
|
93
|
+
--danger: #b55343;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* 柔灰 - 极简低对比 */
|
|
97
|
+
body.theme-gray {
|
|
98
|
+
--bg-primary: #1e1e1e;
|
|
99
|
+
--bg-secondary: #262626;
|
|
100
|
+
--bg-card: #2d2d2d;
|
|
101
|
+
--bg-hover: #383838;
|
|
102
|
+
--text-primary: #c0c0c0;
|
|
103
|
+
--text-secondary: #e0e0e0;
|
|
104
|
+
--text-muted: #808080;
|
|
105
|
+
--border-color: #383838;
|
|
106
|
+
--accent: #a0a0a0;
|
|
107
|
+
--accent-hover: #b0b0b0;
|
|
108
|
+
--success: #5a8a5a;
|
|
109
|
+
--warning: #a09040;
|
|
110
|
+
--danger: #8a4a4a;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* 墨青 - 舒缓蓝绿 */
|
|
114
|
+
body.theme-teal {
|
|
115
|
+
--bg-primary: #0d2b2e;
|
|
116
|
+
--bg-secondary: #123538;
|
|
117
|
+
--bg-card: #1a3f42;
|
|
118
|
+
--bg-hover: #224f52;
|
|
119
|
+
--text-primary: #8ed1cc;
|
|
120
|
+
--text-secondary: #d4ecea;
|
|
121
|
+
--text-muted: #609090;
|
|
122
|
+
--border-color: #224f52;
|
|
123
|
+
--accent: #5bc0be;
|
|
124
|
+
--accent-hover: #6cd4d2;
|
|
125
|
+
--success: #4a8a5a;
|
|
126
|
+
--warning: #b0a040;
|
|
127
|
+
--danger: #b55343;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* 暮紫 - 柔和紫灰 */
|
|
131
|
+
body.theme-dusk {
|
|
132
|
+
--bg-primary: #1a1423;
|
|
133
|
+
--bg-secondary: #221a2e;
|
|
134
|
+
--bg-card: #2a1f33;
|
|
135
|
+
--bg-hover: #352840;
|
|
136
|
+
--text-primary: #d4c0e8;
|
|
137
|
+
--text-secondary: #ede0f5;
|
|
138
|
+
--text-muted: #9080a0;
|
|
139
|
+
--border-color: #352840;
|
|
140
|
+
--accent: #c4a7d1;
|
|
141
|
+
--accent-hover: #d4b8e0;
|
|
142
|
+
--success: #6b8a6b;
|
|
143
|
+
--warning: #a09060;
|
|
144
|
+
--danger: #b55373;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* 纯白 - 明亮模式 */
|
|
148
|
+
body.theme-light {
|
|
149
|
+
--bg-primary: #f0f2f5;
|
|
150
|
+
--bg-secondary: #ffffff;
|
|
151
|
+
--bg-card: #ffffff;
|
|
152
|
+
--bg-hover: #e4e6eb;
|
|
153
|
+
--text-primary: #e94560;
|
|
154
|
+
--text-secondary: #1a1a2e;
|
|
155
|
+
--text-muted: #8a8a9a;
|
|
156
|
+
--border-color: #d0d2d8;
|
|
157
|
+
--accent: #e94560;
|
|
158
|
+
--accent-hover: #d63850;
|
|
159
|
+
--success: #4caf50;
|
|
160
|
+
--warning: #ff9800;
|
|
161
|
+
--danger: #f44336;
|
|
162
|
+
}
|
|
163
|
+
|
|
22
164
|
* {
|
|
23
165
|
margin: 0;
|
|
24
166
|
padding: 0;
|
|
@@ -1156,11 +1298,41 @@ body {
|
|
|
1156
1298
|
}
|
|
1157
1299
|
|
|
1158
1300
|
/* -----------------------------------------------------------------------------
|
|
1159
|
-
语言切换
|
|
1301
|
+
主题切换 & 语言切换
|
|
1160
1302
|
----------------------------------------------------------------------------- */
|
|
1161
1303
|
|
|
1304
|
+
.theme-switch {
|
|
1305
|
+
display: flex;
|
|
1306
|
+
align-items: center;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
#theme-select {
|
|
1310
|
+
padding: 6px 28px 6px 10px;
|
|
1311
|
+
background: var(--bg-card);
|
|
1312
|
+
border: 1px solid var(--border-color);
|
|
1313
|
+
color: var(--text-secondary);
|
|
1314
|
+
border-radius: 6px;
|
|
1315
|
+
font-size: 0.85rem;
|
|
1316
|
+
cursor: pointer;
|
|
1317
|
+
appearance: none;
|
|
1318
|
+
-webkit-appearance: none;
|
|
1319
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23a0a0a0' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
|
1320
|
+
background-repeat: no-repeat;
|
|
1321
|
+
background-position: right 8px center;
|
|
1322
|
+
padding-right: 28px;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
#theme-select:focus {
|
|
1326
|
+
outline: none;
|
|
1327
|
+
border-color: var(--accent);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
#theme-select option {
|
|
1331
|
+
background: var(--bg-secondary);
|
|
1332
|
+
color: var(--text-secondary);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1162
1335
|
.lang-switch {
|
|
1163
|
-
margin-left: auto;
|
|
1164
1336
|
display: flex;
|
|
1165
1337
|
align-items: center;
|
|
1166
1338
|
padding-left: 8px;
|