278 lines
6.4 KiB
Vue
278 lines
6.4 KiB
Vue
<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>
|
||
|
||
|