Web 端本地缓存、首页与会员弹窗体验优化

新增 IndexedDB 与最近文件 L1 缓存、云保存/打开链路;首页支持下载与清空本地缓存;修复清空 storage 后语言初始化报错;优化 design2 背景加载与侧栏标签样式;完善会员激活/支付弹层样式;后端补充文件缩略图字段与 thumb 接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
24kycj
2026-06-11 11:05:44 +08:00
parent 877fd278c2
commit 4ce4486b26
33 changed files with 3190 additions and 1143 deletions
+36 -6
View File
@@ -55,7 +55,8 @@ final class FileService
public static function list(int $userId, int $limit, int $offset): array
{
$stmt = Db::pdo()->prepare(
'SELECT id, name, size, version, updated_at, created_at '
'SELECT id, name, size, version, updated_at, created_at, '
. '(CASE WHEN thumb IS NOT NULL AND thumb <> "" THEN 1 ELSE 0 END) AS has_thumb '
. 'FROM soon_files WHERE user_id = :u AND deleted_at IS NULL '
. 'ORDER BY updated_at DESC LIMIT :lim OFFSET :off'
);
@@ -63,7 +64,12 @@ final class FileService
$stmt->bindValue('lim', $limit, \PDO::PARAM_INT);
$stmt->bindValue('off', $offset, \PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
$rows = $stmt->fetchAll();
foreach ($rows as &$row) {
$row['has_thumb'] = (int)($row['has_thumb'] ?? 0) === 1;
}
unset($row);
return $rows;
}
public static function create(int $userId, string $name, string $json): array
@@ -79,11 +85,15 @@ final class FileService
Json::fail('quota_exceeded', '存储空间已满,请清理文件或续订', 413);
}
$now = date('Y-m-d H:i:s');
$thumb = TemplateService::thumbFromSoonJson($json);
$stmt = Db::pdo()->prepare(
'INSERT INTO soon_files (user_id, name, json, size, version, created_at, updated_at) '
. 'VALUES (:u, :n, :j, :s, 1, :created_at, :updated_at)'
'INSERT INTO soon_files (user_id, name, json, size, version, thumb, created_at, updated_at) '
. 'VALUES (:u, :n, :j, :s, 1, :th, :created_at, :updated_at)'
);
$stmt->execute(['u' => $userId, 'n' => $name, 'j' => $json, 's' => $size, 'created_at' => $now, 'updated_at' => $now]);
$stmt->execute([
'u' => $userId, 'n' => $name, 'j' => $json, 's' => $size, 'th' => $thumb !== '' ? $thumb : null,
'created_at' => $now, 'updated_at' => $now,
]);
$id = (int)Db::pdo()->lastInsertId();
return ['id' => $id, 'name' => $name, 'size' => $size, 'version' => 1, 'updated_at' => $now];
}
@@ -111,12 +121,14 @@ final class FileService
}
$now = date('Y-m-d H:i:s');
$newVersion = (int)$row['version'] + 1;
$thumb = TemplateService::thumbFromSoonJson($json);
$upd = $pdo->prepare(
'UPDATE soon_files SET name = :n, json = :j, size = :s, version = :v, updated_at = :ts '
'UPDATE soon_files SET name = :n, json = :j, size = :s, version = :v, thumb = :th, updated_at = :ts '
. 'WHERE id = :id AND version = :cv'
);
$upd->execute([
'n' => $name, 'j' => $json, 's' => $newSize, 'v' => $newVersion,
'th' => $thumb !== '' ? $thumb : null,
'ts' => $now, 'id' => $id, 'cv' => (int)$row['version'],
]);
if ($upd->rowCount() === 0) {
@@ -152,4 +164,22 @@ final class FileService
}
return $row;
}
public static function outputThumb(int $userId, int $id): void
{
$row = self::fetch($userId, $id);
$thumb = trim((string)($row['thumb'] ?? ''));
if ($thumb === '' || !str_starts_with($thumb, 'data:image/')) {
$thumb = TemplateService::thumbFromSoonJson((string)$row['json']);
if ($thumb !== '') {
Db::pdo()->prepare('UPDATE soon_files SET thumb = :th WHERE id = :id AND user_id = :u')
->execute(['th' => $thumb, 'id' => $id, 'u' => $userId]);
}
}
if ($thumb === '') {
http_response_code(404);
exit;
}
TemplateService::outputThumbDataUrl($thumb, (string)$row['updated_at']);
}
}
@@ -372,12 +372,12 @@ final class MembershipService
$fileStmt->execute(['u' => $userId]);
$row = $fileStmt->fetch() ?: ['files_count' => 0, 'storage_bytes' => 0];
$bytes = (int)$row['storage_bytes'];
$usedMb = $bytes > 0 ? round($bytes / 1024 / 1024, 2) : 0;
$usedMb = $bytes > 0 ? round($bytes / 1024 / 1024, 2) : 0.0;
return [
'files_count' => (int)$row['files_count'],
'storage_bytes' => $bytes,
'used_mb' => $usedMb,
'used_display' => self::formatQuota(max(1, $usedMb)) !== '0 MB' ? self::formatBytes($bytes) : '0 MB',
'used_display' => $bytes > 0 ? self::formatBytes($bytes) : '0 MB',
];
}
+11 -1
View File
@@ -434,12 +434,22 @@ final class TemplateService
}
header('Content-Type: application/json; charset=utf-8');
header('Content-Disposition: inline; filename="' . str_replace('"', '', $name) . '.soon"');
header('Cache-Control: public, max-age=60, must-revalidate');
header('Cache-Control: public, max-age=86400, must-revalidate');
header('ETag: "' . md5((string)$row['updated_at'] . ':' . (int)$row['file_size']) . '"');
readfile($path);
exit;
}
public static function thumbFromSoonJson(string $json): string
{
return self::parseSoonMeta($json)['thumb'];
}
public static function outputThumbDataUrl(string $thumb, string $rev): void
{
self::emitThumbBinary($thumb, $rev);
}
private static function writeFile(int $id, string $json): void
{
$path = self::storageDir() . DIRECTORY_SEPARATOR . $id . '.soon';