From a1e62e487df459a816acf9251244f5fdc6e91053 Mon Sep 17 00:00:00 2001
From: wangxiaogang <1433729587@qq.com>
Date: Mon, 8 Jun 2026 16:58:48 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=95=B0=E6=8D=AE=E5=AF=BC=E5=87=BA?=
=?UTF-8?q?=E5=A2=9E=E5=BC=BA=20=E2=80=94=20=E6=97=A5=E6=9C=9F=E8=8C=83?=
=?UTF-8?q?=E5=9B=B4/=E7=B1=BB=E5=9E=8B=E7=AD=9B=E9=80=89/CSV+JSON=20?=
=?UTF-8?q?=E6=A0=BC=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
client/src/pages/profile/index.vue | 315 +++++++++++++++++++++++++----
1 file changed, 273 insertions(+), 42 deletions(-)
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 @@
分类管理
-
+
+
+
+ 数据导出
+
+
+
+ 日期范围
+
+ { exportStartDate = e.detail.value; updateExportCount() }">
+
+
+ {{ exportStartDate }}
+
+
+ 至
+ { exportEndDate = e.detail.value; updateExportCount() }">
+
+
+ {{ exportEndDate }}
+
+
+
+
+
+
+
+ 交易类型
+
+ 全部
+ 仅支出
+ 仅收入
+
+
+
+
+
+ 导出格式
+
+ CSV
+ JSON
+
+
+
+
+
+
+
+
@@ -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
+}