feat: 支持ZIP压缩包上传(含密码保护)

This commit is contained in:
CHE LIANG ZHAO
2026-01-23 13:46:45 +08:00
parent 49e3176e6b
commit a97a8d6a20
22 changed files with 973 additions and 72 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "web",
"private": true,
"version": "1.0.9",
"version": "1.1.0",
"type": "module",
"scripts": {
"dev": "vite dev",

View File

@@ -100,7 +100,7 @@ export interface MonthlyStatsResponse {
export async function uploadBill(
file: File,
type: BillType,
options?: { year?: number; month?: number }
options?: { year?: number; month?: number; password?: string }
): Promise<UploadResponse> {
const formData = new FormData();
formData.append('file', file);
@@ -112,6 +112,9 @@ export async function uploadBill(
if (options?.month) {
formData.append('month', options.month.toString());
}
if (options?.password) {
formData.append('password', options.password);
}
const response = await apiFetch(`${API_BASE}/api/upload`, {
method: 'POST',

View File

@@ -23,6 +23,8 @@
let isUploading = $state(false);
let uploadResult: UploadResponse | null = $state(null);
let errorMessage = $state('');
let zipPassword = $state('');
let isZipFile = $state(false);
type StatTrend = 'up' | 'down';
interface StatCard {
@@ -186,16 +188,27 @@
}
function selectFile(file: File) {
if (!file.name.endsWith('.csv')) {
errorMessage = '请选择 CSV 格式的账单文件';
const fileName = file.name.toLowerCase();
const isZip = fileName.endsWith('.zip');
const isCsv = fileName.endsWith('.csv');
const isXlsx = fileName.endsWith('.xlsx');
if (!isCsv && !isZip && !isXlsx) {
errorMessage = '请选择 CSV、XLSX 或 ZIP 格式的账单文件';
return;
}
selectedFile = file;
isZipFile = isZip;
errorMessage = '';
uploadResult = null;
// 如果不是 ZIP 文件,清空密码
if (!isZip) {
zipPassword = '';
}
// 根据文件名自动识别账单类型
const fileName = file.name.toLowerCase();
if (fileName.includes('支付宝') || fileName.includes('alipay')) {
selectedType = 'alipay';
} else if (fileName.includes('微信') || fileName.includes('wechat')) {
@@ -207,6 +220,8 @@
selectedFile = null;
uploadResult = null;
errorMessage = '';
zipPassword = '';
isZipFile = false;
}
async function handleUpload() {
@@ -216,7 +231,11 @@
errorMessage = '';
try {
const result = await uploadBill(selectedFile, selectedType);
const options: { year?: number; month?: number; password?: string } = {};
if (isZipFile && zipPassword) {
options.password = zipPassword;
}
const result = await uploadBill(selectedFile, selectedType, options);
if (result.result) {
uploadResult = result;
} else {
@@ -278,7 +297,7 @@
<Card.Header class="flex flex-row items-center justify-between space-y-0">
<div>
<Card.Title>上传账单</Card.Title>
<Card.Description>支持支付宝、微信账单 CSV 文件</Card.Description>
<Card.Description>支持支付宝、微信账单 CSV、XLSX 或 ZIP 文件</Card.Description>
</div>
<Button variant="outline" size="sm" onclick={() => goto('/bills?tab=manual')}>
<Plus class="mr-2 h-4 w-4" />
@@ -301,7 +320,7 @@
<input
type="file"
id="file-input"
accept=".csv"
accept=".csv,.xlsx,.zip"
onchange={handleFileSelect}
hidden
/>
@@ -333,7 +352,7 @@
<p class="font-medium">
{isDragOver ? '松开鼠标上传文件' : '拖拽文件到这里,或点击选择'}
</p>
<p class="text-sm text-muted-foreground">支持 .csv 格式</p>
<p class="text-sm text-muted-foreground">支持 .csv、.xlsx、.zip 格式</p>
</div>
</div>
{/if}
@@ -347,6 +366,19 @@
</div>
{/if}
<!-- ZIP 密码输入 -->
{#if isZipFile}
<div class="flex items-center gap-3">
<span class="text-sm font-medium">ZIP 密码:</span>
<input
type="password"
bind:value={zipPassword}
placeholder="如有密码请输入"
class="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
/>
</div>
{/if}
<!-- 账单类型选择 -->
<div class="flex items-center gap-3">
<span class="text-sm font-medium">账单类型:</span>