Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a12fec4d82 | |||
| 1c0c024b9a | |||
| 359e15a82d | |||
| b7702d0285 | |||
| 37aa05b1e1 | |||
| 329256bd33 |
@@ -70,14 +70,14 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
|||||||
if (alreadySaved && recentRecord.share_url) {
|
if (alreadySaved && recentRecord.share_url) {
|
||||||
console.log(`[Share] 🛡️ Dedup: ${shareUrl} was saved ${DEDUP_WINDOW_SEC}s ago (status=${recentRecord.status}), returning existing share link`);
|
console.log(`[Share] 🛡️ Dedup: ${shareUrl} was saved ${DEDUP_WINDOW_SEC}s ago (status=${recentRecord.status}), returning existing share link`);
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`INSERT INTO save_records (source_type, source_title, source_url, target_cloud, share_url, share_pwd, file_size, file_count, folder_count, duration_ms, status, error_message, folder_name, original_folder_name, ip_address, ip_location, created_at)
|
`INSERT INTO save_records (source_type, source_title, source_url, target_cloud, share_url, share_pwd, file_size, file_count, folder_count, duration_ms, status, error_message, folder_name, original_folder_name, ip_address, ip_location, created_at, config_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
).run(
|
).run(
|
||||||
cloudType, sourceTitle || null, shareUrl, cloudType,
|
cloudType, sourceTitle || null, shareUrl, cloudType,
|
||||||
recentRecord.share_url, recentRecord.share_pwd || null,
|
recentRecord.share_url, recentRecord.share_pwd || null,
|
||||||
null, 0, 0, 0, 'reused', null,
|
null, 0, 0, 0, 'reused', null,
|
||||||
recentRecord.folder_name || null, recentRecord.original_folder_name || null,
|
recentRecord.folder_name || null, recentRecord.original_folder_name || null,
|
||||||
ipAddress || null, ipLocation, localTimestamp(),
|
ipAddress || null, ipLocation, localTimestamp(), null,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -155,7 +155,7 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
|||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let driverResult: { success: boolean; message: string; shareUrl?: string; sharePwd?: string; folderName?: string; fileCount?: number; folderCount?: number; originalFolderName?: string };
|
let driverResult: { success: boolean; message: string; shareUrl?: string; sharePwd?: string; folderName?: string; fileCount?: number; folderCount?: number; fileSize?: number; originalFolderName?: string };
|
||||||
|
|
||||||
switch (cloudType) {
|
switch (cloudType) {
|
||||||
case 'quark': {
|
case 'quark': {
|
||||||
@@ -189,16 +189,16 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
|||||||
}
|
}
|
||||||
|
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`INSERT INTO save_records (source_type, source_title, source_url, target_cloud, share_url, share_pwd, file_size, file_count, folder_count, duration_ms, status, error_message, folder_name, original_folder_name, ip_address, ip_location, created_at)
|
`INSERT INTO save_records (source_type, source_title, source_url, target_cloud, share_url, share_pwd, file_size, file_count, folder_count, duration_ms, status, error_message, folder_name, original_folder_name, ip_address, ip_location, created_at, config_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
).run(
|
).run(
|
||||||
cloudType, sourceTitle || driverResult.folderName || null, shareUrl, cloudType,
|
cloudType, sourceTitle || driverResult.folderName || null, shareUrl, cloudType,
|
||||||
driverResult.shareUrl || null, driverResult.sharePwd || null,
|
driverResult.shareUrl || null, driverResult.sharePwd || null,
|
||||||
null, driverResult.fileCount || 0, driverResult.folderCount || 0,
|
driverResult.fileSize == null ? null : String(driverResult.fileSize), driverResult.fileCount || 0, driverResult.folderCount || 0,
|
||||||
durationMs, driverResult.success ? 'success' : 'failed',
|
durationMs, driverResult.success ? 'success' : 'failed',
|
||||||
driverResult.success ? null : driverResult.message,
|
driverResult.success ? null : driverResult.message,
|
||||||
driverResult.folderName || null, driverResult.originalFolderName || null,
|
driverResult.folderName || null, driverResult.originalFolderName || null,
|
||||||
ipAddress || null, ipLocation, localTimestamp(),
|
ipAddress || null, ipLocation, localTimestamp(), config.id
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -221,9 +221,9 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
|||||||
).run(config.id);
|
).run(config.id);
|
||||||
|
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`INSERT INTO save_records (source_type, source_url, target_cloud, duration_ms, status, error_message, ip_address, ip_location, created_at)
|
`INSERT INTO save_records (source_type, source_url, target_cloud, duration_ms, status, error_message, ip_address, ip_location, created_at, config_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
).run(cloudType, shareUrl, cloudType, durationMs, 'failed', errorMessage, ipAddress || null, ipLocation, localTimestamp());
|
).run(cloudType, shareUrl, cloudType, durationMs, 'failed', errorMessage, ipAddress || null, ipLocation, localTimestamp(), null);
|
||||||
|
|
||||||
return { success: false, message: errorMessage };
|
return { success: false, message: errorMessage };
|
||||||
}
|
}
|
||||||
@@ -270,15 +270,19 @@ export function getSaveRecords(page: number = 1, pageSize: number = 20, startDat
|
|||||||
summaryConditions.push('source_type = ?'); summaryParams.push(sourceType);
|
summaryConditions.push('source_type = ?'); summaryParams.push(sourceType);
|
||||||
}
|
}
|
||||||
if (keyword) { conditions.push('source_title LIKE ?'); params.push(`%${keyword}%`); }
|
if (keyword) { conditions.push('source_title LIKE ?'); params.push(`%${keyword}%`); }
|
||||||
const where = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
|
const srWhere = conditions.length > 0 ? 'WHERE sr.' + conditions.join(' AND sr.') : '';
|
||||||
const total = (db.prepare(`SELECT COUNT(*) as count FROM save_records ${where}`).get(...params) as any).count;
|
const total = (db.prepare(`SELECT COUNT(*) as count FROM save_records ${srWhere.replace(/sr\./g, '')}`).get(...params) as any).count;
|
||||||
const records = db.prepare(
|
const records = db.prepare(
|
||||||
`SELECT * FROM save_records ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
`SELECT sr.*, cc.nickname as config_nickname
|
||||||
).all(...params, pageSize, offset) as SaveRecord[];
|
FROM save_records sr
|
||||||
|
LEFT JOIN cloud_configs cc ON sr.config_id = cc.id
|
||||||
|
${srWhere}
|
||||||
|
ORDER BY sr.created_at DESC LIMIT ? OFFSET ?`
|
||||||
|
).all(...params, pageSize, offset) as any[];
|
||||||
|
|
||||||
const summaryWhere = summaryConditions.length > 0 ? 'WHERE ' + summaryConditions.join(' AND ') : '';
|
const summaryWhere = summaryConditions.length > 0 ? 'WHERE sr.' + summaryConditions.join(' AND sr.') : '';
|
||||||
const summaryRows = db.prepare(
|
const summaryRows = db.prepare(
|
||||||
`SELECT status, COUNT(*) as cnt FROM save_records ${summaryWhere} GROUP BY status`
|
`SELECT status, COUNT(*) as cnt FROM save_records ${summaryWhere.replace(/sr\./g, '')} GROUP BY status`
|
||||||
).all(...summaryParams) as { status: string; cnt: number }[];
|
).all(...summaryParams) as { status: string; cnt: number }[];
|
||||||
let sumTotal = 0, sumSuccess = 0, sumFailed = 0, sumReused = 0;
|
let sumTotal = 0, sumSuccess = 0, sumFailed = 0, sumReused = 0;
|
||||||
for (const r of summaryRows) {
|
for (const r of summaryRows) {
|
||||||
|
|||||||
@@ -428,6 +428,8 @@ export async function getAndValidateCredential(cloudType: string, ipAddress?: st
|
|||||||
|
|
||||||
let config: CloudConfig | undefined;
|
let config: CloudConfig | undefined;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (!ipAddress) {
|
if (!ipAddress) {
|
||||||
// No IP info — fallback to simple LUR
|
// No IP info — fallback to simple LUR
|
||||||
config = db.prepare(
|
config = db.prepare(
|
||||||
@@ -454,8 +456,15 @@ export async function getAndValidateCredential(cloudType: string, ipAddress?: st
|
|||||||
|
|
||||||
const ipTodayCount = ipCountRow?.total || 0;
|
const ipTodayCount = ipCountRow?.total || 0;
|
||||||
|
|
||||||
if (ipTodayCount < 3) {
|
// How many primary accounts does this cloud type have?
|
||||||
// First 2 saves — use a primary account (is_primary=1), fallback to any healthy
|
const primaryCountRow = db.prepare(
|
||||||
|
`SELECT COUNT(*) as c FROM cloud_configs WHERE cloud_type = ? AND is_primary = 1 AND is_active = 1`
|
||||||
|
).get(cloudType) as { c: number };
|
||||||
|
const primaryCount = primaryCountRow?.c || 0;
|
||||||
|
const primaryThreshold = primaryCount * 2; // Each primary account gets 2 uses per IP
|
||||||
|
|
||||||
|
if (ipTodayCount < primaryThreshold) {
|
||||||
|
// First N saves (primaryCount × 2) — use primary accounts (is_primary=1), fallback to any healthy
|
||||||
config = db.prepare(
|
config = db.prepare(
|
||||||
`SELECT * FROM cloud_configs
|
`SELECT * FROM cloud_configs
|
||||||
WHERE cloud_type = ? AND is_active = 1
|
WHERE cloud_type = ? AND is_active = 1
|
||||||
@@ -464,7 +473,7 @@ export async function getAndValidateCredential(cloudType: string, ipAddress?: st
|
|||||||
LIMIT 1`
|
LIMIT 1`
|
||||||
).get(cloudType) as CloudConfig | undefined;
|
).get(cloudType) as CloudConfig | undefined;
|
||||||
} else {
|
} else {
|
||||||
// 3rd+ save — exclude accounts this IP has already used today,
|
// After primary threshold — exclude accounts this IP has already used today,
|
||||||
// fall back to other available accounts round-robin
|
// fall back to other available accounts round-robin
|
||||||
const usedConfigIds = db.prepare(
|
const usedConfigIds = db.prepare(
|
||||||
`SELECT DISTINCT config_id FROM ip_daily_save_counts
|
`SELECT DISTINCT config_id FROM ip_daily_save_counts
|
||||||
|
|||||||
@@ -166,12 +166,13 @@ async function batchDeleteFiles(
|
|||||||
export async function createWarningDirectories(
|
export async function createWarningDirectories(
|
||||||
cookie: string,
|
cookie: string,
|
||||||
dirNames: string[],
|
dirNames: string[],
|
||||||
|
parentDirFid: string = "0",
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!dirNames.length) return;
|
if (!dirNames.length) return;
|
||||||
|
|
||||||
// 先获取根目录下所有文件夹,避免重复创建
|
// 先获取根目录下所有文件夹,避免重复创建
|
||||||
await humanDelay();
|
await humanDelay();
|
||||||
const rootFiles = await listDirAllPages(cookie, "0");
|
const rootFiles = await listDirAllPages(cookie, parentDirFid);
|
||||||
const existingDirs = new Set(
|
const existingDirs = new Set(
|
||||||
rootFiles.filter((f) => f.dir).map((f) => f.file_name),
|
rootFiles.filter((f) => f.dir).map((f) => f.file_name),
|
||||||
);
|
);
|
||||||
@@ -192,7 +193,7 @@ export async function createWarningDirectories(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await createSingleDir(cookie, formattedName);
|
await createSingleDir(cookie, formattedName, parentDirFid);
|
||||||
// 加入已存在集合,防止同名重试
|
// 加入已存在集合,防止同名重试
|
||||||
existingDirs.add(formattedName);
|
existingDirs.add(formattedName);
|
||||||
}
|
}
|
||||||
@@ -204,6 +205,7 @@ export async function createWarningDirectories(
|
|||||||
async function createSingleDir(
|
async function createSingleDir(
|
||||||
cookie: string,
|
cookie: string,
|
||||||
dirName: string,
|
dirName: string,
|
||||||
|
pdirFid: string = "0",
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(
|
const resp = await fetch(
|
||||||
@@ -215,7 +217,7 @@ async function createSingleDir(
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
pdir_fid: "0",
|
pdir_fid: pdirFid,
|
||||||
file_name: dirName,
|
file_name: dirName,
|
||||||
dir: true,
|
dir: true,
|
||||||
dir_path: "",
|
dir_path: "",
|
||||||
@@ -276,7 +278,7 @@ export async function runAdCleanup(
|
|||||||
console.log(
|
console.log(
|
||||||
`[Quark-AdCleanup] 开始创建警示文件夹: ${warningNames.length} 个`,
|
`[Quark-AdCleanup] 开始创建警示文件夹: ${warningNames.length} 个`,
|
||||||
);
|
);
|
||||||
await createWarningDirectories(cookie, warningNames);
|
await createWarningDirectories(cookie, warningNames, savedDirFid);
|
||||||
warningDirs = warningNames.length;
|
warningDirs = warningNames.length;
|
||||||
console.log(
|
console.log(
|
||||||
`[Quark-AdCleanup] 警示文件夹创建完成(共 ${warningDirs} 个)`,
|
`[Quark-AdCleanup] 警示文件夹创建完成(共 ${warningDirs} 个)`,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export async function saveFromShare(
|
|||||||
renamed?: string[];
|
renamed?: string[];
|
||||||
fileCount?: number;
|
fileCount?: number;
|
||||||
folderCount?: number;
|
folderCount?: number;
|
||||||
|
fileSize?: number;
|
||||||
originalFolderName?: string;
|
originalFolderName?: string;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
@@ -203,6 +204,10 @@ export async function saveFromShare(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate total file size
|
||||||
|
const allFiles = topDir && childFiles ? childFiles : topFiles;
|
||||||
|
const fileSize = allFiles.reduce((sum, f) => sum + (Number(f.size) || 0), 0);
|
||||||
|
|
||||||
const renameMsg = renamed.length > 0
|
const renameMsg = renamed.length > 0
|
||||||
? `,已重命名 ${renamed.length} 个文件`
|
? `,已重命名 ${renamed.length} 个文件`
|
||||||
: '';
|
: '';
|
||||||
@@ -218,6 +223,7 @@ export async function saveFromShare(
|
|||||||
renamed: renamed.map(r => `${r.original} → ${r.renamed}`),
|
renamed: renamed.map(r => `${r.original} → ${r.renamed}`),
|
||||||
fileCount,
|
fileCount,
|
||||||
folderCount,
|
folderCount,
|
||||||
|
fileSize,
|
||||||
originalFolderName,
|
originalFolderName,
|
||||||
};
|
};
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -138,27 +138,6 @@ function runMigrations(db: Database.Database): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 迁移: 给已有 save_records 表补充新列 */
|
/** 迁移: 给已有 save_records 表补充新列 */
|
||||||
function migrateSaveRecords(db: Database.Database): void {
|
|
||||||
const newCols: { col: string; def: string }[] = [
|
|
||||||
{ col: 'share_pwd', def: 'TEXT' },
|
|
||||||
{ col: 'file_count', def: 'INTEGER DEFAULT 0' },
|
|
||||||
{ col: 'folder_count', def: 'INTEGER DEFAULT 0' },
|
|
||||||
{ col: 'duration_ms', def: 'INTEGER DEFAULT 0' },
|
|
||||||
{ col: 'status', def: "TEXT NOT NULL DEFAULT ''" },
|
|
||||||
{ col: 'error_message', def: 'TEXT' },
|
|
||||||
{ col: 'folder_name', def: 'TEXT' },
|
|
||||||
{ col: 'request_url', def: 'TEXT' },
|
|
||||||
{ col: 'ip_location', def: 'TEXT' },
|
|
||||||
{ col: 'original_folder_name', def: 'TEXT' },
|
|
||||||
];
|
|
||||||
for (const { col, def } of newCols) {
|
|
||||||
try {
|
|
||||||
db.exec(`ALTER TABLE save_records ADD COLUMN ${col} ${def}`);
|
|
||||||
} catch {
|
|
||||||
// Column already exists — ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 迁移: 给 content_cache 表加 douban_url 列 */
|
/** 迁移: 给 content_cache 表加 douban_url 列 */
|
||||||
function migrateContentCache(db: Database.Database): void {
|
function migrateContentCache(db: Database.Database): void {
|
||||||
@@ -263,6 +242,14 @@ function migrateCloudConfigs(db: Database.Database): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 迁移: 给 save_records 表加 config_id 字段 */
|
||||||
|
function migrateSaveRecords(db: any): void {
|
||||||
|
if (!db.prepare("SELECT sql FROM sqlite_master WHERE name='save_records' AND sql LIKE '%config_id%'").get()) {
|
||||||
|
db.exec("ALTER TABLE save_records ADD COLUMN config_id INTEGER DEFAULT NULL");
|
||||||
|
console.log('[DB] save_records migration: config_id column added');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function seedAdmin(db: Database.Database): void {
|
function seedAdmin(db: Database.Database): void {
|
||||||
const existing = db.prepare('SELECT id FROM admins WHERE username = ?').get(config.adminUsername);
|
const existing = db.prepare('SELECT id FROM admins WHERE username = ?').get(config.adminUsername);
|
||||||
if (existing) return;
|
if (existing) return;
|
||||||
|
|||||||
@@ -85,15 +85,15 @@
|
|||||||
<el-table-column type="expand" width="36">
|
<el-table-column type="expand" width="36">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="expand-detail">
|
<div class="expand-detail">
|
||||||
<!-- Row 1: 原始链接 + 文件夹数量 + 文件数量 -->
|
<!-- Row 1: 原始链接 + 文件大小 + 文件夹+文件数 -->
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<div class="detail-cell">
|
<div class="detail-cell" style="flex:2">
|
||||||
<span class="detail-label">原始链接</span>
|
<span class="detail-label">原始链接</span>
|
||||||
<a :href="row.source_url" target="_blank" class="detail-link">{{ row.source_url }}</a>
|
<a :href="row.source_url" target="_blank" class="detail-link">{{ row.source_url }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-cell" v-if="row.original_folder_name">
|
<div class="detail-cell" v-if="row.file_size">
|
||||||
<span class="detail-label">原始文件夹名</span>
|
<span class="detail-label">文件大小</span>
|
||||||
<code class="detail-code">{{ row.original_folder_name }}</code>
|
<code class="detail-code">{{ formatFileSize(row.file_size) }}</code>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-cell" v-if="row.status !== 'reused' && (row.folder_count > 0 || row.file_count > 0)">
|
<div class="detail-cell" v-if="row.status !== 'reused' && (row.folder_count > 0 || row.file_count > 0)">
|
||||||
<span class="detail-label">文件夹</span>
|
<span class="detail-label">文件夹</span>
|
||||||
@@ -108,9 +108,24 @@
|
|||||||
<span class="reuse-msg">♻️ 直接使用已有分享链接,无需实际转存</span>
|
<span class="reuse-msg">♻️ 直接使用已有分享链接,无需实际转存</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Row 2: 分享链接 + 分享密码 + 转存文件夹 -->
|
<!-- Row 2: 使用账号 + 原始文件夹名 -->
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<div class="detail-cell" v-if="row.share_url">
|
<div class="detail-cell" v-if="row.config_nickname">
|
||||||
|
<span class="detail-label">使用账号</span>
|
||||||
|
<el-tag size="small" type="success" effect="plain">{{ row.config_nickname }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="detail-cell" v-if="row.original_folder_name">
|
||||||
|
<span class="detail-label">原始文件夹名</span>
|
||||||
|
<code class="detail-code">{{ row.original_folder_name }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="detail-cell" v-if="row.folder_name">
|
||||||
|
<span class="detail-label">转存文件夹</span>
|
||||||
|
<code class="detail-code">{{ row.folder_name }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Row 3: 分享链接 + 分享密码 + 耗时 -->
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="detail-cell" v-if="row.share_url" style="flex:2">
|
||||||
<span class="detail-label">分享链接</span>
|
<span class="detail-label">分享链接</span>
|
||||||
<a :href="row.share_url" target="_blank" class="detail-link">{{ row.share_url }}</a>
|
<a :href="row.share_url" target="_blank" class="detail-link">{{ row.share_url }}</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,12 +133,12 @@
|
|||||||
<span class="detail-label">分享密码</span>
|
<span class="detail-label">分享密码</span>
|
||||||
<el-tag size="small" type="warning">{{ row.share_pwd }}</el-tag>
|
<el-tag size="small" type="warning">{{ row.share_pwd }}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-cell" v-if="row.folder_name">
|
<div class="detail-cell">
|
||||||
<span class="detail-label">转存文件夹</span>
|
<span class="detail-label">耗时</span>
|
||||||
<code class="detail-code">{{ row.folder_name }}</code>
|
<span :class="['detail-duration', durationClass(row.duration_ms)]">{{ formatDuration(row.duration_ms) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Row 3: IP地址 + 归属地 -->
|
<!-- Row 4: IP地址 + 归属地 + 创建时间 -->
|
||||||
<div class="detail-row" v-if="row.ip_address">
|
<div class="detail-row" v-if="row.ip_address">
|
||||||
<div class="detail-cell">
|
<div class="detail-cell">
|
||||||
<span class="detail-label">IP 地址</span>
|
<span class="detail-label">IP 地址</span>
|
||||||
@@ -133,8 +148,12 @@
|
|||||||
<span class="detail-label">归属地</span>
|
<span class="detail-label">归属地</span>
|
||||||
<code class="detail-code">{{ formatLocation(row.ip_location) }}</code>
|
<code class="detail-code">{{ formatLocation(row.ip_location) }}</code>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="detail-cell">
|
||||||
|
<span class="detail-label">时间</span>
|
||||||
|
<code class="detail-code">{{ formatTime(row.created_at) }}</code>
|
||||||
</div>
|
</div>
|
||||||
<!-- Row 4: 错误信息(整行) -->
|
</div>
|
||||||
|
<!-- Row 5: 错误信息(整行) -->
|
||||||
<div class="detail-row" v-if="row.status === 'failed' && row.error_message">
|
<div class="detail-row" v-if="row.status === 'failed' && row.error_message">
|
||||||
<div class="detail-cell detail-full">
|
<div class="detail-cell detail-full">
|
||||||
<span class="detail-label">错误信息</span>
|
<span class="detail-label">错误信息</span>
|
||||||
@@ -314,7 +333,7 @@ function formatTime(t: string): string {
|
|||||||
const d = new Date(ts)
|
const d = new Date(ts)
|
||||||
if (isNaN(d.getTime())) return t
|
if (isNaN(d.getTime())) return t
|
||||||
const pad = (n: number) => String(n).padStart(2, '0')
|
const pad = (n: number) => String(n).padStart(2, '0')
|
||||||
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDuration(ms: number): string {
|
function formatDuration(ms: number): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user