diff --git a/client/src/pages/profile/index.vue b/client/src/pages/profile/index.vue index e1d7533..16443d3 100644 --- a/client/src/pages/profile/index.vue +++ b/client/src/pages/profile/index.vue @@ -51,7 +51,7 @@ 分类管理 - + 数据导出 @@ -94,6 +94,60 @@ + + + + 数据导出 + + + + 日期范围 + + + + + {{ exportStartDate }} + + + + + + + {{ exportEndDate }} + + + + + + + + 交易类型 + + 全部 + 仅支出 + 仅收入 + + + + + + 导出格式 + + CSV + JSON + + + + + + {{ exportCount }} 条记录 + + {{ exportLoading ? '导出中...' : '导出 ' + exportFormat.toUpperCase() }} + + + + + @@ -163,6 +217,35 @@ const showAbout = ref(false) const showIdentity = ref(false) const initialLoaded = ref(false) +// 导出面板状态 +const showExportPanel = ref(false) +const exportStartDate = ref(getLocalDateStr().replace(/-/g, '-').replace(/(\d+)$/, '01')) // 本月1日 +const exportEndDate = ref(getLocalDateStr()) +const exportType = ref<'all' | 'expense' | 'income'>('all') +const exportFormat = ref<'csv' | 'json'>('csv') +const exportCount = ref(0) +const exportLoading = ref(false) + +function openExportPanel() { + showExportPanel.value = true + updateExportCount() +} + +async function updateExportCount() { + try { + const data = await getTransactions({ + page: 1, pageSize: 1, + startDate: exportStartDate.value, + endDate: exportEndDate.value, + ...(exportType.value !== 'all' ? { type: exportType.value } : {}), + group_id: groupStore.currentGroupId + }) + exportCount.value = data.total + } catch { + exportCount.value = 0 + } +} + onMounted(async () => { await waitForReady() loading.value = true @@ -323,19 +406,28 @@ function getLocalDateStr(): string { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` } -async function exportData() { +async function doExport() { + if (exportCount.value === 0 || exportLoading.value) return + exportLoading.value = true uni.showLoading({ title: '导出中 0%' }) try { - // 分页获取全部数据(服务端 pageSize 上限 100) + // 分页获取筛选后的数据 const allList: Transaction[] = [] let page = 1 let total = 0 + const baseParams: Record = { + pageSize: 100, + startDate: exportStartDate.value, + endDate: exportEndDate.value, + group_id: groupStore.currentGroupId + } + if (exportType.value !== 'all') baseParams.type = exportType.value + do { - const data = await getTransactions({ page, pageSize: 100, group_id: groupStore.currentGroupId }) + const data = await getTransactions({ page, ...baseParams } as any) allList.push(...data.list) total = data.total page++ - // 更新进度 const progress = Math.min(100, Math.round((allList.length / total) * 100)) uni.showLoading({ title: `导出中 ${progress}%` }) } while (allList.length < total) @@ -346,50 +438,71 @@ async function exportData() { return } - function csvEscape(val: string): string { - if (val.includes(',') || val.includes('"') || val.includes('\n')) { - return '"' + val.replace(/"/g, '""') + '"' + if (exportFormat.value === 'json') { + const json = JSON.stringify(allList.map(t => ({ + date: t.date, + type: t.type === 'expense' ? '支出' : '收入', + category: t.category_name || '未分类', + amount: (t.amount / 100).toFixed(2), + note: t.note || '' + })), null, 2) + + const fileName = `小菜记账_${exportStartDate.value}_${exportEndDate.value}.json` + saveFile(json, fileName, 'application/json') + } else { + function csvEscape(val: string): string { + if (val.includes(',') || val.includes('"') || val.includes('\n')) { + return '"' + val.replace(/"/g, '""') + '"' + } + return val } - return val + const header = '日期,类型,分类,金额(元),备注\n' + const rows = allList.map(t => { + const amount = (t.amount / 100).toFixed(2) + const type = t.type === 'expense' ? '支出' : '收入' + return [t.date, type, t.category_name || '未分类', amount, t.note || ''].map(csvEscape).join(',') + }).join('\n') + + const csv = '' + header + rows + const fileName = `小菜记账_${exportStartDate.value}_${exportEndDate.value}.csv` + saveFile(csv, fileName, 'text/csv') } - const header = '日期,类型,分类,金额(元),备注\n' - const rows = allList.map(t => { - const amount = (t.amount / 100).toFixed(2) - const type = t.type === 'expense' ? '支出' : '收入' - return [t.date, type, t.category_name || '未分类', amount, t.note || ''].map(csvEscape).join(',') - }).join('\n') - - const csv = '' + header + rows - - // #ifdef H5 - const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = `小菜记账_${getLocalDateStr()}.csv` - a.click() - URL.revokeObjectURL(url) - uni.hideLoading() - uni.showToast({ title: '导出成功', icon: 'success' }) - // #endif - - // #ifdef MP-WEIXIN - const fs = uni.getFileSystemManager() - const filePath = `${wx.env.USER_DATA_PATH}/小菜记账_${getLocalDateStr()}.csv` - fs.writeFileSync(filePath, csv, 'utf-8') - uni.hideLoading() - uni.openDocument({ - filePath, - fileType: 'csv', - success: () => {}, - fail: () => uni.showToast({ title: '导出失败', icon: 'none' }) - }) - // #endif } catch { uni.hideLoading() uni.showToast({ title: '导出失败', icon: 'none' }) + } finally { + exportLoading.value = false + showExportPanel.value = false } } + +function saveFile(content: string, fileName: string, mimeType: string) { + // #ifdef H5 + const blob = new Blob([content], { type: `${mimeType};charset=utf-8;` }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = fileName + a.click() + URL.revokeObjectURL(url) + uni.hideLoading() + uni.showToast({ title: '导出成功', icon: 'success' }) + // #endif + + // #ifdef MP-WEIXIN + const fs = uni.getFileSystemManager() + const filePath = `${wx.env.USER_DATA_PATH}/${fileName}` + fs.writeFileSync(filePath, content, 'utf-8') + uni.hideLoading() + const ext = mimeType === 'application/json' ? 'json' : 'csv' + uni.openDocument({ + filePath, + fileType: ext, + success: () => {}, + fail: () => uni.showToast({ title: '导出失败', icon: 'none' }) + }) + // #endif +}