feat: 新增隐藏备注记录显示开关并简化设置与日志菜单文案

- 设置页「外观设置」新增「显示已隐藏备注的记录」开关,默认关闭,切换即时保存
- 新增 GET/PUT /api/setting/show-hidden-data 接口,用户级偏好持久化于 t_user_preference
- 收支记录列表与 Excel 导出按该偏好过滤,关闭时排除已隐藏备注的记录
- 移除「备注关键字搜索时总是跳过隐藏记录」逻辑,改由该偏好统一控制,统计备注搜索同步放开
- 列表内隐藏备注后,偏好关闭时该行即时移除,当前页清空自动回退上一页
- 外观设置面板拆分为两个浅灰底分组,两个配置项区分更清晰
- 顶部用户菜单与设置/日志页标题由「系统设置/系统日志」简化为「设置/日志」,同步中英日三语
This commit is contained in:
2026-09-23 09:28:42 +08:00
parent 66088e8c7e
commit 66d6ab86b5
10 changed files with 220 additions and 25 deletions
+10
View File
@@ -20,3 +20,13 @@ export function getExcludedDisplayStyle() {
export function updateExcludedDisplayStyle(style: string) {
return request.put('/setting/excluded-display-style', { style })
}
/** 查询当前用户是否在收支记录列表与导出中显示已隐藏备注的记录 */
export function getShowHiddenData() {
return request.get('/setting/show-hidden-data')
}
/** 更新当前用户是否在收支记录列表与导出中显示已隐藏备注的记录 */
export function updateShowHiddenData(show: boolean) {
return request.put('/setting/show-hidden-data', { show })
}
+73 -10
View File
@@ -1,12 +1,12 @@
<script setup lang="ts">
// 外观设置面板:配置「不计入统计」记录在收支记录列表中的展示样式
// 外观设置面板:配置「不计入统计」记录在收支记录列表中的展示样式,以及是否显示已隐藏备注的记录
import { ref, onMounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import {
useMessage, NCard, NSpace, NButton, NIcon, NText, NRadioGroup, NRadio,
useMessage, NCard, NSpace, NButton, NIcon, NText, NRadioGroup, NRadio, NSwitch,
} from 'naive-ui'
import { ColorPaletteOutline, SaveOutline } from '@vicons/ionicons5'
import { getExcludedDisplayStyle, updateExcludedDisplayStyle } from '../api/setting'
import { getExcludedDisplayStyle, updateExcludedDisplayStyle, getShowHiddenData, updateShowHiddenData } from '../api/setting'
const message = useMessage()
const { t } = useI18n()
@@ -16,6 +16,11 @@ const style = ref<string>('strikethrough')
const loading = ref(false)
const saving = ref(false)
/** 是否显示已隐藏备注的记录,默认不显示 */
const showHiddenData = ref(false)
const loadingHidden = ref(false)
const savingHidden = ref(false)
/** 展示样式选项(随语言切换响应式更新) */
const styleOptions = computed(() => [
{ label: t('display.strikethrough'), value: 'strikethrough' },
@@ -49,7 +54,41 @@ async function handleSave() {
}
}
onMounted(loadStyle)
/** 加载「显示已隐藏备注记录」偏好,失败时保持默认不显示 */
async function loadHiddenData() {
loadingHidden.value = true
try {
const res: any = await getShowHiddenData()
showHiddenData.value = !!res.data
} catch (e: any) {
// 静默处理,保持默认不显示
} finally {
loadingHidden.value = false
}
}
/** 切换「显示已隐藏备注记录」偏好并即时保存,失败时回滚 */
async function handleHiddenChange(value: string | number | boolean) {
const next = value === true
const previous = showHiddenData.value
showHiddenData.value = next
savingHidden.value = true
try {
const res: any = await updateShowHiddenData(next)
showHiddenData.value = !!res.data
message.success(t('display.hiddenDataSaved'))
} catch (e: any) {
showHiddenData.value = previous
message.error(e.message || t('display.hiddenDataFailed'))
} finally {
savingHidden.value = false
}
}
onMounted(() => {
loadStyle()
loadHiddenData()
})
</script>
<template>
@@ -61,16 +100,14 @@ onMounted(loadStyle)
</NSpace>
</template>
<NText depth="3" style="font-size: 12px;">{{ t('display.hint') }}</NText>
<div class="style-row">
<div class="filter-label">{{ t('display.label') }}</div>
<div class="config-group">
<div class="group-title">{{ t('display.label') }}</div>
<NText depth="3" style="font-size: 12px; display: block; margin-bottom: 8px;">{{ t('display.hint') }}</NText>
<NRadioGroup v-model:value="style" :disabled="loading">
<NRadio v-for="option in styleOptions" :key="option.value" :value="option.value">
{{ option.label }}
</NRadio>
</NRadioGroup>
</div>
<!-- 预览:按当前选择实时展示「不计入统计」记录的样式 -->
<div class="preview">
@@ -95,6 +132,20 @@ onMounted(loadStyle)
</div>
</div>
</div>
</div>
<div class="config-group">
<div class="group-title">{{ t('display.hiddenDataLabel') }}</div>
<NSpace align="center" :size="12">
<NSwitch
:value="showHiddenData"
:disabled="loadingHidden"
:loading="savingHidden"
@update:value="handleHiddenChange"
/>
<NText depth="3" style="font-size: 12px;">{{ t('display.hiddenDataHint') }}</NText>
</NSpace>
</div>
<NSpace justify="end" style="margin-top: 16px; width: 100%;">
<NButton type="primary" :loading="saving" @click="handleSave">
@@ -108,9 +159,20 @@ onMounted(loadStyle)
</template>
<style scoped>
.style-row {
.config-group {
padding: 12px 14px;
background: #fafafc;
border-radius: 8px;
}
.config-group + .config-group {
margin-top: 12px;
}
.group-title {
margin-bottom: 8px;
color: #333;
font-size: 13px;
font-weight: 600;
}
.filter-label {
margin-bottom: 8px;
color: #999;
@@ -123,6 +185,7 @@ onMounted(loadStyle)
border: 1px solid #efeff5;
border-radius: 8px;
overflow: hidden;
background: #fff;
}
.preview-head,
.preview-row {
+8 -4
View File
@@ -93,8 +93,8 @@ export default {
tags: { zh: '标签管理', en: 'Tags', ja: 'タグ管理' },
rules: { zh: '规则管理', en: 'Rules', ja: 'ルール管理' },
emailBills: { zh: '邮箱账单', en: 'Email Bills', ja: 'メール請求' },
logs: { zh: '系统日志', en: 'Logs', ja: 'システムログ' },
settings: { zh: '系统设置', en: 'Settings', ja: 'システム設定' },
logs: { zh: '日志', en: 'Logs', ja: 'ログ' },
settings: { zh: '设置', en: 'Settings', ja: '設定' },
user: { zh: '用户', en: 'User', ja: 'ユーザー' },
logout: { zh: '退出', en: 'Sign Out', ja: 'ログアウト' },
},
@@ -135,7 +135,7 @@ export default {
// 系统设置
settings: {
title: { zh: '系统设置', en: 'Settings', ja: 'システム設定' },
title: { zh: '设置', en: 'Settings', ja: '設定' },
accountSection: { zh: '账号安全', en: 'Account Security', ja: 'アカウントセキュリティ' },
databaseSection: { zh: '数据库配置', en: 'Database', ja: 'データベース' },
aiSection: { zh: 'AI 模型配置', en: 'AI Model', ja: 'AI モデル' },
@@ -192,6 +192,10 @@ export default {
saveSuccess: { zh: '外观设置已保存', en: 'Appearance settings saved', ja: '外観設定を保存しました' },
saveFailed: { zh: '保存外观设置失败', en: 'Failed to save appearance settings', ja: '外観設定の保存に失敗しました' },
loadFailed: { zh: '加载外观设置失败', en: 'Failed to load appearance settings', ja: '外観設定の読み込みに失敗しました' },
hiddenDataLabel: { zh: '显示已隐藏备注的记录', en: 'Show records with hidden remarks', ja: '備考を非表示にした記録を表示' },
hiddenDataHint: { zh: '关闭时,已隐藏备注的记录不会出现在收支记录列表与导出中', en: 'When off, records with hidden remarks are excluded from the transaction list and export', ja: 'オフの場合、備考を非表示にした記録は収支リストとエクスポートに表示されません' },
hiddenDataSaved: { zh: '已更新隐藏备注记录的显示设置', en: 'Hidden remark record display updated', ja: '非表示備考の記録表示を更新しました' },
hiddenDataFailed: { zh: '更新隐藏备注记录的显示设置失败', en: 'Failed to update hidden remark record display', ja: '非表示備考の記録表示の更新に失敗しました' },
},
// 注册设置
@@ -281,7 +285,7 @@ export default {
// 系统日志
log: {
title: { zh: '系统日志', en: 'System Logs', ja: 'システムログ' },
title: { zh: '日志', en: 'Logs', ja: 'ログ' },
httpTab: { zh: '访问日志', en: 'Access Logs', ja: 'アクセスログ' },
loginTab: { zh: '登录日志', en: 'Login Logs', ja: 'ログインログ' },
appTab: { zh: '运行日志', en: 'Application Logs', ja: 'アプリログ' },
+26 -1
View File
@@ -10,7 +10,7 @@ import { getTransactionPage, createTransaction, updateTransaction, deleteTransac
import { getAiConfig, suggestByAi, suggestOneByAi, applyAiSuggestions, queryByAi, parseRecordByAi, parseCategoryByAi, parseTagByAi, parseChannelByAi } from '../api/ai'
import { createChannel } from '../api/channel'
import type { AiSuggestion, AiQueryResult, AiRecordResult, AiRecordImageResult } from '../api/ai'
import { getExcludedDisplayStyle } from '../api/setting'
import { getExcludedDisplayStyle, getShowHiddenData } from '../api/setting'
import { getCategoryList, createCategory } from '../api/category'
import { getTagList, createTag } from '../api/tag'
import { getChannelLabel } from '../utils/channel'
@@ -129,6 +129,8 @@ const pageExcludedSummary = computed(() => {
const loading = ref(false) // 列表加载状态
// 「不计入统计」记录的展示样式:strikethrough-整行删除线(默认)、muted-灰色标签
const excludedDisplayStyle = ref<'strikethrough' | 'muted'>('strikethrough')
// 是否显示已隐藏备注的记录:默认不显示,隐藏后从当前列表移除
const showHiddenData = ref(false)
const submitting = ref(false) // 表单提交状态
const showModal = ref(false) // 新增/编辑弹窗显示状态
const showImportModal = ref(false) // 账单导入弹窗显示状态
@@ -492,6 +494,18 @@ async function setRemarkHidden(row: TransactionItem, hidden: boolean) {
row.remarkHidden = hidden
concealRemark(row.id)
message.success(hidden ? t('transaction.remarkHiddenSuccess') : t('transaction.remarkShownSuccess'))
// 偏好为不显示时,隐藏后将该行从列表移除;当前页清空则回退一页
if (hidden && !showHiddenData.value) {
const index = transactions.value.findIndex((item) => item.id === row.id)
if (index >= 0) {
transactions.value.splice(index, 1)
total.value = Math.max(0, total.value - 1)
}
if (transactions.value.length === 0 && page.value > 1) {
page.value -= 1
fetchData()
}
}
} catch (e: any) {
message.error(e.message || t('common.operationFailed'))
}
@@ -647,6 +661,16 @@ async function loadExcludedDisplayStyle() {
}
}
/** 加载「显示已隐藏备注记录」偏好,失败时保持默认不显示 */
async function loadShowHiddenData() {
try {
const res: any = await getShowHiddenData()
showHiddenData.value = !!res.data
} catch (e) {
// 静默处理,保持默认不显示
}
}
/** 加载分类列表 */
async function fetchCategories() {
try {
@@ -1999,6 +2023,7 @@ onMounted(() => {
remarkHistory.load()
loadAiConfig()
loadExcludedDisplayStyle()
loadShowHiddenData()
fetchCategories()
fetchTags()
loadChannels()