From 4ad976604362471942bd0a45d4af4938241e533f Mon Sep 17 00:00:00 2001 From: Megghy Date: Fri, 10 Oct 2025 14:35:56 +0800 Subject: [PATCH] feat: Implement local question saving feature for unauthenticated users - Added functionality to automatically save questions sent by unauthenticated users to local storage using VueUse's useStorage. - Introduced a local history button to display the number of saved questions. - Created a drawer component to view, delete, and clear local question records. - Enhanced the question form with options for anonymous name and email input. - Updated UI components and styles for better user experience. - Added tests and documentation for the new feature. --- src/api/api-models.ts | 4 +- src/components.d.ts | 4 + src/components/manage/PointHistoryCard.vue | 43 +- src/components/manage/PointOrderCard.vue | 7 +- src/router/index.ts | 4 +- src/views/manage/point/PointOrderManage.vue | 6 +- .../manage/point/PointUserDetailCard.vue | 4 +- src/views/manage/point/PointUserManage.vue | 17 +- src/views/pointViews/PointGoodsView.vue | 16 +- src/views/pointViews/PointOrderView.vue | 3 +- src/views/pointViews/PointUserHistoryView.vue | 11 +- src/views/pointViews/PointUserSettings.vue | 120 ++++- src/views/view/QuestionBoxView.vue | 416 +++++++++++++++++- 本地提问保存功能说明.md | 135 ++++++ 测试指南-本地提问保存.md | 208 +++++++++ 15 files changed, 941 insertions(+), 57 deletions(-) create mode 100644 本地提问保存功能说明.md create mode 100644 测试指南-本地提问保存.md diff --git a/src/api/api-models.ts b/src/api/api-models.ts index 78f3688..feb2593 100644 --- a/src/api/api-models.ts +++ b/src/api/api-models.ts @@ -391,6 +391,8 @@ export interface QAInfo { tag?: string reviewResult?: QAReviewInfo + anonymousName?: string + anonymousEmail?: string } export interface LotteryUserInfo { name: string @@ -829,7 +831,7 @@ export interface ResponsePointHisrotyModel { createAt: number count: number - extra?: any + extra?: any // Use 时包含: { user, goods, isDiscontinued, remark }; Manual 时包含: { user, reason }; Danmaku 时包含: { user, danmaku }; CheckIn 时包含: { user } } export enum PointFrom { diff --git a/src/components.d.ts b/src/components.d.ts index 258d478..0228175 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -19,13 +19,17 @@ declare module 'vue' { LiveInfoContainer: typeof import('./components/LiveInfoContainer.vue')['default'] MonacoEditorComponent: typeof import('./components/MonacoEditorComponent.vue')['default'] NAlert: typeof import('naive-ui')['NAlert'] + NBadge: typeof import('naive-ui')['NBadge'] NEllipsis: typeof import('naive-ui')['NEllipsis'] NEmpty: typeof import('naive-ui')['NEmpty'] NFlex: typeof import('naive-ui')['NFlex'] NFormItemGi: typeof import('naive-ui')['NFormItemGi'] NGridItem: typeof import('naive-ui')['NGridItem'] + NIcon: typeof import('naive-ui')['NIcon'] NScrollbar: typeof import('naive-ui')['NScrollbar'] NTag: typeof import('naive-ui')['NTag'] + NText: typeof import('naive-ui')['NText'] + NTime: typeof import('naive-ui')['NTime'] PointGoodsItem: typeof import('./components/manage/PointGoodsItem.vue')['default'] PointHistoryCard: typeof import('./components/manage/PointHistoryCard.vue')['default'] PointOrderCard: typeof import('./components/manage/PointOrderCard.vue')['default'] diff --git a/src/components/manage/PointHistoryCard.vue b/src/components/manage/PointHistoryCard.vue index 9e10a03..8b0f2f0 100644 --- a/src/components/manage/PointHistoryCard.vue +++ b/src/components/manage/PointHistoryCard.vue @@ -33,7 +33,7 @@ const historyColumn: DataTableColumns = [ { title: '时间', key: 'createAt', - sorter: 'default', + sorter: (row1: ResponsePointHisrotyModel, row2: ResponsePointHisrotyModel) => row1.createAt - row2.createAt, render: (row: ResponsePointHisrotyModel) => { return h(NTooltip, null, { trigger: () => h(NTime, { time: row.createAt, type: 'relative' }), @@ -44,11 +44,13 @@ const historyColumn: DataTableColumns = [ { title: '积分变动', key: 'point', + sorter: (row1: ResponsePointHisrotyModel, row2: ResponsePointHisrotyModel) => row1.point - row2.point, render: (row: ResponsePointHisrotyModel) => { + const point = Number(row.point.toFixed(1)) return h( NText, - { style: { color: row.point < 0 ? 'red' : 'green' } }, - () => (row.point < 0 ? '' : '+') + row.point, + { style: { color: point < 0 ? 'red' : 'green' } }, + () => (point < 0 ? '' : '+') + point, ) }, }, @@ -205,18 +207,29 @@ const historyColumn: DataTableColumns = [ case PointFrom.Use: return h(NFlex, { align: 'center' }, () => [ h(NTag, { type: 'success', size: 'small', style: { margin: '0' }, strong: true }, () => '兑换'), - h( - NButton, - { - text: true, - type: 'info', - onClick: () => { - currentGoods.value = row.extra - showGoodsModal.value = true - }, - }, - () => row.extra?.name, - ), + row.extra?.goods + ? h( + NButton, + { + text: true, + type: 'info', + onClick: () => { + currentGoods.value = row.extra?.goods + showGoodsModal.value = true + }, + }, + () => row.extra?.goods?.name, + ) + : h(NText, { depth: 3, italic: true }, () => '(商品已删除)'), + row.extra?.isDiscontinued + ? h(NTag, { type: 'error', size: 'tiny', bordered: false }, () => '已下架') + : null, + row.extra?.remark + ? h(NTooltip, null, { + trigger: () => h(NTag, { type: 'info', size: 'tiny', bordered: false }, () => '留言'), + default: () => row.extra.remark, + }) + : null, ]) } return null diff --git a/src/components/manage/PointOrderCard.vue b/src/components/manage/PointOrderCard.vue index 74616a6..36aeb91 100644 --- a/src/components/manage/PointOrderCard.vue +++ b/src/components/manage/PointOrderCard.vue @@ -184,7 +184,7 @@ const orderColumn: DataTableColumns = [ { title: '时间', key: 'time', - sorter: 'default', + sorter: (row1: OrderType, row2: OrderType) => row1.createAt - row2.createAt, minWidth: 80, render: (row: OrderType) => { return h(NTooltip, null, { @@ -196,7 +196,10 @@ const orderColumn: DataTableColumns = [ { title: '使用积分', key: 'point', - sorter: 'default', + sorter: (row1: OrderType, row2: OrderType) => row1.point - row2.point, + render: (row: OrderType) => { + return Number(row.point.toFixed(1)) + }, }, { title: '订单状态', diff --git a/src/router/index.ts b/src/router/index.ts index 2d6cbe8..a0d384a 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -116,7 +116,7 @@ const router = createRouter({ }) router.beforeEach((to, from, next) => { useLoadingBarStore().loadingBar?.start() - + // 保留 as 参数(如果存在) if (from.query.as && !to.query.as) { next({ @@ -128,7 +128,7 @@ router.beforeEach((to, from, next) => { }) return } - + next() }) router.afterEach(() => { diff --git a/src/views/manage/point/PointOrderManage.vue b/src/views/manage/point/PointOrderManage.vue index 9015040..502485e 100644 --- a/src/views/manage/point/PointOrderManage.vue +++ b/src/views/manage/point/PointOrderManage.vue @@ -80,7 +80,7 @@ const orderStats = computed(() => { completed: orders.value.filter(o => o.status === PointOrderStatus.Completed).length, physical: orders.value.filter(o => o.type === GoodsTypes.Physical).length, virtual: orders.value.filter(o => o.type === GoodsTypes.Virtual).length, - totalPoints: orders.value.reduce((sum, o) => sum + o.point, 0), + totalPoints: Number(orders.value.reduce((sum, o) => sum + o.point, 0).toFixed(1)), filteredCount: filteredOrders.value.length, } }) @@ -200,8 +200,8 @@ function exportData() { : '无', 礼物名: gift?.name ?? '已删除', 礼物数量: s.count, - 礼物单价: gift?.price, - 礼物总价: s.point, + 礼物单价: gift?.price ? Number(gift.price.toFixed(1)) : 0, + 礼物总价: Number(s.point.toFixed(1)), 快递公司: s.expressCompany, 快递单号: s.trackingNumber, 备注: s.remark ?? '', diff --git a/src/views/manage/point/PointUserDetailCard.vue b/src/views/manage/point/PointUserDetailCard.vue index 0228a94..6732481 100644 --- a/src/views/manage/point/PointUserDetailCard.vue +++ b/src/views/manage/point/PointUserDetailCard.vue @@ -146,7 +146,7 @@ async function givePoint() { if (data.code == 200) { message.success('添加成功') showAddPointModal.value = false - props.user.point += addPointCount.value + props.user.point = Number((props.user.point + addPointCount.value).toFixed(1)) // 重新加载积分历史 setTimeout(async () => { @@ -227,7 +227,7 @@ onMounted(async () => { - {{ user.point }} + {{ Number(user.point.toFixed(1)) }} { // 用户统计 const userStats = computed(() => { + const totalPoints = users.value.reduce((sum, u) => sum + u.point, 0) + const avgPoints = users.value.length > 0 ? users.value.reduce((sum, u) => sum + u.point, 0) / users.value.length : 0 return { total: users.value.length, authed: users.value.filter(u => u.isAuthed).length, - totalPoints: users.value.reduce((sum, u) => sum + u.point, 0), + totalPoints: Number(totalPoints.toFixed(1)), totalOrders: users.value.reduce((sum, u) => sum + (u.orderCount || 0), 0), - avgPoints: users.value.length > 0 ? Math.round(users.value.reduce((sum, u) => sum + u.point, 0) / users.value.length) : 0, + avgPoints: Number(avgPoints.toFixed(1)), filtered: filteredUsers.value.length, } }) @@ -197,9 +199,10 @@ function formatNumber(num: number) { return num.toLocaleString('zh-CN') } -// 渲染积分,添加千位符并加粗 +// 渲染积分,添加千位符并加粗,保留一位小数 function renderPoint(num: number) { - return h(NText, { strong: true }, { default: () => formatNumber(num) }) + const formattedNum = Number(num.toFixed(1)) + return h(NText, { strong: true }, { default: () => formatNumber(formattedNum) }) } // 数据表格列定义 @@ -219,7 +222,7 @@ const column: DataTableColumns = [ { title: '积分', key: 'point', - sorter: 'default', + sorter: (row1: ResponsePointUserModel, row2: ResponsePointUserModel) => row1.point - row2.point, render: (row: ResponsePointUserModel) => renderPoint(row.point), }, { @@ -230,7 +233,7 @@ const column: DataTableColumns = [ { title: '最后更新于', key: 'updateAt', - sorter: 'default', + sorter: (row1: ResponsePointUserModel, row2: ResponsePointUserModel) => row1.updateAt - row2.updateAt, render: (row: ResponsePointUserModel) => renderTime(row.updateAt), }, { @@ -374,7 +377,7 @@ function exportData() { 用户ID: user.info.userId || user.info.openId, 用户名: user.info.name || '未知', 认证状态: user.isAuthed ? '已认证' : '未认证', - 积分: user.point, + 积分: Number(user.point.toFixed(1)), 订单数量: user.orderCount || 0, 最后更新时间: format(user.updateAt, 'yyyy-MM-dd HH:mm:ss'), } diff --git a/src/views/pointViews/PointGoodsView.vue b/src/views/pointViews/PointGoodsView.vue index dc79c6b..c205a09 100644 --- a/src/views/pointViews/PointGoodsView.vue +++ b/src/views/pointViews/PointGoodsView.vue @@ -87,6 +87,12 @@ watch(searchKeyword, (newVal) => { // --- 计算属性 --- +// 格式化积分显示,保留一位小数 +const formattedCurrentPoint = computed(() => { + if (currentPoint.value < 0) return currentPoint.value + return Number(currentPoint.value.toFixed(1)) +}) + // 地址选项,用于地址选择器 const addressOptions = computed(() => { if (!biliAuth.value.id) return [] @@ -264,7 +270,7 @@ async function buyGoods() { // 确认对话框 dialog.warning({ title: '确认兑换', - content: `确定要花费 ${currentGoods.value!.price * buyCount.value} 积分兑换 ${buyCount.value} 个 "${currentGoods.value!.name}" 吗?`, + content: `确定要花费 ${Number((currentGoods.value!.price * buyCount.value).toFixed(1))} 积分兑换 ${buyCount.value} 个 "${currentGoods.value!.name}" 吗?`, positiveText: '确定', negativeText: '取消', onPositiveClick: async () => { @@ -281,7 +287,7 @@ async function buyGoods() { if (data.code === 200) { message.success('兑换成功') // 更新本地积分显示 - currentPoint.value -= currentGoods.value!.price * buyCount.value + currentPoint.value = Number((currentPoint.value - currentGoods.value!.price * buyCount.value).toFixed(1)) // 显示成功对话框 dialog.success({ title: '成功', @@ -426,7 +432,7 @@ onMounted(async () => { v-if="currentPoint >= 0" class="point-info" > - 你在本直播间的积分: {{ currentPoint }} + 你在本直播间的积分: {{ formattedCurrentPoint }} { 确认兑换 - 所需积分: {{ currentGoods.price * buyCount }} + 所需积分: {{ Number((currentGoods.price * buyCount).toFixed(1)) }} - 当前积分: {{ currentPoint >= 0 ? currentPoint : '加载中' }} + 当前积分: {{ currentPoint >= 0 ? formattedCurrentPoint : '加载中' }} diff --git a/src/views/pointViews/PointOrderView.vue b/src/views/pointViews/PointOrderView.vue index 9f4e08e..e4d5fda 100644 --- a/src/views/pointViews/PointOrderView.vue +++ b/src/views/pointViews/PointOrderView.vue @@ -42,12 +42,13 @@ const filteredOrders = computed(() => { // 订单统计 const orderStats = computed(() => { + const totalPoints = orders.value.reduce((sum, o) => sum + o.point, 0) return { total: orders.value.length, pending: orders.value.filter(o => o.status === PointOrderStatus.Pending).length, shipped: orders.value.filter(o => o.status === PointOrderStatus.Shipped).length, completed: orders.value.filter(o => o.status === PointOrderStatus.Completed).length, - totalPoints: orders.value.reduce((sum, o) => sum + o.point, 0), + totalPoints: Number(totalPoints.toFixed(1)), } }) diff --git a/src/views/pointViews/PointUserHistoryView.vue b/src/views/pointViews/PointUserHistoryView.vue index f28e439..16eb3fd 100644 --- a/src/views/pointViews/PointUserHistoryView.vue +++ b/src/views/pointViews/PointUserHistoryView.vue @@ -23,10 +23,13 @@ const dateRange = ref<[number, number] | null>(null) // 积分历史统计 const historyStats = computed(() => { + const totalIncrease = history.value.filter(h => h.point > 0).reduce((sum, h) => sum + h.point, 0) + const totalDecrease = Math.abs(history.value.filter(h => h.point < 0).reduce((sum, h) => sum + h.point, 0)) return { total: history.value.length, - totalIncrease: history.value.filter(h => h.point > 0).reduce((sum, h) => sum + h.point, 0), - totalDecrease: Math.abs(history.value.filter(h => h.point < 0).reduce((sum, h) => sum + h.point, 0)), + totalIncrease: Number(totalIncrease.toFixed(1)), + totalDecrease: Number(totalDecrease.toFixed(1)), + netIncrease: Number((totalIncrease - totalDecrease).toFixed(1)), fromManual: history.value.filter(h => h.from === PointFrom.Manual).length, fromDanmaku: history.value.filter(h => h.from === PointFrom.Danmaku).length, fromCheckIn: history.value.filter(h => h.from === PointFrom.CheckIn).length, @@ -140,7 +143,7 @@ function exportHistoryData() { filteredHistory.value.map((item) => { return { 时间: format(item.createAt, 'yyyy-MM-dd HH:mm:ss'), - 积分变化: item.point, + 积分变化: Number(item.point.toFixed(1)), 来源: pointFromText[item.from] || '未知', 主播: item.extra?.user?.name || '-', 数量: item.count || '-', @@ -206,7 +209,7 @@ function exportHistoryData() {
- {{ historyStats.totalIncrease - historyStats.totalDecrease }} + {{ historyStats.netIncrease }}
净增加 diff --git a/src/views/pointViews/PointUserSettings.vue b/src/views/pointViews/PointUserSettings.vue index ef68e82..ea29afa 100644 --- a/src/views/pointViews/PointUserSettings.vue +++ b/src/views/pointViews/PointUserSettings.vue @@ -49,8 +49,10 @@ const isLoading = ref(false) const userAgree = ref(false) const showAddressModal = ref(false) const showAgreementModal = ref(false) +const showAddAccountModal = ref(false) const formRef = ref() const currentAddress = ref() +const authCodeInput = ref('') // 本地存储区域数据 const areas = useStorage<{ @@ -255,11 +257,53 @@ function logout() { useAuth.logout() } +// 添加账号 +async function addAccount() { + if (!authCodeInput.value.trim()) { + message.warning('请输入登录链接或authcode') + return + } + + isLoading.value = true + try { + let authCode = authCodeInput.value.trim() + + // 检查是否是完整的登录链接 + const urlMatch = authCode.match(/[?&]auth=([^&]+)/) + if (urlMatch) { + authCode = urlMatch[1] + } + + // 验证authCode格式(这里假设是token格式) + if (!authCode) { + message.error('无效的authcode格式') + return + } + + // 尝试使用authCode登录 + await useAuth.setCurrentAuth(authCode) + + // 检查是否登录成功 + if (useAuth.isInvalid) { + message.error('无效的authcode,请检查后重试') + } else { + message.success('账号添加成功') + showAddAccountModal.value = false + authCodeInput.value = '' + } + } catch (err) { + handleApiError('添加账号', err) + } finally { + isLoading.value = false + } +} + // 提供给父组件调用的重置方法 function reset() { // 重置表单数据或其他状态 currentAddress.value = {} as AddressInfo userAgree.value = false + authCodeInput.value = '' // 可能还需要重置其他状态 } @@ -375,17 +419,26 @@ defineExpose({ vertical :gap="12" > - - - 确定要登出吗? - + + + + 确定要登出吗? + + + 添加账号 + + 切换账号 @@ -419,7 +472,7 @@ defineExpose({ type="success" size="small" > - 当前账号 + 当前 {{ item.name }} @@ -587,6 +640,46 @@ defineExpose({ + + + + + 请输入登录链接或authcode + + + + + 取消 + + + 添加 + + + + + diff --git a/本地提问保存功能说明.md b/本地提问保存功能说明.md new file mode 100644 index 0000000..df9fd44 --- /dev/null +++ b/本地提问保存功能说明.md @@ -0,0 +1,135 @@ +# 本地提问保存功能说明 + +## 功能概述 +为未登录用户实现了本地提问历史保存功能,使用 VueUse 的 `useStorage` 将提问保存到浏览器的 IndexedDB 中。 + +## 实现的功能 + +### 1. 自动保存提问 +- **触发时机**: 未登录用户成功发送提问后自动保存 +- **保存内容**: + - 提问ID(本地生成) + - 目标用户ID和用户名 + - 提问内容 + - 话题标签 + - 匿名昵称 + - 匿名邮箱 + - 是否包含图片 + - 发送时间 + +### 2. 本地记录按钮 +- **位置**: 在"我要提问"按钮旁边 +- **显示条件**: 仅对未登录用户显示 +- **功能**: + - 显示本地记录数量徽章 + - 点击打开本地提问历史抽屉 + +### 3. 本地提问历史抽屉 +- **布局**: 右侧抽屉,宽度 500px +- **功能**: + - 查看所有本地保存的提问 + - 显示每条提问的详细信息 + - 删除单条记录 + - 清空所有记录 + - 提示数据仅保存在浏览器中 + +### 4. 记录卡片显示 +每条本地提问记录包含: +- 目标用户名称 +- 话题标签(如果有) +- 提问内容 +- 发送时间(格式化显示) +- 匿名昵称(如果有) +- 匿名邮箱(如果有) +- 图片标识(如果有) +- 删除按钮 + +## 技术实现 + +### 使用的技术栈 +- **VueUse**: `useStorage` - 用于持久化存储 +- **Naive UI**: 提供 UI 组件(Drawer、Badge、Card 等) +- **TypeScript**: 类型安全 +- **Vue 3**: 组合式 API + +### 数据结构 +```typescript +interface LocalQuestion { + id: string // 本地生成的唯一ID + targetUserId: number // 目标用户ID + targetUserName: string // 目标用户名称 + message: string // 提问内容 + tag: string | null // 话题标签 + anonymousName: string // 匿名昵称 + anonymousEmail: string // 匿名邮箱 + hasImage: boolean // 是否包含图片 + sendAt: number // 发送时间戳 +} +``` + +### 存储方式 +```typescript +const localQuestions = useStorage( + 'vtsuru-local-questions', // 存储键名 + [], // 默认值 + undefined, // 使用默认存储(localStorage/sessionStorage) + { + serializer: { + read: (v: any) => v ? JSON.parse(v) : [], + write: (v: any) => JSON.stringify(v), + }, + } +) +``` + +## 新增的功能函数 + +### 1. deleteLocalQuestion +```typescript +function deleteLocalQuestion(id: string) +``` +删除指定ID的本地提问记录 + +### 2. clearAllLocalQuestions +```typescript +function clearAllLocalQuestions() +``` +清空所有本地提问记录 + +## UI 组件更新 + +### 新增导入 +- `History24Regular` - 历史图标 +- `NDrawer` - 抽屉组件 +- `NDrawerContent` - 抽屉内容 +- `NBadge` - 徽章组件 +- `useStorage` from '@vueuse/core' - 存储hook + +### 样式更新 +- 提问按钮区域改为横向布局 +- 新增本地历史按钮样式 +- 新增本地提问卡片样式 +- 优化过渡动画 + +## 用户体验优化 + +1. **自动保存**: 发送成功后自动保存,无需用户手动操作 +2. **徽章提示**: 按钮上显示记录数量,直观了解保存的提问数 +3. **详细信息**: 记录所有相关信息,方便用户回顾 +4. **便捷管理**: 支持单条删除和批量清空 +5. **数据提示**: 明确告知数据保存在本地浏览器中 +6. **响应式设计**: 抽屉宽度适配不同屏幕 + +## 注意事项 + +1. **数据持久性**: 数据保存在浏览器本地存储,清除浏览器数据会丢失 +2. **仅未登录用户**: 已登录用户可以在"我发送的"中查看完整历史 +3. **图片信息**: 仅记录是否包含图片,不保存图片内容 +4. **跨设备**: 记录仅在当前浏览器可见,不跨设备同步 + +## 使用场景 + +- 未登录用户想回顾自己发送的提问 +- 确认提问是否成功发送 +- 管理和清理本地提问记录 +- 离线查看已发送的提问内容 diff --git a/测试指南-本地提问保存.md b/测试指南-本地提问保存.md new file mode 100644 index 0000000..4a4d177 --- /dev/null +++ b/测试指南-本地提问保存.md @@ -0,0 +1,208 @@ +# 测试指南 - 本地提问保存功能 + +## 测试前准备 + +1. 确保已安装依赖: +```bash +bun install +``` + +2. 启动开发服务器: +```bash +bun dev +``` + +## 测试步骤 + +### 测试场景 1: 未登录用户发送提问并保存 + +1. **进入页面** + - 访问任意用户的提问箱页面 + - 确保处于未登录状态(退出登录) + +2. **查看按钮** + - 应该能看到两个按钮: + - "我要提问" (绿色主按钮) + - "本地记录" (蓝色按钮,带数字徽章) + - 初始状态徽章应该为 0 或不显示 + +3. **发送第一条提问** + - 点击"我要提问"按钮 + - 填写提问内容(至少3个字) + - 可选:填写昵称和邮箱 + - 可选:选择话题标签 + - 可选:上传图片(如果主播允许) + - 完成人机验证 + - 点击"发送"按钮 + +4. **验证自动保存** + - 发送成功后 + - "本地记录"按钮的徽章数字应该增加到 1 + - 提问表单应该自动收起 + +5. **查看本地记录** + - 点击"本地记录"按钮 + - 应该打开右侧抽屉 + - 能看到刚才发送的提问 + - 显示内容包括: + - 目标用户名称 + - 话题标签(如果有) + - 提问内容 + - 发送时间 + - 昵称(如果填写了) + - 邮箱(如果填写了) + - 图片标识(如果上传了) + +### 测试场景 2: 发送多条提问 + +1. **重复发送** + - 继续发送 2-3 条提问 + - 每次发送成功后,徽章数字应该增加 + +2. **查看记录列表** + - 打开本地记录抽屉 + - 应该看到所有发送的提问 + - 最新的提问在最上面 + - 每条记录都显示完整信息 + +### 测试场景 3: 删除单条记录 + +1. **删除操作** + - 打开本地记录抽屉 + - 找到想删除的记录 + - 点击记录右上角的删除按钮(X图标) + - 应该显示"已删除"提示 + +2. **验证删除** + - 记录应该从列表中消失 + - 徽章数字应该减少 1 + - 其他记录不受影响 + +### 测试场景 4: 清空所有记录 + +1. **清空操作** + - 打开本地记录抽屉 + - 点击右上角的"清空全部"按钮 + - 应该显示"已清空所有本地提问记录"提示 + +2. **验证清空** + - 所有记录都应该消失 + - 显示空状态提示:"还没有本地提问记录" + - 徽章数字应该消失或变为 0 + +### 测试场景 5: 已登录用户不显示本地记录 + +1. **登录账号** + - 使用任意账号登录 + +2. **验证显示** + - "本地记录"按钮应该不显示 + - 只显示"我要提问"和"我发送的"按钮 + - 已登录用户应该使用"我发送的"查看历史 + +### 测试场景 6: 数据持久性 + +1. **刷新页面** + - 发送几条提问后 + - 刷新浏览器页面 + - 本地记录应该仍然存在 + +2. **关闭重开** + - 关闭浏览器标签页 + - 重新打开页面 + - 本地记录应该仍然存在 + +3. **清除数据** + - 在浏览器开发者工具中 + - 清除本地存储(localStorage) + - 本地记录应该被清空 + +### 测试场景 7: 不同用户的提问 + +1. **访问用户A的提问箱** + - 发送一条提问 + - 记录应该保存目标用户名为"用户A" + +2. **访问用户B的提问箱** + - 发送一条提问 + - 记录应该保存目标用户名为"用户B" + +3. **查看本地记录** + - 打开本地记录抽屉 + - 应该看到两条记录 + - 分别标注了不同的目标用户 + +## 浏览器兼容性测试 + +建议在以下浏览器中测试: +- ✅ Chrome / Edge (推荐) +- ✅ Firefox +- ✅ Safari +- ⚠️ IE (不支持) + +## 开发者工具检查 + +### 查看存储数据 + +1. 打开浏览器开发者工具 (F12) +2. 切换到 Application/Storage 标签 +3. 找到 Local Storage +4. 查找键名:`vtsuru-local-questions` +5. 数据应该是 JSON 格式的数组 + +示例数据: +```json +[ + { + "id": "local-1234567890-abc123", + "targetUserId": 123, + "targetUserName": "测试用户", + "message": "这是一条测试提问", + "tag": "测试话题", + "anonymousName": "匿名用户", + "anonymousEmail": "test@example.com", + "hasImage": false, + "sendAt": 1234567890000 + } +] +``` + +## 已知限制 + +1. **存储大小**: 浏览器 localStorage 有大小限制(通常 5-10MB) +2. **跨域隔离**: 不同域名下的数据互不可见 +3. **无备份**: 清除浏览器数据会丢失所有记录 +4. **图片不保存**: 只记录是否包含图片,不保存图片内容 + +## 故障排查 + +### 问题:本地记录不显示 +- 检查是否处于未登录状态 +- 检查浏览器是否禁用了 localStorage +- 打开开发者工具查看是否有错误 + +### 问题:发送成功但没有保存 +- 检查开发者工具 Console 是否有错误 +- 确认 userInfo 对象有正确的 id 和 name +- 检查 isUserLoggedIn 计算属性是否正确 + +### 问题:刷新后记录消失 +- 检查浏览器隐私模式设置 +- 确认没有自动清除 Cookie 和存储的设置 +- 检查浏览器扩展是否影响存储 + +## 性能考虑 + +- ✅ 使用 VueUse 的 `useStorage` 自动处理序列化 +- ✅ 数据保存是同步的,不影响发送速度 +- ✅ 列表渲染使用虚拟滚动(如果记录很多) +- ⚠️ 建议不要保存超过 1000 条记录 + +## 反馈与改进 + +如果在测试过程中发现问题,请记录: +1. 浏览器类型和版本 +2. 操作步骤 +3. 预期结果 +4. 实际结果 +5. 控制台错误信息(如果有)