Files
EmailBill/Web/src/views/SettingView.vue

347 lines
9.6 KiB
Vue
Raw Normal View History

2025-12-25 11:20:56 +08:00
<template>
2025-12-27 21:15:26 +08:00
<div class="page-container-flex">
2025-12-25 17:41:36 +08:00
<van-nav-bar title="设置" placeholder/>
2025-12-27 21:15:26 +08:00
<div class="scroll-content">
<div class="detail-header" style="padding-bottom: 5px;">
2025-12-29 15:20:32 +08:00
<p>账单</p>
2025-12-27 21:15:26 +08:00
</div>
<van-cell-group inset>
<van-cell title="从支付宝导入" is-link @click="handleImportClick('Alipay')" />
<van-cell title="从微信导入" is-link @click="handleImportClick('WeChat')" />
2025-12-29 15:20:32 +08:00
<van-cell title="周期记录" is-link @click="handlePeriodicRecord" />
2025-12-27 21:15:26 +08:00
</van-cell-group>
2025-12-25 11:20:56 +08:00
2025-12-27 21:15:26 +08:00
<!-- 隐藏的文件选择器 -->
<input ref="fileInputRef" type="file" accept=".csv,.xlsx,.xls" style="display: none" @change="handleFileChange" />
2025-12-25 11:20:56 +08:00
2025-12-27 21:15:26 +08:00
<div class="detail-header" style="padding-bottom: 5px;">
2025-12-29 15:20:32 +08:00
<p>分类</p>
2025-12-27 21:15:26 +08:00
</div>
<van-cell-group inset>
<van-cell title="待确认分类" is-link @click="handleUnconfirmedClassification" />
2025-12-27 21:15:26 +08:00
<van-cell title="编辑分类" is-link @click="handleEditClassification" />
<van-cell title="批量分类" is-link @click="handleBatchClassification" />
<van-cell title="智能分类" is-link @click="handleSmartClassification" />
2025-12-31 11:10:10 +08:00
<!-- <van-cell title="自然语言分类" is-link @click="handleNaturalLanguageClassification" /> -->
2025-12-27 21:15:26 +08:00
</van-cell-group>
<div class="detail-header" style="padding-bottom: 5px;">
<p>通知</p>
</div>
<van-cell-group inset>
<van-cell title="开启消息通知">
<template #right-icon>
<van-switch v-model="notificationEnabled" size="24" :loading="notificationLoading" @change="handleNotificationToggle" />
</template>
</van-cell>
<van-cell v-if="notificationEnabled" title="测试通知" is-link @click="handleTestNotification" />
</van-cell-group>
2025-12-29 16:45:51 +08:00
<div class="detail-header" style="padding-bottom: 5px;">
<p>开发者</p>
</div>
<van-cell-group inset>
<van-cell title="查看日志" is-link @click="handleLogView" />
<van-cell title="清除缓存" is-link @click="handleReloadFromNetwork" />
<van-cell title="定时任务" is-link @click="handleScheduledTasks" />
2026-01-02 19:21:47 +08:00
</van-cell-group>
2025-12-29 15:20:32 +08:00
2025-12-27 21:15:26 +08:00
<div class="detail-header" style="padding-bottom: 5px;">
<p>账户</p>
</div>
<van-cell-group inset>
<van-cell title="退出登录" is-link @click="handleLogout" />
</van-cell-group>
2026-01-02 19:17:21 +08:00
<!-- 底部安全距离 -->
<div style="height: calc(80px + env(safe-area-inset-bottom, 0px))"></div>
2025-12-25 13:27:23 +08:00
</div>
2025-12-25 11:20:56 +08:00
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
2025-12-25 13:27:23 +08:00
import { useRouter } from 'vue-router'
import { showLoadingToast, showSuccessToast, showToast, closeToast, showConfirmDialog } from 'vant'
2025-12-25 11:20:56 +08:00
import { uploadBillFile } from '@/api/billImport'
2025-12-25 13:27:23 +08:00
import { useAuthStore } from '@/stores/auth'
import { getVapidPublicKey, subscribe, testNotification } from '@/api/notification'
import { updateServiceWorker } from '@/registerServiceWorker'
2025-12-25 11:20:56 +08:00
2025-12-25 13:27:23 +08:00
const router = useRouter()
const authStore = useAuthStore()
2025-12-25 11:20:56 +08:00
const fileInputRef = ref(null)
const currentType = ref('')
const notificationEnabled = ref(false)
const notificationLoading = ref(false)
onMounted(async () => {
if ('serviceWorker' in navigator) {
const registration = await navigator.serviceWorker.ready
const subscription = await registration.pushManager.getSubscription()
notificationEnabled.value = !!subscription
}
})
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
const handleNotificationToggle = async (checked) => {
if (!('serviceWorker' in navigator)) {
showToast('您的浏览器不支持推送通知')
notificationEnabled.value = false
return
}
notificationLoading.value = true
try {
const registration = await navigator.serviceWorker.ready
if (checked) {
// 开启通知
const permission = await Notification.requestPermission()
if (permission !== 'granted') {
showToast('请允许通知权限')
notificationEnabled.value = false
return
}
2026-01-02 18:51:28 +08:00
let { success, data, message } = await getVapidPublicKey()
if (!success) {
throw new Error(message || '获取 VAPID 公钥失败')
}
const convertedVapidKey = urlBase64ToUint8Array(data)
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: convertedVapidKey
})
const subJson = subscription.toJSON()
await subscribe({
endpoint: subJson.endpoint,
p256DH: subJson.keys.p256dh,
auth: subJson.keys.auth
})
showSuccessToast('开启成功')
} else {
// 关闭通知
const subscription = await registration.pushManager.getSubscription()
if (subscription) {
await subscription.unsubscribe()
}
showSuccessToast('已关闭')
}
} catch (error) {
console.error(error)
showToast('操作失败: ' + (error.message || '未知错误'))
notificationEnabled.value = !checked // 回滚状态
} finally {
notificationLoading.value = false
}
}
const handleTestNotification = async () => {
try {
await testNotification('这是一条测试消息')
showSuccessToast('发送成功,请查看通知栏')
} catch (error) {
console.error(error)
showToast('发送失败')
}
}
2025-12-25 11:20:56 +08:00
/**
* 处理导入按钮点击
*/
const handleImportClick = (type) => {
currentType.value = type
// 触发文件选择
fileInputRef.value?.click()
}
2025-12-29 15:20:32 +08:00
const handlePeriodicRecord = () => {
router.push({ name: 'periodic-record' })
}
2025-12-25 11:20:56 +08:00
/**
* 处理文件选择
*/
const handleFileChange = async (event) => {
const file = event.target.files?.[0]
if (!file) {
return
}
// 验证文件类型
const validTypes = ['text/csv', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
if (!validTypes.includes(file.type)) {
showToast('请选择 CSV 或 Excel 文件')
return
}
// 验证文件大小(限制为 10MB
const maxSize = 10 * 1024 * 1024
if (file.size > maxSize) {
showToast('文件大小不能超过 10MB')
return
}
try {
// 显示加载提示
showLoadingToast({
message: '上传中...',
forbidClick: true,
duration: 0
})
// 上传文件
const typeName = currentType.value === 'Alipay' ? '支付宝' : '微信'
const { success, message } = await uploadBillFile(file, currentType.value)
if (!success) {
showToast(message || `${typeName}账单导入失败`)
return
}
showSuccessToast(message || `${typeName}账单导入成功`)
} catch (error) {
console.error('上传失败:', error)
showToast('上传失败: ' + (error.message || '未知错误'))
}
finally {
closeToast()
// 清空文件输入,允许重复选择同一文件
event.target.value = ''
}
}
2025-12-25 13:27:23 +08:00
2025-12-26 15:21:31 +08:00
const handleEditClassification = () => {
router.push({ name: 'classification-edit' })
}
const handleBatchClassification = () => {
router.push({ name: 'classification-batch' })
}
const handleSmartClassification = () => {
router.push({ name: 'smart-classification' })
}
2025-12-31 12:46:03 +08:00
// const handleNaturalLanguageClassification = () => {
// router.push({ name: 'classification-nlp' })
// }
2025-12-26 15:21:31 +08:00
2025-12-25 13:27:23 +08:00
/**
* 处理退出登录
*/
const handleLogout = async () => {
try {
await showConfirmDialog({
title: '提示',
message: '确定要退出登录吗?',
})
authStore.logout()
showSuccessToast('已退出登录')
router.push({ name: 'login' })
} catch (error) {
console.error('取消退出登录:', error)
showToast('已取消退出登录')
}
}
2025-12-29 16:45:51 +08:00
/**
* 处理查看日志
*/
const handleLogView = () => {
router.push({ name: 'log' })
}
2026-01-02 19:21:47 +08:00
const handleUnconfirmedClassification = () => {
router.push({ name: 'unconfirmed-classification' })
}
const handleReloadFromNetwork = async () => {
2026-01-02 19:21:47 +08:00
try {
await showConfirmDialog({
title: '提示',
message: '确定要刷新网络吗?此操作不可撤销。',
2026-01-02 19:21:47 +08:00
})
// PWA程序强制页面更新到最新版本
if ('serviceWorker' in navigator) {
2026-01-04 18:51:55 +08:00
await updateServiceWorker()
showSuccessToast('正在更新,请稍候...')
// 延迟刷新页面以加载新版本
setTimeout(() => {
window.location.reload()
}, 1500)
2026-01-02 19:21:47 +08:00
} else {
showToast('当前环境不支持此操作')
2026-01-02 19:21:47 +08:00
return
}
} catch (error) {
console.error('取消刷新网络:', error)
showToast('已取消刷新网络')
2026-01-02 19:21:47 +08:00
}
}
const handleScheduledTasks = () => {
router.push({ name: 'scheduled-tasks' })
}
2025-12-25 11:20:56 +08:00
</script>
<style scoped>
/* 页面背景色 */
:deep(body) {
background-color: #f5f5f5;
}
@media (prefers-color-scheme: dark) {
:deep(body) {
background-color: #1a1a1a;
}
}
/* 增加卡片对比度 */
:deep(.van-cell-group--inset) {
background-color: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
@media (prefers-color-scheme: dark) {
:deep(.van-cell-group--inset) {
background-color: #2c2c2c;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
}
.detail-header {
padding: 16px 16px 5px 16px;
2025-12-29 15:20:32 +08:00
margin-bottom: 5px;
2025-12-25 11:20:56 +08:00
}
.detail-header p {
margin: 0;
font-size: 14px;
color: #969799;
font-weight: normal;
}
2025-12-26 18:03:52 +08:00
/* 设置页面容器背景色 */
:deep(.van-nav-bar) {
background: transparent !important;
}
2025-12-25 11:20:56 +08:00
</style>