Files
2026-06-11 09:53:11 +08:00

278 lines
6.4 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="app-page" :class="themeClass">
<view class="surface-card page-hero">
<text class="hero-kicker">BACKUP</text>
<text class="hero-title">备份与恢复</text>
<text class="hero-desc">导出本地 JSON 备份恢复时覆盖当前设备数据适合换机或手动留档</text>
<view class="hero-tags">
<text class="hero-tag">本地文件</text>
<text class="hero-tag soft">{{ store.state.settings.lastBackupAt ? '最近已备份' : '尚未备份' }}</text>
</view>
</view>
<ad-custom unit-id="adunit-64707ea333329399"></ad-custom>
<section-card title="导出备份" subtitle="将账单、预算和设置导出为 JSON 文件,便于迁移设备或手动保存">
<view class="hero-panel surface-strong">
<view>
<text class="panel-title">安全备份当前数据</text>
<text class="panel-desc">所有数据均为本地文件不会自动上传服务器</text>
</view>
<view class="status-chip">{{ store.state.settings.lastBackupAt ? '可继续备份' : '建议先备份' }}</view>
</view>
<view class="action-row single-row">
<view class="primary-button" @click="exportBackupFile">导出备份</view>
</view>
<text class="tiny-text info-line" v-if="store.state.settings.lastBackupAt">最近备份{{ store.state.settings.lastBackupAt }}</text>
</section-card>
<section-card title="恢复备份" subtitle="粘贴之前导出的 JSON 内容,恢复后将覆盖当前设备数据">
<view class="input-shell textarea-shell">
<textarea v-model="importText" placeholder="请粘贴备份 JSON"></textarea>
</view>
<view class="action-row">
<view class="ghost-button" @click="importText = ''">清空内容</view>
<view class="primary-button" @click="restoreBackup">开始恢复</view>
</view>
</section-card>
<section-card title="数据操作" subtitle="谨慎执行不可撤销的本地清理操作">
<view class="menu-list">
<view class="menu-item surface-strong" @click="copyBackupText">
<view>
<text class="menu-title">复制备份内容</text>
</view>
<text class="status-chip">复制</text>
</view>
<view class="menu-item surface-strong danger-shell" @click="clearCache">
<view>
<text class="menu-title negative">清空全部数据</text>
</view>
<text class="danger-text">执行</text>
</view>
</view>
</section-card>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import SectionCard from '../../../components/SectionCard.vue'
import { useAppStore } from '../../../utils/store'
import { onShow } from '@dcloudio/uni-app'
const store = useAppStore()
const importText = ref('')
const themeClass = computed(() => (store.state.settings.theme === 'dark' ? 'theme-dark' : ''))
function writeBackupFile(content) {
const timeLabel = new Date().toLocaleString()
if (typeof wx !== 'undefined' && wx.getFileSystemManager) {
const filePath = `${wx.env.USER_DATA_PATH}/bill-helper-backup.json`
wx.getFileSystemManager().writeFile({
filePath,
data: content,
encoding: 'utf8',
success: () => {
store.markBackup(timeLabel)
uni.showModal({ title: '备份成功', content: `文件已生成:${filePath}`, showCancel: false })
},
fail: () => {
uni.setClipboardData({ data: content })
}
})
return
}
store.markBackup(timeLabel)
uni.setClipboardData({ data: content })
}
function exportBackupFile() {
writeBackupFile(store.exportBackup())
}
function restoreBackup() {
if (!importText.value.trim()) {
uni.showToast({ title: '请先粘贴备份内容', icon: 'none' })
return
}
try {
store.importBackup(importText.value)
importText.value = ''
uni.showToast({ title: '备份恢复成功', icon: 'none' })
} catch (error) {
uni.showToast({ title: '备份内容无效', icon: 'none' })
}
}
function copyBackupText() {
uni.setClipboardData({ data: store.exportBackup() })
}
function clearCache() {
uni.showModal({
title: '清空全部数据',
content: '确认删除当前设备中的账单、预算和设置吗?此操作不可撤销。',
success: ({ confirm }) => {
if (confirm) {
store.resetAll()
uni.showToast({ title: '本地数据已清空', icon: 'none' })
}
}
})
}
onShow(()=>{
showIt()
})
function showIt(){
let interstitialAd = null;
if (wx.createInterstitialAd) {
interstitialAd = wx.createInterstitialAd({
adUnitId: 'adunit-0abc32053b19a4e9'
})
interstitialAd.onLoad(() => {})
interstitialAd.onError((err) => {
console.error('插屏广告加载失败', err)
})
interstitialAd.onClose(() => {})
}
setTimeout(()=>{
if (interstitialAd) {
interstitialAd.show().catch((err) => {
console.error('插屏广告显示失败', err)
})
}
}, 2280)
}
</script>
<style lang="scss" scoped>
.page-hero {
padding: 30rpx;
background: linear-gradient(145deg, rgba(16, 42, 67, 0.96) 0%, rgba(61, 102, 178, 0.92) 100%);
color: #ffffff;
}
.hero-kicker,
.hero-desc,
.hero-tag.soft {
color: rgba(255, 255, 255, 0.76);
}
.hero-kicker {
font-size: 20rpx;
letter-spacing: 4rpx;
}
.hero-title {
display: block;
margin-top: 12rpx;
font-size: 44rpx;
font-weight: 700;
}
.hero-desc {
display: block;
margin-top: 14rpx;
font-size: 24rpx;
line-height: 1.7;
}
.hero-tags {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
margin-top: 22rpx;
}
.hero-tag,
.status-chip {
padding: 12rpx 18rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.16);
font-size: 22rpx;
}
.hero-panel,
.menu-item,
.action-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.hero-panel,
.menu-item {
padding: 26rpx;
border-radius: 28rpx;
}
.hero-panel {
justify-content: space-between;
}
.panel-title,
.menu-title {
display: block;
font-size: 31rpx;
font-weight: 700;
color: var(--text-primary);
}
.panel-desc,
.status-chip {
background: var(--brand-soft);
color: var(--brand);
}
.action-row {
margin-top: 18rpx;
}
.single-row .primary-button,
.action-row .ghost-button,
.action-row .primary-button {
flex: 1;
}
.info-line {
display: block;
margin-top: 16rpx;
}
.textarea-shell {
align-items: flex-start;
min-height: 280rpx;
padding: 20rpx 24rpx;
}
.textarea-shell textarea {
min-height: 240rpx;
}
.menu-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.menu-item {
justify-content: space-between;
}
.danger-shell {
border: 1rpx solid rgba(210, 85, 67, 0.12);
}
.danger-text {
font-size: 24rpx;
color: var(--danger);
}
</style>