chore: v0.1.6 UI优化 - 两列网格布局、暗色适配、系统配置浮窗保存、退出登录统一到侧边栏
This commit is contained in:
77
packages/backend/package-lock.json
generated
77
packages/backend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "cloudsearch-backend",
|
"name": "cloudsearch-backend",
|
||||||
"version": "2.0.9",
|
"version": "0.1.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "cloudsearch-backend",
|
"name": "cloudsearch-backend",
|
||||||
"version": "2.0.9",
|
"version": "0.1.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"better-sqlite3": "^11.0.0",
|
"better-sqlite3": "^11.0.0",
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"playwright": "^1.52.0",
|
"playwright": "^1.52.0",
|
||||||
"sharp": "^0.33.0",
|
"sharp": "^0.33.0",
|
||||||
|
"socks-proxy-agent": "^9.0.0",
|
||||||
"uuid": "^10.0.0"
|
"uuid": "^10.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -2396,6 +2397,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@@ -3306,6 +3315,70 @@
|
|||||||
"is-arrayish": "^0.3.1"
|
"is-arrayish": "^0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/smart-buffer": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks": {
|
||||||
|
"version": "2.8.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz",
|
||||||
|
"integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "^10.1.1",
|
||||||
|
"smart-buffer": "^4.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks-proxy-agent": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-fFlbMlfsXhK02ZB8aZY7Hwxh/IHBV9b1Oq9bvBk6tkFWXvdAxUgA0wbw/NYR5liU3Y5+KI6U4FH3kYJt9QYv0w==",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "8.0.0",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"socks": "^2.8.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks-proxy-agent/node_modules/agent-base": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-QT8i0hCz6C/KQ+KTAbSNwCHDGdmUJl2tp2ZpNlGSWCfhUNVbYG2WLE3MdZGBAgXPV4GAvjGMxo+C1hroyxmZEg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks-proxy-agent/node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks-proxy-agent/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cloudsearch-backend",
|
"name": "cloudsearch-backend",
|
||||||
"version": "0.0.3",
|
"version": "0.1.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch src/main.ts",
|
"dev": "tsx watch src/main.ts",
|
||||||
|
|||||||
1191
packages/backend/src/cloud/baidu.driver.ts
Normal file
1191
packages/backend/src/cloud/baidu.driver.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@ interface CleanupOpResult { trashed: number; errors: string[] }
|
|||||||
|
|
||||||
interface CloudCleanupDriver {
|
interface CloudCleanupDriver {
|
||||||
/** Trash date folders (YYYY-MM-DD) older than `days`. */
|
/** Trash date folders (YYYY-MM-DD) older than `days`. */
|
||||||
cleanupOldDateFolders(days: number): Promise<CleanupOpResult>;
|
cleanupOldDateFolders(days: number, whitelistDirs?: string[]): Promise<CleanupOpResult>;
|
||||||
/**
|
/**
|
||||||
* If used space exceeds thresholdPercent% of TOTAL capacity,
|
* If used space exceeds thresholdPercent% of TOTAL capacity,
|
||||||
* delete oldest date folders until totalBytes * deletePercent/100
|
* delete oldest date folders until totalBytes * deletePercent/100
|
||||||
@@ -27,7 +27,7 @@ interface CloudCleanupDriver {
|
|||||||
* @param thresholdPercent — trigger when usage >= this % of total
|
* @param thresholdPercent — trigger when usage >= this % of total
|
||||||
* @param deletePercent — free this % of total capacity
|
* @param deletePercent — free this % of total capacity
|
||||||
*/
|
*/
|
||||||
cleanupBySpaceThreshold(thresholdPercent: number, deletePercent: number): Promise<CleanupOpResult>;
|
cleanupBySpaceThreshold(thresholdPercent: number, deletePercent: number, whitelistDirs?: string[]): Promise<CleanupOpResult>;
|
||||||
/** Permanently empty the recycle bin. */
|
/** Permanently empty the recycle bin. */
|
||||||
emptyTrash(): Promise<boolean>;
|
emptyTrash(): Promise<boolean>;
|
||||||
}
|
}
|
||||||
@@ -62,6 +62,18 @@ interface CleanupStats {
|
|||||||
errors: string[];
|
errors: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Read whitelist dirs from system_configs (cleanup_whitelist_dirs). */
|
||||||
|
export function getWhitelistDirs(): string[] {
|
||||||
|
const raw = getSystemConfig('cleanup_whitelist_dirs');
|
||||||
|
if (!raw) return [];
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return Array.isArray(parsed) ? parsed.filter((d: any) => typeof d === 'string') : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Get all active cloud configs (any type). Used by the orchestrator. */
|
/** Get all active cloud configs (any type). Used by the orchestrator. */
|
||||||
function getActiveCleanupConfigs(): Array<{ id: number; cloud_type: string; cookie: string; nickname?: string }> {
|
function getActiveCleanupConfigs(): Array<{ id: number; cloud_type: string; cookie: string; nickname?: string }> {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
@@ -77,6 +89,7 @@ function getActiveCleanupConfigs(): Array<{ id: number; cloud_type: string; cook
|
|||||||
*/
|
*/
|
||||||
async function cleanupCloudFiles(days: number): Promise<CleanupOpResult> {
|
async function cleanupCloudFiles(days: number): Promise<CleanupOpResult> {
|
||||||
const configs = getActiveCleanupConfigs();
|
const configs = getActiveCleanupConfigs();
|
||||||
|
const whitelistDirs = getWhitelistDirs();
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
let totalTrashed = 0;
|
let totalTrashed = 0;
|
||||||
|
|
||||||
@@ -87,7 +100,7 @@ async function cleanupCloudFiles(days: number): Promise<CleanupOpResult> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await driver.cleanupOldDateFolders(days);
|
const result = await driver.cleanupOldDateFolders(days, whitelistDirs);
|
||||||
totalTrashed += result.trashed;
|
totalTrashed += result.trashed;
|
||||||
errors.push(...result.errors.map(e => `[${cfg.cloud_type}#${cfg.id}] ${e}`));
|
errors.push(...result.errors.map(e => `[${cfg.cloud_type}#${cfg.id}] ${e}`));
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -108,6 +121,7 @@ async function cleanupAllBySpaceThreshold(
|
|||||||
deletePercent: number,
|
deletePercent: number,
|
||||||
): Promise<CleanupOpResult> {
|
): Promise<CleanupOpResult> {
|
||||||
const configs = getActiveCleanupConfigs();
|
const configs = getActiveCleanupConfigs();
|
||||||
|
const whitelistDirs = getWhitelistDirs();
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
let totalTrashed = 0;
|
let totalTrashed = 0;
|
||||||
|
|
||||||
@@ -118,7 +132,7 @@ async function cleanupAllBySpaceThreshold(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await driver.cleanupBySpaceThreshold(thresholdPercent, deletePercent);
|
const result = await driver.cleanupBySpaceThreshold(thresholdPercent, deletePercent, whitelistDirs);
|
||||||
totalTrashed += result.trashed;
|
totalTrashed += result.trashed;
|
||||||
errors.push(...result.errors.map(e => `[${cfg.cloud_type}#${cfg.id}] ${e}`));
|
errors.push(...result.errors.map(e => `[${cfg.cloud_type}#${cfg.id}] ${e}`));
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -322,31 +322,86 @@ export function cleanupOldSaveRecords(): void {
|
|||||||
// ── Storage Refresh ───────────────────────────────────────────────
|
// ── Storage Refresh ───────────────────────────────────────────────
|
||||||
|
|
||||||
export async function refreshAllStorageInfo(): Promise<void> {
|
export async function refreshAllStorageInfo(): Promise<void> {
|
||||||
const configs = getActiveCloudConfigs().filter(c => c.cloud_type === 'quark' && c.cookie);
|
const configs = getActiveCloudConfigs().filter(c => c.cookie);
|
||||||
if (configs.length === 0) return;
|
if (configs.length === 0) return;
|
||||||
|
|
||||||
|
const verifyCookies = getSystemConfig('cleanup_verify_enabled') === 'true';
|
||||||
|
|
||||||
for (const cfg of configs) {
|
for (const cfg of configs) {
|
||||||
try {
|
try {
|
||||||
const { QuarkDriver } = require('./drivers/quark.driver');
|
|
||||||
const driver = new QuarkDriver({ cookie: decrypt(cfg.cookie!), nickname: cfg.nickname });
|
|
||||||
const storage = await driver.getStorageInfo(
|
|
||||||
decrypt(cfg.cookie!),
|
|
||||||
(fullUsed: string, total: string) => {
|
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
db.prepare(
|
const decryptedCookie = decrypt(cfg.cookie!);
|
||||||
|
|
||||||
|
switch (cfg.cloud_type) {
|
||||||
|
case 'quark': {
|
||||||
|
const driver = new QuarkDriver({ cookie: decryptedCookie, nickname: cfg.nickname });
|
||||||
|
|
||||||
|
// Get storage info (includes background calibration callback)
|
||||||
|
const storage = await driver.getStorageInfo(
|
||||||
|
(fullUsed: string, total: string) => {
|
||||||
|
const dbInner = getDb();
|
||||||
|
dbInner.prepare(
|
||||||
`UPDATE cloud_configs SET storage_used = ?, storage_total = ? WHERE id = ?`
|
`UPDATE cloud_configs SET storage_used = ?, storage_total = ? WHERE id = ?`
|
||||||
).run(fullUsed, total, cfg.id);
|
).run(fullUsed, total, cfg.id);
|
||||||
console.log(`[Storage] Background calibration done for quark#${cfg.id}: ${fullUsed} / ${total}`);
|
console.log(`[Storage] Background calibration done for quark#${cfg.id}: ${fullUsed} / ${total}`);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (storage.totalBytes > 0 || storage.usedBytes > 0) {
|
if (storage.totalBytes > 0 || storage.usedBytes > 0) {
|
||||||
const db = getDb();
|
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`UPDATE cloud_configs SET storage_used = ?, storage_total = ? WHERE id = ?`
|
`UPDATE cloud_configs SET storage_used = ?, storage_total = ? WHERE id = ?`
|
||||||
).run(storage.used, storage.total, cfg.id);
|
).run(storage.used, storage.total, cfg.id);
|
||||||
|
console.log(`[Storage] Updated quark#${cfg.id}: ${storage.used} / ${storage.total}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cookie verification
|
||||||
|
if (verifyCookies) {
|
||||||
|
const valid = await driver.validate();
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE cloud_configs SET verification_status = ?, updated_at = ? WHERE id = ?`
|
||||||
|
).run(valid ? 'valid' : 'invalid', localTimestamp(), cfg.id);
|
||||||
|
console.log(`[Storage] Verification for quark#${cfg.id}: ${valid ? 'valid' : 'invalid'}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'baidu': {
|
||||||
|
const driver = new BaiduDriver({ cookie: decryptedCookie, nickname: cfg.nickname });
|
||||||
|
|
||||||
|
// Get storage info
|
||||||
|
const storage = await driver.getStorageInfo();
|
||||||
|
if (storage.used !== '0 B' && storage.total !== '0 B') {
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE cloud_configs SET storage_used = ?, storage_total = ? WHERE id = ?`
|
||||||
|
).run(storage.used, storage.total, cfg.id);
|
||||||
|
console.log(`[Storage] Updated baidu#${cfg.id}: ${storage.used} / ${storage.total}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cookie verification
|
||||||
|
if (verifyCookies) {
|
||||||
|
const valid = await driver.validate();
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE cloud_configs SET verification_status = ?, updated_at = ? WHERE id = ?`
|
||||||
|
).run(valid ? 'valid' : 'invalid', localTimestamp(), cfg.id);
|
||||||
|
console.log(`[Storage] Verification for baidu#${cfg.id}: ${valid ? 'valid' : 'invalid'}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(`[Storage] Skipping ${cfg.cloud_type}#${cfg.id} — unsupported cloud type for storage refresh`);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(`[Storage] Failed to refresh quark#${cfg.id}:`, err.message);
|
console.error(`[Storage] Failed to refresh ${cfg.cloud_type}#${cfg.id}:`, err.message);
|
||||||
|
// On error, mark as invalid if verification is enabled
|
||||||
|
if (verifyCookies) {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE cloud_configs SET verification_status = 'invalid', updated_at = ? WHERE id = ?`
|
||||||
|
).run(localTimestamp(), cfg.id);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,6 +306,11 @@ function seedSystemConfigs(db: Database.Database): void {
|
|||||||
{ key: 'cleanup_space_threshold_percent', value: '90', description: '空间使用阈值百分比(超过此值时触发强制清理)' },
|
{ key: 'cleanup_space_threshold_percent', value: '90', description: '空间使用阈值百分比(超过此值时触发强制清理)' },
|
||||||
{ key: 'cleanup_space_threshold_delete_percent', value: '10', description: '触发阈值清理时释放总空间的百分比(如 10 表示累计删除最旧文件直到达到总空间的 10%,6TB 总空间 → 释放 ~600GB)' },
|
{ key: 'cleanup_space_threshold_delete_percent', value: '10', description: '触发阈值清理时释放总空间的百分比(如 10 表示累计删除最旧文件直到达到总空间的 10%,6TB 总空间 → 释放 ~600GB)' },
|
||||||
{ key: 'save_reuse_enabled', value: 'true', description: '启用分享链接复用(相同原始链接不再重复转存,直接复用之前的分享链接)' },
|
{ key: 'save_reuse_enabled', value: 'true', description: '启用分享链接复用(相同原始链接不再重复转存,直接复用之前的分享链接)' },
|
||||||
|
{ key: 'cleanup_whitelist_dirs', value: '[]', description: '清理白名单目录名称列表(JSON数组),这些目录不会被自动清理' },
|
||||||
|
{ key: 'storage_refresh_interval', value: '60', description: '存储空间刷新间隔(分钟),0=不自动刷新' },
|
||||||
|
{ key: 'cleanup_auto_refresh_storage', value: 'true', description: '启用自动刷新存储空间信息' },
|
||||||
|
{ key: 'cleanup_verify_enabled', value: 'true', description: '定期验证网盘 Cookie 有效性(随存储刷新一起执行)' },
|
||||||
|
{ key: 'cleanup_verify_interval', value: '30', description: 'Cookie 有效性检测间隔(分钟)' },
|
||||||
{ key: 'cleanup_last_run', value: '', description: '上次自动清理时间' },
|
{ key: 'cleanup_last_run', value: '', description: '上次自动清理时间' },
|
||||||
{ key: 'cleanup_last_stats', value: '', description: '上次清理结果统计(JSON)' },
|
{ key: 'cleanup_last_stats', value: '', description: '上次清理结果统计(JSON)' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -38,14 +38,32 @@ export async function getStorageInfoQuick(cookie: string, fallbackTotal?: string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quick used-space estimate: sum root-level file sizes + subdir sizes
|
// Accurate used space via /member API (1 call, no full traversal needed)
|
||||||
|
// Ref: pan.quark.cn/1/clouddrive/member returns use_capacity + total_capacity
|
||||||
let usedBytes = 0;
|
let usedBytes = 0;
|
||||||
|
try {
|
||||||
|
const memberParams = new URLSearchParams({ pr: 'ucpro', fr: 'pc', uc_param_str: '', __t: String(Date.now()), __dt: '1000' });
|
||||||
|
const memberResp = await fetch(`https://pan.quark.cn/1/clouddrive/member?${memberParams.toString()}`, {
|
||||||
|
headers: getHeaders(cookie),
|
||||||
|
signal: AbortSignal.timeout(10000),
|
||||||
|
});
|
||||||
|
if (memberResp.ok) {
|
||||||
|
const memberData = await memberResp.json() as any;
|
||||||
|
if (memberData.status === 200 && memberData.data?.use_capacity != null) {
|
||||||
|
usedBytes = memberData.data.use_capacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Fallback: sum root-level file sizes (夸克 folders return size=0)
|
||||||
|
if (usedBytes === 0) {
|
||||||
try {
|
try {
|
||||||
const rootFiles = await listRootDir(cookie);
|
const rootFiles = await listRootDir(cookie);
|
||||||
for (const f of rootFiles) {
|
for (const f of rootFiles) {
|
||||||
usedBytes += f.size || 0;
|
usedBytes += f.size || 0;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
// Cache the result (3h window)
|
// Cache the result (3h window)
|
||||||
const currentHourBlock = Math.floor(new Date().getHours() / 3);
|
const currentHourBlock = Math.floor(new Date().getHours() / 3);
|
||||||
@@ -231,7 +249,7 @@ export async function emptyTrash(cookie: string): Promise<boolean> {
|
|||||||
/**
|
/**
|
||||||
* Cleanup: trash date-named folders (YYYY-MM-DD) older than `days`.
|
* Cleanup: trash date-named folders (YYYY-MM-DD) older than `days`.
|
||||||
*/
|
*/
|
||||||
export async function cleanupOldDateFolders(cookie: string, days: number): Promise<{ trashed: number; errors: string[] }> {
|
export async function cleanupOldDateFolders(cookie: string, days: number, whitelistDirs?: string[]): Promise<{ trashed: number; errors: string[] }> {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
const cutoff = new Date();
|
const cutoff = new Date();
|
||||||
cutoff.setDate(cutoff.getDate() - days);
|
cutoff.setDate(cutoff.getDate() - days);
|
||||||
@@ -242,6 +260,7 @@ export async function cleanupOldDateFolders(cookie: string, days: number): Promi
|
|||||||
const oldFolders = rootItems.filter(item => {
|
const oldFolders = rootItems.filter(item => {
|
||||||
if (!item.dir) return false;
|
if (!item.dir) return false;
|
||||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(item.file_name)) return false;
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(item.file_name)) return false;
|
||||||
|
if (whitelistDirs && whitelistDirs.includes(item.file_name)) return false;
|
||||||
return item.file_name < cutoffStr;
|
return item.file_name < cutoffStr;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -270,6 +289,7 @@ export async function cleanupBySpaceThreshold(
|
|||||||
cookie: string,
|
cookie: string,
|
||||||
thresholdPercent: number,
|
thresholdPercent: number,
|
||||||
deletePercent: number,
|
deletePercent: number,
|
||||||
|
whitelistDirs?: string[],
|
||||||
): Promise<{ trashed: number; errors: string[] }> {
|
): Promise<{ trashed: number; errors: string[] }> {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
@@ -288,6 +308,7 @@ export async function cleanupBySpaceThreshold(
|
|||||||
const rootItems = await listRootDir(cookie);
|
const rootItems = await listRootDir(cookie);
|
||||||
const dateFolders = rootItems
|
const dateFolders = rootItems
|
||||||
.filter(item => item.dir && /^\d{4}-\d{2}-\d{2}$/.test(item.file_name))
|
.filter(item => item.dir && /^\d{4}-\d{2}-\d{2}$/.test(item.file_name))
|
||||||
|
.filter(item => !whitelistDirs || !whitelistDirs.includes(item.file_name))
|
||||||
.sort((a, b) => a.file_name.localeCompare(b.file_name));
|
.sort((a, b) => a.file_name.localeCompare(b.file_name));
|
||||||
|
|
||||||
if (dateFolders.length === 0) return { trashed: 0, errors: [] };
|
if (dateFolders.length === 0) return { trashed: 0, errors: [] };
|
||||||
|
|||||||
@@ -177,10 +177,20 @@ async function start(): Promise<void> {
|
|||||||
setInterval(() => { checkAndRunScheduledCleanup().catch(err => console.error('[Cleanup] Scheduler error:', err.message)); }, CLEANUP_INTERVAL);
|
setInterval(() => { checkAndRunScheduledCleanup().catch(err => console.error('[Cleanup] Scheduler error:', err.message)); }, CLEANUP_INTERVAL);
|
||||||
setTimeout(() => { checkAndRunScheduledCleanup().catch(err => console.error('[Cleanup] Initial check error:', err.message)); }, 30000);
|
setTimeout(() => { checkAndRunScheduledCleanup().catch(err => console.error('[Cleanup] Initial check error:', err.message)); }, 30000);
|
||||||
|
|
||||||
// Storage info refresh scheduler — every 60 minutes
|
// Storage info refresh scheduler — configurable via system_configs
|
||||||
const STORAGE_REFRESH_INTERVAL = 60 * 60 * 1000;
|
const { getSystemConfig: getSysCfg } = require('./admin/system-config.service');
|
||||||
setInterval(() => { refreshAllStorageInfo().catch(err => console.error('[Storage] Refresh error:', err.message)); }, STORAGE_REFRESH_INTERVAL);
|
const storageRefreshEnabled = getSysCfg('cleanup_auto_refresh_storage') !== 'false';
|
||||||
|
const storageRefreshMin = parseInt(getSysCfg('storage_refresh_interval') || '60', 10) || 60;
|
||||||
|
const storageRefreshMs = storageRefreshMin * 60 * 1000;
|
||||||
|
const verifyCookies = getSysCfg('cleanup_verify_enabled') === 'true';
|
||||||
|
|
||||||
|
if (!storageRefreshEnabled || storageRefreshMin === 0) {
|
||||||
|
console.log(`[Storage] Auto-refresh disabled (enabled=${storageRefreshEnabled}, interval=${storageRefreshMin} min)`);
|
||||||
|
} else {
|
||||||
|
console.log(`[Storage] Auto-refresh every ${storageRefreshMin} minutes (verifyCookies=${verifyCookies})`);
|
||||||
|
setInterval(() => { refreshAllStorageInfo().catch(err => console.error('[Storage] Refresh error:', err.message)); }, storageRefreshMs);
|
||||||
setTimeout(() => { refreshAllStorageInfo().catch(err => console.error('[Storage] Initial refresh error:', err.message)); }, 60000);
|
setTimeout(() => { refreshAllStorageInfo().catch(err => console.error('[Storage] Initial refresh error:', err.message)); }, 60000);
|
||||||
|
}
|
||||||
|
|
||||||
const server = app.listen(config.port, () => {
|
const server = app.listen(config.port, () => {
|
||||||
console.log(`[Server] CloudSearch Backend running on port ${config.port} (${config.nodeEnv})`);
|
console.log(`[Server] CloudSearch Backend running on port ${config.port} (${config.nodeEnv})`);
|
||||||
|
|||||||
@@ -9,4 +9,4 @@
|
|||||||
* 修改此文件的同时请同步更新后端 package.json 中的 version 字段。
|
* 修改此文件的同时请同步更新后端 package.json 中的 version 字段。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const APP_VERSION = "0.1.1";
|
export const APP_VERSION = "0.1.6";
|
||||||
|
|||||||
4
packages/frontend/package-lock.json
generated
4
packages/frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "cloudsearch-frontend",
|
"name": "cloudsearch-frontend",
|
||||||
"version": "1.1.8",
|
"version": "0.1.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "cloudsearch-frontend",
|
"name": "cloudsearch-frontend",
|
||||||
"version": "1.1.8",
|
"version": "0.1.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"axios": "^1.7.0",
|
"axios": "^1.7.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cloudsearch-frontend",
|
"name": "cloudsearch-frontend",
|
||||||
"version": "0.1.1",
|
"version": "0.1.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -26,43 +26,107 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* =============================================
|
||||||
|
CloudSearch Design System — Global Tokens
|
||||||
|
============================================= */
|
||||||
:root {
|
:root {
|
||||||
--bg: #f5f7fa;
|
/* ── Backgrounds ── */
|
||||||
|
--bg: #f0f2f5;
|
||||||
--bg-card: #ffffff;
|
--bg-card: #ffffff;
|
||||||
--bg-input: #f5f7fa;
|
--bg-input: #f5f7fa;
|
||||||
--text: #303133;
|
--bg-page: #f0f2f5;
|
||||||
--text2: #909399;
|
--bg-card-header: linear-gradient(135deg, #f8f9fc 0%, #eef1f8 100%);
|
||||||
--text3: #c0c4cc;
|
|
||||||
--border: #e4e7ed;
|
/* ── Text ── */
|
||||||
|
--text: #1d2129;
|
||||||
|
--text-secondary: #4e5969;
|
||||||
|
--text-tertiary: #86909c;
|
||||||
|
--text-placeholder: #c9cdd4;
|
||||||
|
|
||||||
|
/* ── Borders ── */
|
||||||
|
--border: #e5e6eb;
|
||||||
|
--border-light: #f2f3f5;
|
||||||
|
|
||||||
|
/* ── Primary ── */
|
||||||
--primary: #409eff;
|
--primary: #409eff;
|
||||||
|
--primary-hover: #66b1ff;
|
||||||
--primary-light: rgba(64, 158, 255, 0.08);
|
--primary-light: rgba(64, 158, 255, 0.08);
|
||||||
--shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
--primary-soft: #ecf5ff;
|
||||||
--hover: #f5f7fa;
|
|
||||||
|
/* ── Shadows ── */
|
||||||
|
--shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||||
|
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.06);
|
||||||
|
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
/* ── Radius ── */
|
||||||
|
--radius-sm: 6px;
|
||||||
|
--radius-md: 10px;
|
||||||
|
--radius-lg: 14px;
|
||||||
|
--radius-xl: 20px;
|
||||||
|
|
||||||
|
/* ── Sizing ── */
|
||||||
|
--sidebar-w: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"] {
|
||||||
--bg: #141414;
|
--bg: #141414;
|
||||||
--bg-card: #1f1f1f;
|
--bg-card: #1d1d1d;
|
||||||
--bg-input: #2a2a2a;
|
--bg-input: #2a2a2a;
|
||||||
|
--bg-page: #0f0f0f;
|
||||||
|
--bg-card-header: linear-gradient(135deg, #1d1d1d 0%, #1a1a1a 100%);
|
||||||
|
|
||||||
--text: #e5e5e5;
|
--text: #e5e5e5;
|
||||||
--text2: #999999;
|
--text-secondary: #999999;
|
||||||
--text3: #666666;
|
--text-tertiary: #666666;
|
||||||
|
--text-placeholder: #555555;
|
||||||
|
|
||||||
--border: #333333;
|
--border: #333333;
|
||||||
|
--border-light: #2a2a2a;
|
||||||
|
|
||||||
--primary: #409eff;
|
--primary: #409eff;
|
||||||
|
--primary-hover: #66b1ff;
|
||||||
--primary-light: rgba(64, 158, 255, 0.15);
|
--primary-light: rgba(64, 158, 255, 0.15);
|
||||||
--shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
|
--primary-soft: rgba(64, 158, 255, 0.1);
|
||||||
--hover: #2a2a2a;
|
|
||||||
|
--shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||||
|
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] body { background: var(--bg); color: var(--text); }
|
[data-theme="dark"] body { background: var(--bg); color: var(--text); }
|
||||||
[data-theme="dark"] .el-card, [data-theme="dark"] .el-dialog, [data-theme="dark"] .el-menu { background-color: var(--bg-card) !important; border-color: var(--border) !important; color: var(--text) !important; }
|
[data-theme="dark"] .el-card,
|
||||||
[data-theme="dark"] .el-input__wrapper, [data-theme="dark"] .el-select .el-input__wrapper { background-color: var(--bg-input) !important; border-color: var(--border) !important; }
|
[data-theme="dark"] .el-dialog,
|
||||||
[data-theme="dark"] .el-input__inner, [data-theme="dark"] .el-textarea__inner { background-color: var(--bg-input) !important; color: var(--text) !important; }
|
[data-theme="dark"] .el-menu,
|
||||||
[data-theme="dark"] .el-button--default { background: var(--bg-card); border-color: var(--border); color: var(--text); }
|
[data-theme="dark"] .el-table,
|
||||||
[data-theme="dark"] .rank-panel, [data-theme="dark"] .rank-item { background: var(--bg-card); border-color: var(--border); }
|
[data-theme="dark"] .el-select-dropdown,
|
||||||
[data-theme="dark"] .panel-title, [data-theme="dark"] .rank-name, [data-theme="dark"] .card-title { color: var(--text); }
|
[data-theme="dark"] .el-popover {
|
||||||
[data-theme="dark"] .card-meta, [data-theme="dark"] .rank-cnt, [data-theme="dark"] .panel-footer span:first-child { color: var(--text2); }
|
--el-bg-color: var(--bg-card) !important;
|
||||||
[data-theme="dark"] .site-footer { background: var(--bg-card); border-color: var(--border); color: var(--text2); }
|
--el-border-color: var(--border) !important;
|
||||||
|
--el-text-color-primary: var(--text) !important;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .el-dialog__body,
|
||||||
|
[data-theme="dark"] .el-table__body td {
|
||||||
|
background-color: var(--bg-card) !important;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .el-table__header th {
|
||||||
|
background-color: #262626 !important;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .el-input__wrapper,
|
||||||
|
[data-theme="dark"] .el-select .el-input__wrapper {
|
||||||
|
background-color: var(--bg-input) !important;
|
||||||
|
border-color: var(--border) !important;
|
||||||
|
box-shadow: 0 0 0 1px var(--border) inset !important;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .el-input__inner,
|
||||||
|
[data-theme="dark"] .el-textarea__inner {
|
||||||
|
background-color: var(--bg-input) !important;
|
||||||
|
color: var(--text) !important;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .el-button--default {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-color: var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
.theme-toggle {
|
.theme-toggle {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -79,10 +143,51 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow-md);
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
.theme-toggle:hover { transform: scale(1.1); border-color: var(--primary); }
|
.theme-toggle:hover { transform: scale(1.1); border-color: var(--primary); }
|
||||||
|
|
||||||
#app { min-height: 100vh; background: var(--bg); color: var(--text); }
|
#app { min-height: 100vh; background: var(--bg-page); color: var(--text); }
|
||||||
|
|
||||||
|
/* ── Global element-plus overrides ── */
|
||||||
|
.el-card {
|
||||||
|
border-radius: var(--radius-lg) !important;
|
||||||
|
border: 1px solid var(--border) !important;
|
||||||
|
box-shadow: var(--shadow-sm) !important;
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
.el-card:hover {
|
||||||
|
box-shadow: var(--shadow-md) !important;
|
||||||
|
}
|
||||||
|
.el-card__header {
|
||||||
|
padding: 16px 20px !important;
|
||||||
|
border-bottom: 1px solid var(--border-light) !important;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
background: var(--bg-card-header);
|
||||||
|
}
|
||||||
|
.el-card__body {
|
||||||
|
padding: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Typography helpers ── */
|
||||||
|
.form-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.section-divider {
|
||||||
|
border-top: 1px solid var(--border-light);
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Fade transition ── */
|
||||||
|
.fade-enter-active, .fade-leave-active { transition: opacity 0.2s ease; }
|
||||||
|
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,17 +7,20 @@
|
|||||||
<div class="dash-row dash-row-stats">
|
<div class="dash-row dash-row-stats">
|
||||||
<el-card
|
<el-card
|
||||||
v-for="item in statItems" :key="item.key"
|
v-for="item in statItems" :key="item.key"
|
||||||
class="stat-card stt-card"
|
class="stat-card"
|
||||||
|
shadow="never"
|
||||||
>
|
>
|
||||||
<div class="stt-label">{{ item.label }}</div>
|
<div class="stat-label">{{ item.label }}</div>
|
||||||
<div class="stt-value">{{ (stats as any)[item.key] ?? 0 }}</div>
|
<div class="stat-value">{{ (stats as any)[item.key] ?? 0 }}</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Row 2: 网盘使用空间 — flex-wrap 自适应 -->
|
<!-- Row 2: 网盘使用空间 -->
|
||||||
<div class="dash-row">
|
<div class="dash-row">
|
||||||
<el-card class="storage-section-card">
|
<el-card class="storage-card" shadow="never">
|
||||||
<template #header><span>💾 网盘使用空间</span></template>
|
<template #header>
|
||||||
|
<span>💾 网盘存储空间</span>
|
||||||
|
</template>
|
||||||
<div class="storage-grid">
|
<div class="storage-grid">
|
||||||
<div
|
<div
|
||||||
v-for="item in stats.cloudUsage" :key="item.cloudType + '-' + (item.nickname || '')"
|
v-for="item in stats.cloudUsage" :key="item.cloudType + '-' + (item.nickname || '')"
|
||||||
@@ -37,21 +40,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<el-progress
|
<el-progress
|
||||||
:percentage="cloudStoragePercent(item)"
|
:percentage="cloudStoragePercent(item)"
|
||||||
:stroke-width="12"
|
:stroke-width="10"
|
||||||
:color="cloudStoragePercent(item) > 80 ? '#f56c6c' : cloudStoragePercent(item) > 60 ? '#e6a23c' : '#67c23a'"
|
:color="cloudStoragePercent(item) > 80 ? '#f56c6c' : cloudStoragePercent(item) > 60 ? '#e6a23c' : '#67c23a'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-if="stats.cloudUsage.length === 0" description="暂无网盘数据" :image-size="80" />
|
<el-empty v-if="stats.cloudUsage.length === 0" description="暂无网盘数据" :image-size="72" />
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Row 3: 近 7 天趋势 + 热门搜索关键词 Top 20 + 操作来源 IP Top 10 -->
|
<!-- Row 3: 近 7 天趋势 + 热门搜索关键词 Top 20 + 操作来源 IP Top 10 -->
|
||||||
<div class="dash-row dash-row-cols-3">
|
<div class="dash-row dash-row-cols-3">
|
||||||
<el-card class="insight-card trend-card">
|
<el-card class="insight-card trend-card" shadow="never">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="insight-header">
|
<div class="insight-header">
|
||||||
<span>📈 网站使用趋势图</span>
|
<span>📈 使用趋势</span>
|
||||||
<div class="trend-day-btns">
|
<div class="trend-day-btns">
|
||||||
<button
|
<button
|
||||||
v-for="d in trendDayOptions" :key="d"
|
v-for="d in trendDayOptions" :key="d"
|
||||||
@@ -82,8 +85,8 @@
|
|||||||
<div ref="chartRef" class="trend-chart-echarts"></div>
|
<div ref="chartRef" class="trend-chart-echarts"></div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card class="insight-card">
|
<el-card class="insight-card" shadow="never">
|
||||||
<template #header><span>🔍 热门搜索关键词 Top 20</span></template>
|
<template #header><span>🔍 热门搜索 Top 20</span></template>
|
||||||
<div class="keyword-list">
|
<div class="keyword-list">
|
||||||
<el-tag
|
<el-tag
|
||||||
v-for="(kw, i) in stats.hotKeywords" :key="kw.keyword"
|
v-for="(kw, i) in stats.hotKeywords" :key="kw.keyword"
|
||||||
@@ -91,11 +94,11 @@
|
|||||||
size="small"
|
size="small"
|
||||||
class="keyword-tag"
|
class="keyword-tag"
|
||||||
>{{ kw.keyword }}<sup class="kw-count">{{ kw.count }}</sup></el-tag>
|
>{{ kw.keyword }}<sup class="kw-count">{{ kw.count }}</sup></el-tag>
|
||||||
<el-empty v-if="stats.hotKeywords.length === 0" description="暂无搜索数据" :image-size="60" />
|
<el-empty v-if="stats.hotKeywords.length === 0" description="暂无搜索数据" :image-size="56" />
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card class="insight-card">
|
<el-card class="insight-card" shadow="never">
|
||||||
<template #header>
|
<template #header>
|
||||||
<el-tabs v-model="rightTab" class="card-tabs" @tab-click="handleTabClick">
|
<el-tabs v-model="rightTab" class="card-tabs" @tab-click="handleTabClick">
|
||||||
<el-tab-pane label="🌐 IP Top 10" name="ip" />
|
<el-tab-pane label="🌐 IP Top 10" name="ip" />
|
||||||
@@ -109,7 +112,7 @@
|
|||||||
<span class="ip-loc" v-if="ip.ip_location">{{ formatLocation(ip.ip_location) }}</span>
|
<span class="ip-loc" v-if="ip.ip_location">{{ formatLocation(ip.ip_location) }}</span>
|
||||||
<span class="ip-count">{{ ip.count }} 次</span>
|
<span class="ip-count">{{ ip.count }} 次</span>
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-if="stats.topIps.length === 0" description="暂无数据" :image-size="60" />
|
<el-empty v-if="stats.topIps.length === 0" description="暂无数据" :image-size="56" />
|
||||||
</div>
|
</div>
|
||||||
<div class="province-list" v-show="rightTab === 'province'">
|
<div class="province-list" v-show="rightTab === 'province'">
|
||||||
<div
|
<div
|
||||||
@@ -130,16 +133,18 @@
|
|||||||
<span class="province-name">{{ item.province }}</span>
|
<span class="province-name">{{ item.province }}</span>
|
||||||
<span class="province-count">{{ item.count }} 次</span>
|
<span class="province-count">{{ item.count }} 次</span>
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-if="!stats.provinceRankings?.length" description="暂无数据" :image-size="60" />
|
<el-empty v-if="!stats.provinceRankings?.length" description="暂无数据" :image-size="56" />
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 网盘配置 - 设置及授权 -->
|
<!-- 网盘设置及授权 -->
|
||||||
<div v-if="showCloudToggle" class="section-content">
|
<div v-if="showCloudToggle" class="section-content">
|
||||||
<el-card class="config-card">
|
<el-card class="config-card" shadow="never">
|
||||||
<template #header><span>📂 网盘设置及授权</span></template>
|
<template #header>
|
||||||
|
<span>📂 网盘类型开关</span>
|
||||||
|
</template>
|
||||||
<div class="cloud-toggle-grid">
|
<div class="cloud-toggle-grid">
|
||||||
<div
|
<div
|
||||||
v-for="ct in cloudTypes"
|
v-for="ct in cloudTypes"
|
||||||
@@ -163,7 +168,6 @@
|
|||||||
<CloudConfig />
|
<CloudConfig />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- 系统配置 -->
|
<!-- 系统配置 -->
|
||||||
<div v-if="showSystemConfig" class="section-content">
|
<div v-if="showSystemConfig" class="section-content">
|
||||||
<SystemConfig :section="activeSystemSection" />
|
<SystemConfig :section="activeSystemSection" />
|
||||||
@@ -233,14 +237,11 @@ watch(() => stats.value.trendTrend, () => {
|
|||||||
watch(() => activeMenu.value, (val, oldVal) => {
|
watch(() => activeMenu.value, (val, oldVal) => {
|
||||||
if (val === 'dashboard' && oldVal !== 'dashboard') {
|
if (val === 'dashboard' && oldVal !== 'dashboard') {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// v-show keeps DOM alive; chart instance may need to be re-initialized
|
|
||||||
// after being hidden (ECharts can lose its canvas when container has display:none)
|
|
||||||
const el = document.querySelector('.trend-chart-echarts') as HTMLElement | null
|
const el = document.querySelector('.trend-chart-echarts') as HTMLElement | null
|
||||||
if (el && el.childElementCount === 0) {
|
if (el && el.childElementCount === 0) {
|
||||||
renderTrendChart()
|
renderTrendChart()
|
||||||
initTrendChart()
|
initTrendChart()
|
||||||
} else if (el) {
|
} else if (el) {
|
||||||
// Chart still exists, just resize
|
|
||||||
initTrendChart()
|
initTrendChart()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -384,7 +385,7 @@ const pageTitle = computed(() => {
|
|||||||
return '系统配置 — ' + (sysSectionTitles[activeMenu.value] || '')
|
return '系统配置 — ' + (sysSectionTitles[activeMenu.value] || '')
|
||||||
}
|
}
|
||||||
if (activeMenu.value === 'cloud-configs-toggle' || activeMenu.value === 'cloud-configs-cleanup') {
|
if (activeMenu.value === 'cloud-configs-toggle' || activeMenu.value === 'cloud-configs-cleanup') {
|
||||||
return '网盘配置'
|
return '网盘管理'
|
||||||
}
|
}
|
||||||
return pageTitles[activeMenu.value] || '管理后台'
|
return pageTitles[activeMenu.value] || '管理后台'
|
||||||
})
|
})
|
||||||
@@ -481,218 +482,60 @@ async function handleCloudToggle(type: string, enabled: boolean) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.admin-layout {
|
.section-content { }
|
||||||
display: flex;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === 左侧菜单:深色渐变背景 === */
|
/* ── Row 1: 统计卡片 ── */
|
||||||
.admin-menu {
|
.dash-row { margin-bottom: 20px; }
|
||||||
width: 220px;
|
|
||||||
min-height: 100vh;
|
|
||||||
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
|
||||||
border-right: none;
|
|
||||||
flex-shrink: 0;
|
|
||||||
transition: box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
.admin-menu :deep(.el-menu-item) {
|
|
||||||
color: #a2a3b7;
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.admin-menu :deep(.el-menu-item)::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 3px;
|
|
||||||
background: linear-gradient(180deg, #409eff, #7c3aed);
|
|
||||||
transform: scaleY(0);
|
|
||||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
border-radius: 0 2px 2px 0;
|
|
||||||
}
|
|
||||||
.admin-menu :deep(.el-menu-item.is-active) {
|
|
||||||
color: #409eff;
|
|
||||||
background: linear-gradient(90deg, rgba(64, 158, 255, 0.15) 0%, rgba(124, 58, 237, 0.08) 100%);
|
|
||||||
}
|
|
||||||
.admin-menu :deep(.el-menu-item.is-active)::after {
|
|
||||||
transform: scaleY(1);
|
|
||||||
}
|
|
||||||
.admin-menu :deep(.el-menu-item:hover) {
|
|
||||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.02) 100%);
|
|
||||||
color: #e0e0e0;
|
|
||||||
}
|
|
||||||
/* Sub-menu styles */
|
|
||||||
.admin-menu :deep(.el-sub-menu__title) {
|
|
||||||
color: #a2a3b7;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
.admin-menu :deep(.el-sub-menu__title:hover) {
|
|
||||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.02) 100%);
|
|
||||||
color: #e0e0e0;
|
|
||||||
}
|
|
||||||
.admin-menu :deep(.el-sub-menu.is-active .el-sub-menu__title) {
|
|
||||||
color: #409eff;
|
|
||||||
}
|
|
||||||
.admin-menu :deep(.el-sub-menu .el-menu) {
|
|
||||||
background: rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
.admin-menu :deep(.el-sub-menu .el-menu .el-menu-item) {
|
|
||||||
padding-left: 56px !important;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.menu-header {
|
|
||||||
padding: 24px 20px 16px;
|
|
||||||
color: #fff;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.menu-header h2 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
background: linear-gradient(90deg, #fff, #90caf9);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
.menu-header p {
|
|
||||||
font-size: 12px;
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.version-footer {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 12px;
|
|
||||||
left: 0;
|
|
||||||
width: 220px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
color: rgba(255, 255, 255, 0.35);
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
padding: 8px 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === 右侧内容区 === */
|
|
||||||
.admin-content {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
.content-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 20px 24px;
|
|
||||||
border-bottom: none;
|
|
||||||
background: linear-gradient(135deg, #f5f7fa 0%, #eef1f5 100%);
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.content-header h2 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
.content-body {
|
|
||||||
padding: 24px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === 网盘类型开关 — 网格布局 === */
|
|
||||||
.config-card {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
border-radius: 14px;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.06) !important;
|
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.config-card :deep(.el-card__header) {
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
|
||||||
padding: 16px 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
background: linear-gradient(135deg, #fafafa, #f5f5f5);
|
|
||||||
}
|
|
||||||
.config-card :deep(.el-card__body) {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.cloud-toggle-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.cloud-toggle-chip {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
transition: background 0.2s, box-shadow 0.15s;
|
|
||||||
}
|
|
||||||
.cloud-toggle-chip:hover {
|
|
||||||
background: #f0f2f5;
|
|
||||||
box-shadow: 0 2px 6px rgba(0,0,0,0.04);
|
|
||||||
}
|
|
||||||
.cloud-icon-img {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
.cloud-label {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.form-tip {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === 仪表盘布局 === */
|
|
||||||
.dash-row {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.dash-row-stats {
|
.dash-row-stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(6, 1fr);
|
grid-template-columns: repeat(6, 1fr);
|
||||||
gap: 16px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
@media (max-width: 1200px) { .dash-row-stats { grid-template-columns: repeat(3, 1fr); } }
|
@media (max-width: 1200px) { .dash-row-stats { grid-template-columns: repeat(3, 1fr); } }
|
||||||
@media (max-width: 700px) { .dash-row-stats { grid-template-columns: repeat(2, 1fr); } }
|
@media (max-width: 700px) { .dash-row-stats { grid-template-columns: repeat(2, 1fr); } }
|
||||||
|
|
||||||
/* 统计卡片 — 文字在上,数字在下 */
|
.stat-card {
|
||||||
.stt-card {
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: none !important;
|
background: var(--bg-card) !important;
|
||||||
background: linear-gradient(135deg, #f0f5ff, #e8f0fe);
|
border: 1px solid var(--border) !important;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius-lg) !important;
|
||||||
transition: transform 0.25s ease, box-shadow 0.25s ease;
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
cursor: default;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.stt-card:hover {
|
/* 统计卡片左侧装饰条 — 不同颜色区分 */
|
||||||
|
.stat-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 4px;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 14px 0 0 14px;
|
||||||
|
}
|
||||||
|
.stat-card:nth-child(1)::before { background: linear-gradient(180deg, #409eff, #79bbff); }
|
||||||
|
.stat-card:nth-child(2)::before { background: linear-gradient(180deg, #67c23a, #95d475); }
|
||||||
|
.stat-card:nth-child(3)::before { background: linear-gradient(180deg, #e6a23c, #f3d19e); }
|
||||||
|
.stat-card:nth-child(4)::before { background: linear-gradient(180deg, #7c3aed, #a78bfa); }
|
||||||
|
.stat-card:nth-child(5)::before { background: linear-gradient(180deg, #f56c6c, #f89898); }
|
||||||
|
.stat-card:nth-child(6)::before { background: linear-gradient(180deg, #36cfc9, #6fe0d9); }
|
||||||
|
.stat-card:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
box-shadow: 0 8px 24px rgba(64, 158, 255, 0.15);
|
box-shadow: 0 8px 24px rgba(0,0,0,0.08) !important;
|
||||||
}
|
}
|
||||||
.stt-card :deep(.el-card__body) {
|
.stat-card :deep(.el-card__body) {
|
||||||
padding: 20px 12px 16px;
|
padding: 22px 14px 18px !important;
|
||||||
}
|
}
|
||||||
.stt-label {
|
.stat-label {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #909399;
|
color: var(--text-tertiary);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 8px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
.stt-value {
|
.stat-value {
|
||||||
font-size: 32px;
|
font-size: 30px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
background: linear-gradient(135deg, #409eff, #7c3aed);
|
background: linear-gradient(135deg, #409eff, #7c3aed);
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
@@ -702,26 +545,35 @@ async function handleCloudToggle(type: string, enabled: boolean) {
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === 第二排:3 列洞察卡片 === */
|
/* ── Row 2: 三列洞察卡片 ── */
|
||||||
.dash-row-cols-3 {
|
.dash-row-cols-3 {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
gap: 16px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
@media (max-width: 1100px) { .dash-row-cols-3 { grid-template-columns: 1fr 1fr; } }
|
@media (max-width: 1100px) { .dash-row-cols-3 { grid-template-columns: 1fr 1fr; } }
|
||||||
@media (max-width: 700px) { .dash-row-cols-3 { grid-template-columns: 1fr; } }
|
@media (max-width: 700px) { .dash-row-cols-3 { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
.insight-card {
|
.insight-card {
|
||||||
border-radius: 14px;
|
border-radius: var(--radius-lg);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.06) !important;
|
border: 1px solid var(--border) !important;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
transition: box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
.insight-card :deep(.el-card__header) {
|
.insight-card:hover {
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
box-shadow: var(--shadow-md) !important;
|
||||||
padding: 14px 18px;
|
}
|
||||||
font-weight: 600;
|
.insight-card :deep(.el-card__body) {
|
||||||
font-size: 14px;
|
padding: 0 !important;
|
||||||
background: linear-gradient(135deg, #fafafa, #f5f5f5);
|
}
|
||||||
|
.insight-card.trend-card :deep(.el-card__body) {
|
||||||
|
padding: 8px 6px 0 !important;
|
||||||
|
height: 400px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.insight-card:not(.trend-card) :deep(.el-card__body) {
|
||||||
|
padding: 14px 18px !important;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.insight-header {
|
.insight-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -734,80 +586,63 @@ async function handleCloudToggle(type: string, enabled: boolean) {
|
|||||||
}
|
}
|
||||||
.trend-day-btn {
|
.trend-day-btn {
|
||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
border-radius: 6px;
|
border-radius: var(--radius-sm);
|
||||||
border: 1px solid #dcdfe6;
|
border: 1px solid var(--border);
|
||||||
background: #fff;
|
background: var(--bg-card);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #909399;
|
color: var(--text-tertiary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
.trend-day-btn:hover {
|
.trend-day-btn:hover {
|
||||||
border-color: #409eff;
|
border-color: var(--primary);
|
||||||
color: #409eff;
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
.trend-day-btn.active {
|
.trend-day-btn.active {
|
||||||
background: #409eff;
|
background: var(--primary);
|
||||||
border-color: #409eff;
|
border-color: var(--primary);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.insight-card :deep(.el-card__body) {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.insight-card.trend-card :deep(.el-card__body) {
|
|
||||||
padding: 8px 6px 0;
|
|
||||||
height: 380px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.insight-card:not(.trend-card) :deep(.el-card__body) {
|
|
||||||
padding: 14px 18px;
|
|
||||||
max-height: 380px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ECharts 趋势图容器 — 铺满整个卡片 */
|
/* ECharts */
|
||||||
.trend-chart-echarts {
|
.trend-chart-echarts {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 310px;
|
min-height: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 趋势汇总行 */
|
/* Trend summary */
|
||||||
.trend-summary-row {
|
.trend-summary-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 10px 12px 4px;
|
padding: 12px 14px 6px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid var(--border-light);
|
||||||
}
|
|
||||||
.trend-summary-item {
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
.trend-summary-item { text-align: center; }
|
||||||
.trend-summary-num {
|
.trend-summary-num {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 18px;
|
font-size: 17px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #303133;
|
color: var(--text);
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
.trend-summary-desc {
|
.trend-summary-desc {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #909399;
|
color: var(--text-tertiary);
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 关键词列表 */
|
/* Keywords */
|
||||||
.keyword-list {
|
.keyword-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
align-content: flex-start;
|
align-content: flex-start;
|
||||||
}
|
}
|
||||||
.keyword-tag {
|
.keyword-tag { cursor: default; }
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
.kw-count {
|
.kw-count {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
@@ -815,7 +650,7 @@ async function handleCloudToggle(type: string, enabled: boolean) {
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* IP 列表 */
|
/* IP list */
|
||||||
.ip-list {
|
.ip-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -826,12 +661,12 @@ async function handleCloudToggle(type: string, enabled: boolean) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-sm);
|
||||||
background: #f8f9fa;
|
background: var(--bg);
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
.ip-row:hover {
|
.ip-row:hover {
|
||||||
background: #f0f2f5;
|
background: var(--border-light);
|
||||||
}
|
}
|
||||||
.ip-rank {
|
.ip-rank {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
@@ -847,9 +682,9 @@ async function handleCloudToggle(type: string, enabled: boolean) {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.ip-addr {
|
.ip-addr {
|
||||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #303133;
|
color: var(--text);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -861,19 +696,19 @@ async function handleCloudToggle(type: string, enabled: boolean) {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: rgba(64, 158, 255, 0.1);
|
background: var(--primary-soft);
|
||||||
color: #409eff;
|
color: var(--primary);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.ip-count {
|
.ip-count {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #909399;
|
color: var(--text-tertiary);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 地域使用榜(省份排行) */
|
/* Province */
|
||||||
.province-list {
|
.province-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -884,11 +719,11 @@ async function handleCloudToggle(type: string, enabled: boolean) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
border-radius: 6px;
|
border-radius: var(--radius-sm);
|
||||||
background: #f8f9fa;
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
.province-row:hover {
|
.province-row:hover {
|
||||||
background: #f0f2f5;
|
background: var(--border-light);
|
||||||
}
|
}
|
||||||
.province-rank {
|
.province-rank {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
@@ -906,7 +741,7 @@ async function handleCloudToggle(type: string, enabled: boolean) {
|
|||||||
.province-bar-wrap {
|
.province-bar-wrap {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
background: #e8e8e8;
|
background: var(--border-light);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -920,7 +755,7 @@ async function handleCloudToggle(type: string, enabled: boolean) {
|
|||||||
}
|
}
|
||||||
.province-name {
|
.province-name {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #303133;
|
color: var(--text);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
@@ -929,45 +764,46 @@ async function handleCloudToggle(type: string, enabled: boolean) {
|
|||||||
}
|
}
|
||||||
.province-count {
|
.province-count {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #909399;
|
color: var(--text-tertiary);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === 第三排:网盘空间 — flex-wrap 自适应 === */
|
/* ── Storage section ── */
|
||||||
.storage-section-card {
|
.storage-card {
|
||||||
border-radius: 14px;
|
border-radius: var(--radius-lg);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.06) !important;
|
border: 1px solid var(--border) !important;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
|
||||||
}
|
|
||||||
.storage-section-card :deep(.el-card__header) {
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
|
||||||
padding: 14px 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
background: linear-gradient(135deg, #fafafa, #f5f5f5);
|
|
||||||
}
|
|
||||||
.storage-section-card :deep(.el-card__body) {
|
|
||||||
padding: 16px 20px;
|
|
||||||
}
|
}
|
||||||
.storage-grid {
|
.storage-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 16px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
.storage-drive-card {
|
.storage-drive-card {
|
||||||
flex: 1 1 280px;
|
flex: 1 1 280px;
|
||||||
min-width: 260px;
|
min-width: 260px;
|
||||||
border: 1px solid #ebeef5;
|
border: 1px solid var(--border);
|
||||||
border-radius: 12px;
|
border-radius: var(--radius-md);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: #fafbfc;
|
background: var(--bg-card);
|
||||||
transition: box-shadow 0.2s;
|
transition: box-shadow 0.2s, transform 0.2s;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.storage-drive-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, #409eff, #7c3aed);
|
||||||
}
|
}
|
||||||
.storage-drive-card:hover {
|
.storage-drive-card:hover {
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
|
box-shadow: var(--shadow-md);
|
||||||
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
.drive-header {
|
.drive-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -976,8 +812,8 @@ async function handleCloudToggle(type: string, enabled: boolean) {
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
.drive-icon {
|
.drive-icon {
|
||||||
width: 24px;
|
width: 22px;
|
||||||
height: 24px;
|
height: 22px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
@@ -985,7 +821,7 @@ async function handleCloudToggle(type: string, enabled: boolean) {
|
|||||||
.drive-name {
|
.drive-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #303133;
|
color: var(--text);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -998,74 +834,54 @@ async function handleCloudToggle(type: string, enabled: boolean) {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.drive-status.active { background: #f0f9eb; color: #67c23a; }
|
.drive-status.active { background: #f0f9eb; color: #67c23a; }
|
||||||
.drive-status.inactive { background: #fef0f0; color: #f56c6c; }
|
.drive-status.inactive { background: var(--bg); color: var(--text-tertiary); }
|
||||||
.drive-space {
|
.drive-space {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 4px;
|
gap: 3px;
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.drive-used {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
.drive-sep {
|
|
||||||
color: #c0c4cc;
|
|
||||||
}
|
|
||||||
.drive-total {
|
|
||||||
color: #909399;
|
|
||||||
}
|
}
|
||||||
|
.drive-used { color: var(--text); font-weight: 600; }
|
||||||
|
.drive-sep { color: var(--border); }
|
||||||
|
.drive-total { color: var(--text-secondary); font-weight: 500; }
|
||||||
|
|
||||||
/* Section content scroll */
|
/* ── Cloud toggle ── */
|
||||||
.section-content {
|
.config-card {
|
||||||
min-height: 400px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
.config-card :deep(.el-card__header) {
|
||||||
/* Tabs switcher inside card header */
|
background: var(--bg-card-header);
|
||||||
.insight-card :deep(.card-tabs) {
|
|
||||||
margin-top: -10px;
|
|
||||||
}
|
}
|
||||||
.insight-card :deep(.card-tabs .el-tabs__header) {
|
.cloud-toggle-grid {
|
||||||
margin: 0;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.insight-card :deep(.card-tabs .el-tabs__nav-wrap::after) {
|
.cloud-toggle-chip {
|
||||||
height: 0;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
transition: background 0.2s, box-shadow 0.15s;
|
||||||
}
|
}
|
||||||
.insight-card :deep(.card-tabs .el-tabs__item) {
|
.cloud-toggle-chip:hover {
|
||||||
height: 36px;
|
background: var(--border-light);
|
||||||
line-height: 36px;
|
box-shadow: var(--shadow-sm);
|
||||||
font-size: 13px;
|
|
||||||
padding: 0 12px;
|
|
||||||
}
|
}
|
||||||
.insight-card :deep(.card-tabs .el-tabs__active-bar) {
|
.cloud-icon-img {
|
||||||
height: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Province rows in narrow card */
|
|
||||||
.province-list .province-row {
|
|
||||||
gap: 4px;
|
|
||||||
padding: 3px 0;
|
|
||||||
}
|
|
||||||
.province-list .province-rank {
|
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
font-size: 10px;
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
.province-list .province-bar-wrap {
|
.cloud-label {
|
||||||
height: 12px;
|
font-size: 13px;
|
||||||
}
|
font-weight: 500;
|
||||||
.province-list .province-name {
|
flex: 1;
|
||||||
width: 60px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.province-list .province-count {
|
|
||||||
width: 42px;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove old tab styles */
|
|
||||||
.admin-tabs :deep(.el-tabs__header) {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,56 +1,75 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="admin-layout">
|
<div class="admin-layout">
|
||||||
|
<aside class="admin-sidebar">
|
||||||
|
<div class="sidebar-brand">
|
||||||
|
<div class="sidebar-logo">☁️</div>
|
||||||
|
<div class="sidebar-brand-text">
|
||||||
|
<h2>{{ siteName || 'CloudSearch' }}</h2>
|
||||||
|
<p>管理控制台</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-menu
|
<el-menu
|
||||||
:default-active="activeMenu"
|
:default-active="activeMenu"
|
||||||
class="admin-menu"
|
class="sidebar-menu"
|
||||||
@select="handleMenuSelect"
|
@select="handleMenuSelect"
|
||||||
>
|
>
|
||||||
<div class="menu-header">
|
|
||||||
<h2>{{ siteName || 'CloudSearch' }}</h2>
|
|
||||||
<p>管理后台</p>
|
|
||||||
</div>
|
|
||||||
<el-menu-item index="dashboard">
|
<el-menu-item index="dashboard">
|
||||||
<el-icon><DataBoard /></el-icon>
|
<el-icon><DataBoard /></el-icon>
|
||||||
<span>仪表盘</span>
|
<span>仪表盘</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
|
||||||
<el-sub-menu index="cloud-configs">
|
<el-sub-menu index="cloud-configs">
|
||||||
<template #title>
|
<template #title>
|
||||||
<el-icon><Connection /></el-icon>
|
<el-icon><Connection /></el-icon>
|
||||||
<span>网盘配置</span>
|
<span>网盘管理</span>
|
||||||
</template>
|
</template>
|
||||||
<el-menu-item index="cloud-configs-toggle">网盘设置及授权</el-menu-item>
|
<el-menu-item index="cloud-configs-toggle">📋 设置及授权</el-menu-item>
|
||||||
<el-menu-item index="cloud-configs-cleanup">存储清理</el-menu-item>
|
<el-menu-item index="cloud-configs-cleanup">🧹 存储清理</el-menu-item>
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
|
|
||||||
<el-sub-menu index="system">
|
<el-sub-menu index="system">
|
||||||
<template #title>
|
<template #title>
|
||||||
<el-icon><Setting /></el-icon>
|
<el-icon><Setting /></el-icon>
|
||||||
<span>系统配置</span>
|
<span>系统设置</span>
|
||||||
</template>
|
</template>
|
||||||
<el-menu-item index="sys-site">网站设置</el-menu-item>
|
<el-menu-item index="sys-site">🌐 网站设置</el-menu-item>
|
||||||
<el-menu-item index="sys-services">外部服务和缓存</el-menu-item>
|
<el-menu-item index="sys-services">🔗 外部服务 & 缓存</el-menu-item>
|
||||||
<el-menu-item index="sys-strategy">性能配置</el-menu-item>
|
<el-menu-item index="sys-strategy">⚡ 性能配置</el-menu-item>
|
||||||
<el-menu-item index="sys-password">修改管理员密码</el-menu-item>
|
<el-menu-item index="sys-password">🔑 修改密码</el-menu-item>
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
|
|
||||||
<el-menu-item index="save-records">
|
<el-menu-item index="save-records">
|
||||||
<el-icon><DocumentCopy /></el-icon>
|
<el-icon><DocumentCopy /></el-icon>
|
||||||
<span>转存日志</span>
|
<span>转存日志</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<div class="version-footer">T {{ appVersion }}</div>
|
|
||||||
|
<div class="sidebar-spacer"></div>
|
||||||
|
|
||||||
|
<div class="sidebar-version">v{{ appVersion }}</div>
|
||||||
|
|
||||||
<el-menu-item index="logout">
|
<el-menu-item index="logout">
|
||||||
<el-icon><SwitchButton /></el-icon>
|
<el-icon><SwitchButton /></el-icon>
|
||||||
<span>退出登录</span>
|
<span>退出登录</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<div class="admin-content">
|
<div class="admin-content">
|
||||||
<div class="content-header">
|
<header class="content-header">
|
||||||
<h2>{{ pageTitle }}</h2>
|
<div class="content-breadcrumb">
|
||||||
<el-button text @click="goBackHome">返回前台</el-button>
|
<span class="breadcrumb-current">{{ pageTitle }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-body">
|
<div class="content-actions">
|
||||||
|
<el-button text size="small" @click="goBackHome">
|
||||||
|
<el-icon><ArrowLeft /></el-icon>
|
||||||
|
返回前台
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="content-body">
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -58,7 +77,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { DataBoard, Connection, Setting, SwitchButton, DocumentCopy } from '@element-plus/icons-vue'
|
import { DataBoard, Connection, Setting, SwitchButton, DocumentCopy, ArrowLeft } from '@element-plus/icons-vue'
|
||||||
import { getSiteConfig } from '../../api'
|
import { getSiteConfig } from '../../api'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -129,14 +148,163 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.admin-menu .menu-header { padding: 16px 20px 8px; text-align: center; border-bottom: 1px solid var(--el-border-color-light); }
|
.admin-layout {
|
||||||
.admin-menu .menu-header h2 { margin: 0; font-size: 16px; color: var(--el-color-primary); }
|
display: flex;
|
||||||
.admin-menu .menu-header p { margin: 4px 0 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
height: 100vh;
|
||||||
.version-footer { padding: 8px; text-align: center; font-size: 11px; color: var(--el-text-color-placeholder); border-top: 1px solid var(--el-border-color-light); margin-top: auto; }
|
background: var(--bg-page);
|
||||||
.admin-layout { display: flex; height: 100vh; }
|
}
|
||||||
.admin-menu { width: 220px; flex-shrink: 0; display: flex; flex-direction: column; }
|
|
||||||
.admin-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
/* ── Sidebar ── */
|
||||||
.content-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 24px; border-bottom: 1px solid var(--el-border-color-light); background: var(--el-bg-color); }
|
.admin-sidebar {
|
||||||
.content-header h2 { margin: 0; font-size: 18px; }
|
width: var(--sidebar-w);
|
||||||
.content-body { flex: 1; overflow-y: auto; padding: 20px 24px; background: var(--el-bg-color-page); }
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: linear-gradient(180deg, #111827 0%, #1e293b 100%);
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 20px 20px 16px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
.sidebar-logo {
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.sidebar-brand-text h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.3;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.sidebar-brand-text p {
|
||||||
|
font-size: 11px;
|
||||||
|
margin: 2px 0 0;
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Menu ── */
|
||||||
|
.sidebar-menu {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: transparent !important;
|
||||||
|
border-right: none !important;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.sidebar-menu :deep(.el-menu-item),
|
||||||
|
.sidebar-menu :deep(.el-sub-menu__title) {
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin: 0 6px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
.sidebar-menu :deep(.el-menu-item):hover,
|
||||||
|
.sidebar-menu :deep(.el-sub-menu__title):hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
.sidebar-menu :deep(.el-menu-item.is-active) {
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(90deg, rgba(64, 158, 255, 0.25) 0%, rgba(99, 102, 241, 0.15) 100%);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.sidebar-menu :deep(.el-menu-item)::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.sidebar-menu :deep(.el-sub-menu .el-menu) {
|
||||||
|
background: rgba(0, 0, 0, 0.2) !important;
|
||||||
|
}
|
||||||
|
.sidebar-menu :deep(.el-sub-menu .el-menu .el-menu-item) {
|
||||||
|
padding-left: 52px !important;
|
||||||
|
font-size: 13px;
|
||||||
|
height: 38px;
|
||||||
|
line-height: 38px;
|
||||||
|
}
|
||||||
|
.sidebar-menu :deep(.el-icon) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.sidebar-version {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.25);
|
||||||
|
padding: 8px 0;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Content Area ── */
|
||||||
|
.admin-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 28px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.content-breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.breadcrumb-current {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.content-actions :deep(.el-button) {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 24px 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Global page save bar ── */
|
||||||
|
.content-body :deep(.save-bar) {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 32px;
|
||||||
|
right: 32px;
|
||||||
|
z-index: 100;
|
||||||
|
background: var(--bg-card);
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
transition: box-shadow 0.2s, transform 0.2s;
|
||||||
|
}
|
||||||
|
.content-body :deep(.save-bar:hover) {
|
||||||
|
box-shadow: 0 6px 24px rgba(0,0,0,0.18);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="admin-login-page">
|
<div class="admin-login-page">
|
||||||
|
<div class="login-bg-pattern"></div>
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<h1 class="login-title">{{ siteName || 'CloudSearch' }} 管理后台</h1>
|
<div class="login-brand">
|
||||||
|
<div class="login-logo">☁️</div>
|
||||||
|
<h1 class="login-title">{{ siteName || 'CloudSearch' }}</h1>
|
||||||
|
<p class="login-subtitle">管理后台</p>
|
||||||
|
</div>
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="0" size="large" @keyup.enter="handleLogin">
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="0" size="large" @keyup.enter="handleLogin">
|
||||||
<el-form-item prop="username">
|
<el-form-item prop="username">
|
||||||
<el-input v-model="form.username" placeholder="用户名" prefix-icon="User" />
|
<el-input v-model="form.username" placeholder="用户名" prefix-icon="User" />
|
||||||
@@ -11,17 +16,18 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" :loading="loading" class="login-btn" @click="handleLogin">
|
<el-button type="primary" :loading="loading" class="login-btn" @click="handleLogin">
|
||||||
登录
|
{{ loading ? '登录中...' : '登 录' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<p v-if="errorMsg" class="error-msg">{{ errorMsg }}</p>
|
<p v-if="errorMsg" class="error-msg">{{ errorMsg }}</p>
|
||||||
|
<p class="login-footer">CloudSearch v{{ appVersion }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { User, Lock } from '@element-plus/icons-vue'
|
import { User, Lock } from '@element-plus/icons-vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { getSiteConfig, adminLogin } from '../../api'
|
import { getSiteConfig, adminLogin } from '../../api'
|
||||||
@@ -31,8 +37,8 @@ const formRef = ref<InstanceType<typeof ElForm>>()
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const errorMsg = ref('')
|
const errorMsg = ref('')
|
||||||
const siteName = ref('')
|
const siteName = ref('')
|
||||||
|
const appVersion = ref('')
|
||||||
|
|
||||||
// 获取网站名称
|
|
||||||
getSiteConfig().then(cfg => {
|
getSiteConfig().then(cfg => {
|
||||||
if (cfg.site_name) siteName.value = cfg.site_name
|
if (cfg.site_name) siteName.value = cfg.site_name
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
@@ -64,6 +70,14 @@ async function handleLogin() {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const h = await fetch('/health')
|
||||||
|
const hv = await h.json()
|
||||||
|
appVersion.value = hv.version || ''
|
||||||
|
} catch {}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -72,29 +86,84 @@ async function handleLogin() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.login-bg-pattern {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 50%, rgba(102, 126, 234, 0.12) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 80% 30%, rgba(118, 75, 162, 0.12) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 50% 80%, rgba(64, 158, 255, 0.08) 0%, transparent 50%);
|
||||||
}
|
}
|
||||||
.login-card {
|
.login-card {
|
||||||
|
position: relative;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
padding: 40px;
|
padding: 48px 40px 36px;
|
||||||
background: var(--bg-white);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
border-radius: 16px;
|
backdrop-filter: blur(20px);
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
border-radius: var(--radius-xl);
|
||||||
|
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.login-brand {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 36px;
|
||||||
|
}
|
||||||
|
.login-logo {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
.login-title {
|
.login-title {
|
||||||
text-align: center;
|
font-size: 26px;
|
||||||
font-size: 24px;
|
font-weight: 800;
|
||||||
font-weight: 700;
|
color: #1d2129;
|
||||||
color: #303133;
|
margin: 0 0 4px;
|
||||||
margin-bottom: 32px;
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
.login-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #86909c;
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: 2px;
|
||||||
}
|
}
|
||||||
.login-btn {
|
.login-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
}
|
}
|
||||||
.error-msg {
|
.error-msg {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #f56c6c;
|
color: #f56c6c;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #fef0f0;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.login-footer {
|
||||||
|
text-align: center;
|
||||||
|
color: #c9cdd4;
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .login-card {
|
||||||
|
background: rgba(29, 29, 29, 0.95);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .login-title {
|
||||||
|
color: #e5e5e5;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .login-subtitle {
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .error-msg {
|
||||||
|
background: rgba(245, 108, 108, 0.12);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,53 +2,108 @@
|
|||||||
<div class="cleanup-section">
|
<div class="cleanup-section">
|
||||||
<el-card class="config-card">
|
<el-card class="config-card">
|
||||||
<template #header><span>🧹 存储清理</span></template>
|
<template #header><span>🧹 存储清理</span></template>
|
||||||
<el-form label-width="160px" label-position="left" size="small">
|
<div class="cleanup-grid">
|
||||||
<el-form-item label="启用自动清理">
|
<!-- 列1: ⏱ 基础清理策略 -->
|
||||||
<el-switch v-model="cleanupEnabled" active-text="启用" inactive-text="关闭" />
|
<div class="cleanup-group">
|
||||||
<div class="form-tip" style="margin-left: 8px;">
|
<div class="cleanup-group-label">⏱ 基础清理策略</div>
|
||||||
每天自动检查一次,将过期文件移入回收站、删除旧日志、清空回收站释放空间
|
<el-form label-width="120px" label-position="left" size="small">
|
||||||
|
<el-form-item label="自动清理">
|
||||||
|
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
|
||||||
|
<el-switch v-model="cleanupEnabled" size="small" />
|
||||||
|
<span class="cleanup-hint">每天自动检查一次,删除过期日志、移入回收站文件</span>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="云盘文件保留天数">
|
|
||||||
<el-input-number v-model="cleanupFileRetentionDays" :min="1" :max="365" style="width: 140px" />
|
|
||||||
<div class="form-tip" style="margin-left: 8px;">超过此天数的日期文件夹将被移入回收站</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="转存日志保留天数">
|
|
||||||
<el-input-number v-model="cleanupLogRetentionDays" :min="1" :max="365" style="width: 140px" />
|
|
||||||
<div class="form-tip" style="margin-left: 8px;">超过此天数的转存记录将被删除</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="清空回收站">
|
<el-form-item label="清空回收站">
|
||||||
<el-switch v-model="cleanupEmptyTrash" active-text="启用" inactive-text="关闭" />
|
<div style="display: flex; align-items: center; gap: 10px;">
|
||||||
<div class="form-tip" style="margin-left: 8px;">移入回收站后自动清空,永久删除文件以释放存储空间</div>
|
<el-switch v-model="cleanupEmptyTrash" size="small" />
|
||||||
|
<span class="cleanup-hint">清理时一并清空各网盘回收站</span>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-divider content-position="left">空间阈值自动清理</el-divider>
|
<el-form-item label="白名单目录">
|
||||||
<el-form-item label="启用空间阈值清理">
|
<div style="width: 100%;">
|
||||||
<el-switch v-model="cleanupSpaceThresholdEnabled" active-text="启用" inactive-text="关闭" />
|
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px;" v-if="whitelistDirs.length">
|
||||||
<div class="form-tip" style="margin-left: 8px;">已用空间超过阈值时,按比例删除最旧的转存文件(优先级高于保留天数)</div>
|
<el-tag v-for="(dir, i) in whitelistDirs" :key="i" closable size="small" @close="removeWhitelistDir(i)">{{ dir }}</el-tag>
|
||||||
</el-form-item>
|
</div>
|
||||||
<el-form-item v-if="cleanupSpaceThresholdEnabled" label="使用阈值">
|
<div style="display: flex; gap: 6px;">
|
||||||
<el-slider v-model="cleanupSpaceThresholdPercent" :min="50" :max="99" style="width: 200px" show-input />
|
<el-input v-model="newWhitelistDir" placeholder="输入目录名" size="small" style="width: 160px" @keyup.enter="addWhitelistDir" />
|
||||||
<div class="form-tip" style="margin-left: 8px;">已用空间超过此百分比时触发强制清理</div>
|
<el-button type="primary" size="small" @click="addWhitelistDir">添加</el-button>
|
||||||
</el-form-item>
|
</div>
|
||||||
<el-form-item v-if="cleanupSpaceThresholdEnabled" label="删除比例">
|
</div>
|
||||||
<el-slider v-model="cleanupSpaceThresholdDeletePercent" :min="5" :max="50" :step="5" style="width: 200px" show-input />
|
|
||||||
<div class="form-tip" style="margin-left: 8px;">触发清理时释放总空间的百分比(如 10% 表示累计删除最旧文件直到达到总空间的 10%,6TB 总空间 → 释放 ~600GB)</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-divider content-position="left">分享链接复用</el-divider>
|
|
||||||
<el-form-item label="复用已有分享链接">
|
|
||||||
<el-switch v-model="saveReuseEnabled" active-text="启用" inactive-text="关闭" />
|
|
||||||
<div class="form-tip" style="margin-left: 8px;">相同原始链接不再重复转存,复用已有分享链接(会验证原链接有效性;60秒内重复请求直接返回已有链接)</div>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-divider content-position="left">手动操作</el-divider>
|
|
||||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
|
||||||
<el-button type="primary" size="small" :loading="cleanupSaving" @click="handleSaveCleanupConfigs">💾 保存清理配置</el-button>
|
|
||||||
<el-button type="danger" size="small" :loading="cleanupRunning" @click="handleRunCleanup">{{ cleanupRunning ? '清理中...' : '🗑️ 立即清理' }}</el-button>
|
|
||||||
<el-button type="warning" size="small" :loading="emptyTrashRunning" @click="handleEmptyTrash">{{ emptyTrashRunning ? '清空中...' : '🧹 立即清空回收站' }}</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="lastCleanupTime" class="cleanup-info" style="margin-top: 10px;">
|
|
||||||
<span>⏰ 上次清理:{{ lastCleanupTime }}</span>
|
<!-- 列2: 📦 保留设置 -->
|
||||||
<span v-if="lastCleanupStats" style="margin-left: 16px;">📊 {{ lastCleanupStats }}</span>
|
<div class="cleanup-group">
|
||||||
|
<div class="cleanup-group-label">📦 保留设置</div>
|
||||||
|
<el-form label-width="120px" label-position="left" size="small">
|
||||||
|
<el-form-item label="文件保留">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<el-input-number v-model="cleanupFileRetentionDays" :min="1" :max="365" style="width: 100px" size="small" />
|
||||||
|
<span>天</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="日志保留">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<el-input-number v-model="cleanupLogRetentionDays" :min="1" :max="365" style="width: 100px" size="small" />
|
||||||
|
<span>天</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Cookie检测">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<el-input-number v-model="verifyIntervalMinutes" :min="5" :max="1440" :step="5" style="width: 100px" size="small" />
|
||||||
|
<span>分钟</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="空间校准">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<el-input-number v-model="storageRefreshIntervalMinutes" :min="5" :max="1440" :step="5" style="width: 100px" size="small" />
|
||||||
|
<span>分钟</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 列3: 📊 空间阈值自动清理 -->
|
||||||
|
<div class="cleanup-group">
|
||||||
|
<div class="cleanup-group-label">📊 空间阈值自动清理</div>
|
||||||
|
<el-form label-width="120px" label-position="left" size="small">
|
||||||
|
<el-form-item label="启用">
|
||||||
|
<el-switch v-model="cleanupSpaceThresholdEnabled" size="small" />
|
||||||
|
<span class="cleanup-hint" style="margin-left: 8px;">已用空间超过阈值时,按比例删除最旧的转存文件</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="cleanupSpaceThresholdEnabled" label="使用阈值">
|
||||||
|
<el-slider v-model="cleanupSpaceThresholdPercent" :min="50" :max="99" style="width: 140px" show-input size="small" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="cleanupSpaceThresholdEnabled" label="删除比例">
|
||||||
|
<el-slider v-model="cleanupSpaceThresholdDeletePercent" :min="5" :max="50" :step="5" style="width: 140px" show-input size="small" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 列4: 🔗 分享链接复用 -->
|
||||||
|
<div class="cleanup-group">
|
||||||
|
<div class="cleanup-group-label">🔗 分享链接复用</div>
|
||||||
|
<el-form label-width="120px" label-position="left" size="small">
|
||||||
|
<el-form-item label="复用">
|
||||||
|
<el-switch v-model="saveReuseEnabled" size="small" />
|
||||||
|
<span class="cleanup-hint" style="margin-left: 8px;">相同原始链接不再重复转存,复用已有分享链接(会自动验证原链接有效性;60秒内重复请求直接返回已有链接)</span>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部:手动操作(跨列全宽) -->
|
||||||
|
<div class="cleanup-actions">
|
||||||
|
<div class="cleanup-actions-buttons">
|
||||||
|
<el-button type="primary" :loading="cleanupSaving" @click="handleSaveCleanupConfigs">💾 保存清理配置</el-button>
|
||||||
|
<el-button type="danger" :loading="cleanupRunning" @click="handleRunCleanup">{{ cleanupRunning ? '清理中...' : '🗑️ 立即清理' }}</el-button>
|
||||||
|
<el-button type="warning" :loading="emptyTrashRunning" @click="handleEmptyTrash">{{ emptyTrashRunning ? '清空中...' : '🧹 清空回收站' }}</el-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="lastCleanupTime" class="cleanup-info">
|
||||||
|
⏰ 上次清理:{{ lastCleanupTime }}
|
||||||
|
<span v-if="lastCleanupStats" style="margin-left: 12px;">📊 {{ lastCleanupStats }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,12 +171,51 @@ const saveReuseEnabled = computed({
|
|||||||
set: (val: boolean) => { sysConfigs.save_reuse_enabled = val ? 'true' : 'false' },
|
set: (val: boolean) => { sysConfigs.save_reuse_enabled = val ? 'true' : 'false' },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 白名单
|
||||||
|
const whitelistDirs = ref<string[]>([])
|
||||||
|
const newWhitelistDir = ref('')
|
||||||
|
|
||||||
|
function loadWhitelistDirs() {
|
||||||
|
try {
|
||||||
|
const raw = String(sysConfigs.cleanup_whitelist_dirs || '[]')
|
||||||
|
whitelistDirs.value = JSON.parse(raw)
|
||||||
|
} catch {
|
||||||
|
whitelistDirs.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addWhitelistDir() {
|
||||||
|
const name = newWhitelistDir.value.trim()
|
||||||
|
if (!name) return
|
||||||
|
if (whitelistDirs.value.includes(name)) {
|
||||||
|
ElMessage.warning('该目录已在白名单中')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
whitelistDirs.value.push(name)
|
||||||
|
newWhitelistDir.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeWhitelistDir(index: number) {
|
||||||
|
whitelistDirs.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cookie检测间隔 + 空间校准间隔
|
||||||
|
const verifyIntervalMinutes = computed({
|
||||||
|
get: () => Number(sysConfigs.cleanup_verify_interval ?? 30),
|
||||||
|
set: (val: number) => { sysConfigs.cleanup_verify_interval = val },
|
||||||
|
})
|
||||||
|
const storageRefreshIntervalMinutes = computed({
|
||||||
|
get: () => Number(sysConfigs.storage_refresh_interval ?? 180),
|
||||||
|
set: (val: number) => { sysConfigs.storage_refresh_interval = val },
|
||||||
|
})
|
||||||
|
|
||||||
async function loadCleanupConfigs() {
|
async function loadCleanupConfigs() {
|
||||||
try {
|
try {
|
||||||
const raw = await getSystemConfigs()
|
const raw = await getSystemConfigs()
|
||||||
for (const cfg of raw) {
|
for (const cfg of raw) {
|
||||||
sysConfigs[cfg.key] = cfg.value
|
sysConfigs[cfg.key] = cfg.value
|
||||||
}
|
}
|
||||||
|
loadWhitelistDirs()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载清理配置失败', e)
|
console.error('加载清理配置失败', e)
|
||||||
}
|
}
|
||||||
@@ -130,8 +224,15 @@ async function loadCleanupConfigs() {
|
|||||||
async function handleSaveCleanupConfigs() {
|
async function handleSaveCleanupConfigs() {
|
||||||
cleanupSaving.value = true
|
cleanupSaving.value = true
|
||||||
try {
|
try {
|
||||||
const keys = ['cleanup_enabled', 'cleanup_file_retention_days', 'cleanup_log_retention_days', 'cleanup_empty_trash', 'cleanup_space_threshold_enabled', 'cleanup_space_threshold_percent', 'cleanup_space_threshold_delete_percent', 'save_reuse_enabled']
|
const keys = [
|
||||||
|
'cleanup_enabled', 'cleanup_file_retention_days', 'cleanup_log_retention_days',
|
||||||
|
'cleanup_empty_trash',
|
||||||
|
'cleanup_space_threshold_enabled', 'cleanup_space_threshold_percent', 'cleanup_space_threshold_delete_percent',
|
||||||
|
'save_reuse_enabled',
|
||||||
|
'cleanup_verify_interval', 'storage_refresh_interval',
|
||||||
|
]
|
||||||
const entries = keys.map(key => ({ key, value: String(sysConfigs[key] ?? '') }))
|
const entries = keys.map(key => ({ key, value: String(sysConfigs[key] ?? '') }))
|
||||||
|
entries.push({ key: 'cleanup_whitelist_dirs', value: JSON.stringify(whitelistDirs.value) })
|
||||||
await updateSystemConfigs(entries)
|
await updateSystemConfigs(entries)
|
||||||
ElMessage.success('清理配置已保存')
|
ElMessage.success('清理配置已保存')
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -181,14 +282,74 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.cleanup-section .config-card {
|
.cleanup-section .config-card {
|
||||||
max-width: 800px;
|
/* 全宽展示,没有max-width限制 */
|
||||||
}
|
}
|
||||||
.form-tip {
|
.cleanup-section :deep(.el-card__header) {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 2列网格布局 ── */
|
||||||
|
.cleanup-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cleanup-group {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 16px 18px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.cleanup-group:hover {
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.cleanup-group-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px dashed var(--border-light);
|
||||||
|
}
|
||||||
|
.cleanup-hint {
|
||||||
|
color: var(--text-tertiary);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--el-text-color-secondary);
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 底部操作栏(跨列全宽) ── */
|
||||||
|
.cleanup-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.cleanup-actions-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.cleanup-info {
|
.cleanup-info {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--text-tertiary);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 响应式:窄屏时改为单列 ── */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.cleanup-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -29,7 +29,9 @@
|
|||||||
<el-button @click="verifyAll">全部重新验证</el-button>
|
<el-button @click="verifyAll">全部重新验证</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table :data="configs" stripe style="width: 100%">
|
<el-card shadow="never" class="table-card">
|
||||||
|
<template #header><span>📋 网盘配置列表</span></template>
|
||||||
|
<el-table :data="configs" stripe style="width: 100%" empty-text="暂无网盘配置,点击上方「新增配置」添加">
|
||||||
<el-table-column label="网盘类型" width="110">
|
<el-table-column label="网盘类型" width="110">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<CloudBadge :cloud_type="row.cloud_type" />
|
<CloudBadge :cloud_type="row.cloud_type" />
|
||||||
@@ -121,10 +123,26 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<!-- 新增/编辑弹窗 -->
|
<!-- 新增/编辑弹窗 -->
|
||||||
<el-dialog v-model="dialogVisible" :title="editingId ? '编辑配置' : '新增配置'" width="560px">
|
<el-dialog v-model="dialogVisible" :title="editingId ? '编辑配置' : '新增配置'" width="560px">
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||||
|
<!-- 白名单目录提示 -->
|
||||||
|
<el-alert
|
||||||
|
v-show="whitelistDirs.length > 0"
|
||||||
|
type="warning"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom: 18px;"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<div style="line-height: 1.6;">
|
||||||
|
<div>🧹 请在网盘主目录内创建:<b>{{ whitelistDirs.join('、') }}</b> 目录</div>
|
||||||
|
<div>并将你的重要文件移至该目录,<b>只有这个目录不会被自动清理</b></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-alert>
|
||||||
<el-form-item label="网盘类型" prop="cloud_type">
|
<el-form-item label="网盘类型" prop="cloud_type">
|
||||||
<el-select v-model="form.cloud_type" style="width: 100%" :disabled="!!editingId" @change="onCloudTypeChange">
|
<el-select v-model="form.cloud_type" style="width: 100%" :disabled="!!editingId" @change="onCloudTypeChange">
|
||||||
<el-option
|
<el-option
|
||||||
@@ -152,11 +170,6 @@
|
|||||||
input-style="font-family: monospace; font-size: 12px;"
|
input-style="font-family: monospace; font-size: 12px;"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label=" ">
|
|
||||||
<el-button type="primary" :loading="form._verifying" @click="verifyAndFillNickname" style="width: 100%">
|
|
||||||
{{ form._verifying ? '验证中...' : '🔍 自动获取(验证 Cookie 并回填信息)' }}
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
<!-- Cookie 获取教程(根据网盘类型切换) -->
|
<!-- Cookie 获取教程(根据网盘类型切换) -->
|
||||||
<el-form-item label=" " v-if="form.cloud_type && form.cloud_type !== ''" class="cookie-tips-item">
|
<el-form-item label=" " v-if="form.cloud_type && form.cloud_type !== ''" class="cookie-tips-item">
|
||||||
<div class="cookie-tips" :class="`cookie-tips-${form.cloud_type}`">
|
<div class="cookie-tips" :class="`cookie-tips-${form.cloud_type}`">
|
||||||
@@ -182,6 +195,7 @@ import { CLOUD_LABELS } from '../../types'
|
|||||||
import type { CloudType, CloudConfig } from '../../types'
|
import type { CloudType, CloudConfig } from '../../types'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { getCloudConfigs, saveCloudConfig, updateCloudConfig, deleteCloudConfig, testCloudConnection, getCloudTypes, toggleCloudType, setPrimary } from '../../api'
|
import { getCloudConfigs, saveCloudConfig, updateCloudConfig, deleteCloudConfig, testCloudConnection, getCloudTypes, toggleCloudType, setPrimary } from '../../api'
|
||||||
|
import { getSystemConfigs } from '../../api'
|
||||||
import CloudBadge from '../../components/CloudBadge.vue'
|
import CloudBadge from '../../components/CloudBadge.vue'
|
||||||
import type { ElForm } from 'element-plus'
|
import type { ElForm } from 'element-plus'
|
||||||
|
|
||||||
@@ -305,6 +319,7 @@ const cookieTutorialHtml = computed(() => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadConfigs()
|
await loadConfigs()
|
||||||
await loadCloudTypes()
|
await loadCloudTypes()
|
||||||
|
await loadSystemConfigs()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 每30分钟自动验证一次
|
// 每30分钟自动验证一次
|
||||||
@@ -325,6 +340,27 @@ async function loadCloudTypes() {
|
|||||||
} catch (e) { console.error('加载网盘类型失败', e) }
|
} catch (e) { console.error('加载网盘类型失败', e) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 加载系统配置,提取清理白名单目录 */
|
||||||
|
const systemConfigs = ref<Record<string, string>>({})
|
||||||
|
const whitelistDirs = computed(() => {
|
||||||
|
try {
|
||||||
|
const raw = systemConfigs.value.cleanup_whitelist_dirs || '[]'
|
||||||
|
const arr = JSON.parse(raw)
|
||||||
|
return Array.isArray(arr) && arr.length > 0 ? arr : []
|
||||||
|
} catch { return [] }
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadSystemConfigs() {
|
||||||
|
try {
|
||||||
|
const list = await getSystemConfigs()
|
||||||
|
const map: Record<string, string> = {}
|
||||||
|
for (const item of list) {
|
||||||
|
map[item.key] = item.value
|
||||||
|
}
|
||||||
|
systemConfigs.value = map
|
||||||
|
} catch (e) { console.error('加载系统配置失败', e) }
|
||||||
|
}
|
||||||
|
|
||||||
async function handleCloudToggle(type: string, enabled: boolean) {
|
async function handleCloudToggle(type: string, enabled: boolean) {
|
||||||
const ct = cloudTypes.value.find(c => c.type === type)
|
const ct = cloudTypes.value.find(c => c.type === type)
|
||||||
if (!ct) return
|
if (!ct) return
|
||||||
@@ -413,33 +449,6 @@ async function verifyOne(row: CloudConfig & { _verifying?: boolean }, silent = f
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verifyAndFillNickname() {
|
|
||||||
if (!form.cookie) {
|
|
||||||
ElMessage.warning('请先输入 Cookie')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!form.cloud_type) {
|
|
||||||
ElMessage.warning('请先选择网盘类型')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
form._verifying = true
|
|
||||||
try {
|
|
||||||
const result = await testCloudConnection(form.cloud_type as CloudType, form.cookie)
|
|
||||||
if (result.success) {
|
|
||||||
if (result.nickname) form.nickname = result.nickname
|
|
||||||
if (result.storage_used) form._storageUsed = result.storage_used
|
|
||||||
if (result.storage_total) form._storageTotal = result.storage_total
|
|
||||||
ElMessage.success(`昵称:${result.nickname || '获取成功'}`)
|
|
||||||
} else {
|
|
||||||
ElMessage.warning(result.message || '验证失败,请检查 Cookie')
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
ElMessage.error(e.response?.data?.error || '验证失败,请检查 Cookie')
|
|
||||||
} finally {
|
|
||||||
form._verifying = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDialog(row: CloudConfig | null) {
|
function openDialog(row: CloudConfig | null) {
|
||||||
if (row) {
|
if (row) {
|
||||||
editingId.value = row.id ?? null
|
editingId.value = row.id ?? null
|
||||||
@@ -473,10 +482,9 @@ async function handleSave() {
|
|||||||
// 1. 表单校验(含推广账号必填)
|
// 1. 表单校验(含推广账号必填)
|
||||||
const valid = await formRef.value?.validate().catch(() => false)
|
const valid = await formRef.value?.validate().catch(() => false)
|
||||||
if (!valid) return
|
if (!valid) return
|
||||||
|
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
// 2. 如果有 Cookie,先验证 Cookie
|
// 2. 如果有 Cookie,验证 Cookie 有效/无效(不回填昵称和空间)
|
||||||
if (form.cookie) {
|
if (form.cookie) {
|
||||||
try {
|
try {
|
||||||
const verifyResult = await testCloudConnection(form.cloud_type as CloudType, form.cookie)
|
const verifyResult = await testCloudConnection(form.cloud_type as CloudType, form.cookie)
|
||||||
@@ -485,17 +493,12 @@ async function handleSave() {
|
|||||||
saving.value = false
|
saving.value = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 保存验证结果
|
|
||||||
if (verifyResult.nickname && !form.nickname) form.nickname = verifyResult.nickname
|
|
||||||
if (verifyResult.storage_used) form._storageUsed = verifyResult.storage_used
|
|
||||||
if (verifyResult.storage_total) form._storageTotal = verifyResult.storage_total
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
ElMessage.error(`Cookie验证失败:${e.response?.data?.error || '网络错误'}`)
|
ElMessage.error(`Cookie验证失败:${e.response?.data?.error || '网络错误'}`)
|
||||||
saving.value = false
|
saving.value = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 保存配置
|
// 3. 保存配置
|
||||||
if (editingId.value) {
|
if (editingId.value) {
|
||||||
await updateCloudConfig({
|
await updateCloudConfig({
|
||||||
@@ -506,8 +509,6 @@ async function handleSave() {
|
|||||||
is_transfer_enabled: form.is_transfer_enabled ? 1 : 0,
|
is_transfer_enabled: form.is_transfer_enabled ? 1 : 0,
|
||||||
cookie: form.cookie || undefined,
|
cookie: form.cookie || undefined,
|
||||||
is_active: 1,
|
is_active: 1,
|
||||||
storage_used: form._storageUsed || undefined,
|
|
||||||
storage_total: form._storageTotal || undefined,
|
|
||||||
})
|
})
|
||||||
ElMessage.success('配置更新成功')
|
ElMessage.success('配置更新成功')
|
||||||
} else {
|
} else {
|
||||||
@@ -518,8 +519,6 @@ async function handleSave() {
|
|||||||
is_transfer_enabled: form.is_transfer_enabled ? 1 : 0,
|
is_transfer_enabled: form.is_transfer_enabled ? 1 : 0,
|
||||||
cookie: form.cookie,
|
cookie: form.cookie,
|
||||||
is_active: 1,
|
is_active: 1,
|
||||||
storage_used: form._storageUsed || undefined,
|
|
||||||
storage_total: form._storageTotal || undefined,
|
|
||||||
})
|
})
|
||||||
ElMessage.success('配置保存成功')
|
ElMessage.success('配置保存成功')
|
||||||
}
|
}
|
||||||
@@ -584,16 +583,27 @@ function storageFree(row: CloudConfig): string {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.cloud-config {
|
.cloud-config {
|
||||||
background: var(--bg-white);
|
/* Uses global card styles from App.vue */
|
||||||
border-radius: var(--radius-card);
|
|
||||||
padding: 24px;
|
|
||||||
}
|
}
|
||||||
.cloud-toggle-grid { display: flex; flex-wrap: wrap; gap: 12px; }
|
|
||||||
.cloud-toggle-chip { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border: 1px solid var(--el-border-color-light); border-radius: 8px; background: var(--el-bg-color); }
|
/* ── Table card wrapper ── */
|
||||||
.cloud-toggle-chip:hover { border-color: var(--el-color-primary-light-5); }
|
.table-card {
|
||||||
.cloud-icon-img { width: 20px; height: 20px; object-fit: contain; }
|
border-radius: var(--radius-lg);
|
||||||
.cloud-label { font-size: 13px; font-weight: 500; }
|
border: 1px solid var(--border) !important;
|
||||||
.form-tip { font-size: 12px; color: var(--el-text-color-secondary); }
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.table-card :deep(.el-card__header) {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--bg-card-header);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 12px 18px;
|
||||||
|
}
|
||||||
|
.table-card :deep(.el-card__body) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Toolbar ── */
|
||||||
.toolbar {
|
.toolbar {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -601,24 +611,35 @@ function storageFree(row: CloudConfig): string {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.sign-summary-tag {
|
|
||||||
margin-left: 4px;
|
/* ── Table cells ── */
|
||||||
}
|
|
||||||
.nickname-text {
|
.nickname-text {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #303133;
|
color: var(--text);
|
||||||
}
|
}
|
||||||
.promotion-text {
|
.promotion-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #606266;
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
.uid-cell {
|
.uid-cell {
|
||||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #909399;
|
color: var(--text-tertiary);
|
||||||
letter-spacing: 0.3px;
|
letter-spacing: 0.3px;
|
||||||
}
|
}
|
||||||
/* 空间进度条 */
|
.save-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
.verifying {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Storage bar ── */
|
||||||
.storage-cell {
|
.storage-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -627,7 +648,7 @@ function storageFree(row: CloudConfig): string {
|
|||||||
}
|
}
|
||||||
.storage-bar-wrap {
|
.storage-bar-wrap {
|
||||||
height: 4px;
|
height: 4px;
|
||||||
background: #f0f2f5;
|
background: var(--border-light);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -641,44 +662,27 @@ function storageFree(row: CloudConfig): string {
|
|||||||
.storage-bar-fill.bar-danger { background: #f56c6c; }
|
.storage-bar-fill.bar-danger { background: #f56c6c; }
|
||||||
.storage-text {
|
.storage-text {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #909399;
|
color: var(--text-tertiary);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
}
|
}
|
||||||
.storage-used { color: #606266; font-weight: 600; }
|
.storage-used { color: var(--text-secondary); font-weight: 600; }
|
||||||
.storage-total { color: #303133; font-weight: 600; }
|
.storage-total { color: var(--text); font-weight: 600; }
|
||||||
.storage-free { color: #909399; }
|
.storage-free { color: var(--text-tertiary); }
|
||||||
.save-count {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
.verifying {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
:deep(.el-input-group__append) {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
:deep(.el-input-group__append .el-button) {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cookie 教程卡片 */
|
/* ── Cookie tutorial card ── */
|
||||||
.cookie-tips-item :deep(.el-form-item__content) {
|
.cookie-tips-item :deep(.el-form-item__content) {
|
||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
}
|
}
|
||||||
.cookie-tips {
|
.cookie-tips {
|
||||||
background: #f8faff;
|
background: #f8faff;
|
||||||
border: 1px solid #e8f0fe;
|
border: 1px solid #e8f0fe;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-sm);
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
color: #606266;
|
color: var(--text-secondary);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@@ -687,7 +691,7 @@ function storageFree(row: CloudConfig): string {
|
|||||||
}
|
}
|
||||||
.cookie-tips-title {
|
.cookie-tips-title {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #409eff;
|
color: var(--primary);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.cookie-tips-steps {
|
.cookie-tips-steps {
|
||||||
@@ -698,11 +702,11 @@ function storageFree(row: CloudConfig): string {
|
|||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
.cookie-tips-steps code {
|
.cookie-tips-steps code {
|
||||||
background: #ecf5ff;
|
background: var(--primary-soft);
|
||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||||
}
|
}
|
||||||
.cookie-tips-note {
|
.cookie-tips-note {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
@@ -718,4 +722,42 @@ function storageFree(row: CloudConfig): string {
|
|||||||
background: #f5f0e0;
|
background: #f5f0e0;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .cookie-tips {
|
||||||
|
background: rgba(64, 158, 255, 0.06);
|
||||||
|
border-color: rgba(64, 158, 255, 0.15);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .cookie-tips-title {
|
||||||
|
color: #66b1ff;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .cookie-tips-steps code {
|
||||||
|
background: rgba(64, 158, 255, 0.12);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .cookie-tips-note {
|
||||||
|
background: rgba(255, 193, 7, 0.1);
|
||||||
|
border-color: rgba(255, 193, 7, 0.2);
|
||||||
|
color: #d4a84b;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .cookie-tips-note code {
|
||||||
|
background: rgba(255, 193, 7, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Cloud toggle (re-used from AdminDashboard) ── */
|
||||||
|
.cloud-toggle-grid { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
|
.cloud-toggle-chip { display: flex; align-items: center; gap: 6px; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg); }
|
||||||
|
.cloud-toggle-chip:hover { border-color: var(--primary); }
|
||||||
|
.cloud-icon-img { width: 20px; height: 20px; object-fit: contain; flex-shrink: 0; }
|
||||||
|
.cloud-label { font-size: 13px; font-weight: 500; }
|
||||||
|
|
||||||
|
/* ── Dialog refinements ── */
|
||||||
|
:deep(.el-dialog__header) {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
:deep(.el-dialog__body) {
|
||||||
|
padding: 20px 24px;
|
||||||
|
}
|
||||||
|
:deep(.el-dialog__wrapper .el-dialog) {
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -74,6 +74,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Table ── -->
|
<!-- ── Table ── -->
|
||||||
|
<el-card shadow="never" class="save-table-card">
|
||||||
|
<template #header><span>📋 转存日志列表</span></template>
|
||||||
<div class="el-table-wrap">
|
<div class="el-table-wrap">
|
||||||
<el-table
|
<el-table
|
||||||
:data="records" stripe style="width: 100%"
|
:data="records" stripe style="width: 100%"
|
||||||
@@ -250,6 +252,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<!-- ── Pagination ── -->
|
<!-- ── Pagination ── -->
|
||||||
<div class="pagination-wrap" v-if="total > 0">
|
<div class="pagination-wrap" v-if="total > 0">
|
||||||
@@ -599,6 +602,20 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Table ── */
|
/* ── Table ── */
|
||||||
|
.save-table-card {
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--border) !important;
|
||||||
|
}
|
||||||
|
.save-table-card :deep(.el-card__header) {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--bg-card-header);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 12px 18px;
|
||||||
|
}
|
||||||
|
.save-table-card :deep(.el-card__body) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
.save-records :deep(.el-table) {
|
.save-records :deep(.el-table) {
|
||||||
border: 1px solid var(--el-border-color-light, #ebeef5);
|
border: 1px solid var(--el-border-color-light, #ebeef5);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
@@ -497,7 +497,6 @@
|
|||||||
<el-button type="primary" size="large" :loading="saving" @click="handleSave">
|
<el-button type="primary" size="large" :loading="saving" @click="handleSave">
|
||||||
保存配置
|
保存配置
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="large" @click="handleLogout">退出登录</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1097,16 +1096,6 @@ async function handleRemoveLogo() {
|
|||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
.save-bar {
|
|
||||||
position: sticky;
|
|
||||||
bottom: 0;
|
|
||||||
background: var(--bg-white);
|
|
||||||
padding: 16px 0;
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
margin-top: 24px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end; padding-right: 24px; gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── 搜索策略 3列网格 ── */
|
/* ── 搜索策略 3列网格 ── */
|
||||||
.strategy-grid {
|
.strategy-grid {
|
||||||
|
|||||||
Reference in New Issue
Block a user