539 lines
12 KiB
Groovy
539 lines
12 KiB
Groovy
|
|
<template>
|
|||
|
|
<view class="history-container">
|
|||
|
|
<!-- 时间筛选 -->
|
|||
|
|
<view class="filter-section">
|
|||
|
|
<view class="filter-bar">
|
|||
|
|
<view
|
|||
|
|
v-for="(filter, index) in filters"
|
|||
|
|
:key="index"
|
|||
|
|
:class="['filter-item', activeFilter === index ? 'active' : '']"
|
|||
|
|
@click="handleFilterChange(index)"
|
|||
|
|
>
|
|||
|
|
{{ filter }}
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
<view class="clear-btn-wrapper">
|
|||
|
|
<button class="clear-btn" @click="clearAll">清空</button>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 历史列表 -->
|
|||
|
|
<scroll-view scroll-y class="history-list">
|
|||
|
|
<view v-if="loading" class="loading-box">
|
|||
|
|
<text class="loading-text">⏳ 加载中...</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view v-else-if="currentRecords.length === 0" class="empty-box">
|
|||
|
|
<text class="empty-icon">📋</text>
|
|||
|
|
<text class="empty-text">暂无历史记录</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view v-else>
|
|||
|
|
<view
|
|||
|
|
v-for="(group, date) in groupedRecords"
|
|||
|
|
:key="date"
|
|||
|
|
class="date-group"
|
|||
|
|
>
|
|||
|
|
<view class="date-header">{{ date }}</view>
|
|||
|
|
<view
|
|||
|
|
v-for="record in group"
|
|||
|
|
:key="record.id"
|
|||
|
|
class="history-item"
|
|||
|
|
@click="viewRecord(record)"
|
|||
|
|
>
|
|||
|
|
<view class="item-icon">{{ record.icon }}</view>
|
|||
|
|
<view class="item-content">
|
|||
|
|
<text class="item-title">{{ record.title }}</text>
|
|||
|
|
<text class="item-desc">{{ record.desc }}</text>
|
|||
|
|
<view class="item-footer">
|
|||
|
|
<text class="item-time">{{ record.time }}</text>
|
|||
|
|
<text v-if="record.amount !== 0" :class="['item-amount', record.amount > 0 ? 'refund-amount' : 'deduct-amount']">
|
|||
|
|
{{ record.amount > 0 ? '+' : '' }}{{ record.amount.toFixed(2) }}元
|
|||
|
|
</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="item-action" @click.stop="deleteRecord(record)">
|
|||
|
|
<image src="/static/iconfont/delete.svg" class="delete-icon" mode="aspectFit"></image>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</scroll-view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import { API_BASE, API_ENDPOINTS, buildURL } from '@/config/api.js';
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
API_BASE,
|
|||
|
|
activeFilter: 0,
|
|||
|
|
filters: ['全部', '今天', '本周', '本月'],
|
|||
|
|
loading: false,
|
|||
|
|
records: [
|
|||
|
|
{
|
|||
|
|
id: 1,
|
|||
|
|
type: 'voice',
|
|||
|
|
icon: '🎤',
|
|||
|
|
title: '创建了声音克隆',
|
|||
|
|
desc: '妈妈的声音',
|
|||
|
|
time: '10:30',
|
|||
|
|
date: '2024-12-04'
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 2,
|
|||
|
|
type: 'photo',
|
|||
|
|
icon: '📸',
|
|||
|
|
title: '复活了照片',
|
|||
|
|
desc: '爷爷的照片',
|
|||
|
|
time: '15:20',
|
|||
|
|
date: '2024-12-03'
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 3,
|
|||
|
|
type: 'call',
|
|||
|
|
icon: '📞',
|
|||
|
|
title: '进行了AI通话',
|
|||
|
|
desc: '与奶奶的对话',
|
|||
|
|
time: '09:15',
|
|||
|
|
date: '2024-12-03'
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 4,
|
|||
|
|
type: 'voice',
|
|||
|
|
icon: '🎤',
|
|||
|
|
title: '语音合成',
|
|||
|
|
desc: '合成了一段语音',
|
|||
|
|
time: '14:30',
|
|||
|
|
date: '2024-12-02'
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
};
|
|||
|
|
},
|
|||
|
|
computed: {
|
|||
|
|
currentRecords() {
|
|||
|
|
if (this.activeFilter === 0) {
|
|||
|
|
// 全部
|
|||
|
|
return this.records;
|
|||
|
|
} else if (this.activeFilter === 1) {
|
|||
|
|
// 今天
|
|||
|
|
const today = new Date().toISOString().split('T')[0];
|
|||
|
|
return this.records.filter(r => r.date === today);
|
|||
|
|
} else if (this.activeFilter === 2) {
|
|||
|
|
// 本周
|
|||
|
|
const now = new Date();
|
|||
|
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|||
|
|
return this.records.filter(r => {
|
|||
|
|
const recordDate = new Date(r.date);
|
|||
|
|
return recordDate >= weekAgo && recordDate <= now;
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
// 本月
|
|||
|
|
const now = new Date();
|
|||
|
|
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|||
|
|
return this.records.filter(r => {
|
|||
|
|
const recordDate = new Date(r.date);
|
|||
|
|
return recordDate >= monthAgo && recordDate <= now;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
groupedRecords() {
|
|||
|
|
const groups = {};
|
|||
|
|
this.currentRecords.forEach(record => {
|
|||
|
|
const dateLabel = this.getDateLabel(record.date);
|
|||
|
|
if (!groups[dateLabel]) {
|
|||
|
|
groups[dateLabel] = [];
|
|||
|
|
}
|
|||
|
|
groups[dateLabel].push(record);
|
|||
|
|
});
|
|||
|
|
return groups;
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onLoad() {
|
|||
|
|
this.loadHistory();
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
methods: {
|
|||
|
|
// 加载历史记录
|
|||
|
|
loadHistory() {
|
|||
|
|
this.loading = true;
|
|||
|
|
const userId = uni.getStorageSync('userId') || '';
|
|||
|
|
const token = uni.getStorageSync('token') || '';
|
|||
|
|
|
|||
|
|
uni.request({
|
|||
|
|
url: `${this.API_BASE}/api/history?userId=${userId}&page=0&size=100`,
|
|||
|
|
method: 'GET',
|
|||
|
|
header: {
|
|||
|
|
'X-User-Id': userId,
|
|||
|
|
'Authorization': token ? `Bearer ${token}` : ''
|
|||
|
|
},
|
|||
|
|
success: (res) => {
|
|||
|
|
console.log('[History] 用户ID:', userId);
|
|||
|
|
console.log('[History] API响应:', res.data);
|
|||
|
|
if (res.data && res.data.content) {
|
|||
|
|
// 转换后端数据格式为前端格式,过滤掉AI对话记录
|
|||
|
|
this.records = res.data.content
|
|||
|
|
.filter(item => item.actionType !== 'AI_CALL') // 过滤掉AI对话记录
|
|||
|
|
.map(item => {
|
|||
|
|
const date = new Date(item.createdAt);
|
|||
|
|
const hours = String(date.getHours()).padStart(2, '0');
|
|||
|
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|||
|
|
const year = date.getFullYear();
|
|||
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|||
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
id: item.id,
|
|||
|
|
type: item.actionType?.toLowerCase() || 'voice',
|
|||
|
|
icon: this.getIconByType(item.actionType),
|
|||
|
|
title: item.actionTitle || '操作记录',
|
|||
|
|
desc: item.actionDesc || '',
|
|||
|
|
time: `${hours}:${minutes}`,
|
|||
|
|
date: `${year}-${month}-${day}`,
|
|||
|
|
amount: item.amount || 0
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
console.log('[History] 转换后的记录:', this.records);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
fail: (err) => {
|
|||
|
|
console.error('[History] 加载失败:', err);
|
|||
|
|
// 保留模拟数据作为后备
|
|||
|
|
},
|
|||
|
|
complete: () => {
|
|||
|
|
this.loading = false;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 处理筛选变化
|
|||
|
|
handleFilterChange(index) {
|
|||
|
|
console.log('[Filter] 切换筛选:', this.filters[index]);
|
|||
|
|
this.activeFilter = index;
|
|||
|
|
console.log('[Filter] 当前记录数:', this.records.length);
|
|||
|
|
console.log('[Filter] 筛选后记录数:', this.currentRecords.length);
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 根据类型获取图标
|
|||
|
|
getIconByType(type) {
|
|||
|
|
const icons = {
|
|||
|
|
'CREATE_VOICE': '🎤',
|
|||
|
|
'SYNTHESIZE': '🎤',
|
|||
|
|
'PHOTO_REVIVAL': '📸',
|
|||
|
|
'VIDEO_CALL': '📞',
|
|||
|
|
'AI_CALL': '💬'
|
|||
|
|
};
|
|||
|
|
return icons[type] || '📋';
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
formatDate(date) {
|
|||
|
|
const year = date.getFullYear();
|
|||
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|||
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|||
|
|
return `${year}-${month}-${day}`;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
getDateLabel(dateStr) {
|
|||
|
|
const today = this.formatDate(new Date());
|
|||
|
|
const yesterday = this.formatDate(new Date(Date.now() - 24 * 60 * 60 * 1000));
|
|||
|
|
|
|||
|
|
if (dateStr === today) {
|
|||
|
|
return '今天';
|
|||
|
|
} else if (dateStr === yesterday) {
|
|||
|
|
return '昨天';
|
|||
|
|
} else {
|
|||
|
|
return dateStr;
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
viewRecord(record) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '查看:' + record.title,
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
deleteRecord(record) {
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '删除记录',
|
|||
|
|
content: `确定要删除"${record.title}"吗?`,
|
|||
|
|
success: (res) => {
|
|||
|
|
if (res.confirm) {
|
|||
|
|
const userId = uni.getStorageSync('userId') || '';
|
|||
|
|
const token = uni.getStorageSync('token') || '';
|
|||
|
|
|
|||
|
|
// 调用后端API删除
|
|||
|
|
uni.request({
|
|||
|
|
url: `${this.API_BASE}/api/history/${record.id}?userId=${userId}`,
|
|||
|
|
method: 'DELETE',
|
|||
|
|
header: {
|
|||
|
|
'X-User-Id': userId,
|
|||
|
|
'Authorization': token ? `Bearer ${token}` : ''
|
|||
|
|
},
|
|||
|
|
success: (apiRes) => {
|
|||
|
|
console.log('[Delete] API响应:', apiRes.data);
|
|||
|
|
if (apiRes.statusCode === 200) {
|
|||
|
|
// 从本地数组中删除
|
|||
|
|
const index = this.records.findIndex(r => r.id === record.id);
|
|||
|
|
if (index > -1) {
|
|||
|
|
this.records.splice(index, 1);
|
|||
|
|
}
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '删除成功',
|
|||
|
|
icon: 'success'
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '删除失败',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
fail: (err) => {
|
|||
|
|
console.error('[Delete] 删除失败:', err);
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '删除失败',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
clearAll() {
|
|||
|
|
if (this.records.length === 0) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '暂无记录',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '清空历史',
|
|||
|
|
content: '确定要清空所有历史记录吗?',
|
|||
|
|
success: (res) => {
|
|||
|
|
if (res.confirm) {
|
|||
|
|
const userId = uni.getStorageSync('userId') || '';
|
|||
|
|
const token = uni.getStorageSync('token') || '';
|
|||
|
|
|
|||
|
|
// 批量删除所有记录
|
|||
|
|
const deletePromises = this.records.map(record => {
|
|||
|
|
return new Promise((resolve) => {
|
|||
|
|
uni.request({
|
|||
|
|
url: `${this.API_BASE}/api/history/${record.id}?userId=${userId}`,
|
|||
|
|
method: 'DELETE',
|
|||
|
|
header: {
|
|||
|
|
'X-User-Id': userId,
|
|||
|
|
'Authorization': token ? `Bearer ${token}` : ''
|
|||
|
|
},
|
|||
|
|
success: () => resolve(true),
|
|||
|
|
fail: () => resolve(false)
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 等待所有删除完成
|
|||
|
|
Promise.all(deletePromises).then(() => {
|
|||
|
|
this.records = [];
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '已清空',
|
|||
|
|
icon: 'success'
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.history-container {
|
|||
|
|
min-height: 100vh;
|
|||
|
|
background: #FDF8F2;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 筛选区域 */
|
|||
|
|
.filter-section {
|
|||
|
|
padding: 20upx 30upx;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 20upx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 筛选栏 */
|
|||
|
|
.filter-bar {
|
|||
|
|
flex: 1;
|
|||
|
|
display: flex;
|
|||
|
|
background: rgba(255, 255, 255, 0.95);
|
|||
|
|
padding: 20upx;
|
|||
|
|
gap: 20upx;
|
|||
|
|
box-shadow: 0 2upx 8upx rgba(0, 0, 0, 0.05);
|
|||
|
|
border-radius: 32upx;
|
|||
|
|
backdrop-filter: blur(10upx);
|
|||
|
|
border: 2upx solid rgba(255, 255, 255, 0.8);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.clear-btn-wrapper {
|
|||
|
|
.clear-btn {
|
|||
|
|
padding: 20upx 32upx;
|
|||
|
|
background: rgba(255, 255, 255, 0.95);
|
|||
|
|
border: 2upx solid #ff6b6b;
|
|||
|
|
border-radius: 24upx;
|
|||
|
|
color: #ff6b6b;
|
|||
|
|
font-size: 26upx;
|
|||
|
|
font-weight: 600;
|
|||
|
|
backdrop-filter: blur(10upx);
|
|||
|
|
transition: all 0.3s;
|
|||
|
|
|
|||
|
|
&:active {
|
|||
|
|
background: #ff6b6b;
|
|||
|
|
color: white;
|
|||
|
|
transform: scale(0.95);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.filter-item {
|
|||
|
|
flex: 1;
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 16upx 0;
|
|||
|
|
font-size: 26upx;
|
|||
|
|
color: #666;
|
|||
|
|
border-radius: 20upx;
|
|||
|
|
background: #f5f5f5;
|
|||
|
|
transition: all 0.3s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.filter-item.active {
|
|||
|
|
background: linear-gradient(135deg, #8B7355 0%, #6D8B8B 100%);
|
|||
|
|
color: white;
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 历史列表 */
|
|||
|
|
.history-list {
|
|||
|
|
flex: 1;
|
|||
|
|
padding: 30upx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.loading-box,
|
|||
|
|
.empty-box {
|
|||
|
|
padding: 200upx 40upx;
|
|||
|
|
text-align: center;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.loading-text {
|
|||
|
|
font-size: 28upx;
|
|||
|
|
color: #999;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-icon {
|
|||
|
|
font-size: 120upx;
|
|||
|
|
margin-bottom: 30upx;
|
|||
|
|
opacity: 0.5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-text {
|
|||
|
|
font-size: 32upx;
|
|||
|
|
color: #666;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 日期分组 */
|
|||
|
|
.date-group {
|
|||
|
|
margin-bottom: 40upx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.date-header {
|
|||
|
|
font-size: 26upx;
|
|||
|
|
color: #999;
|
|||
|
|
padding: 20upx 0;
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-item {
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 30upx;
|
|||
|
|
padding: 30upx;
|
|||
|
|
margin-bottom: 20upx;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
box-shadow: 0 8upx 30upx rgba(0, 0, 0, 0.08);
|
|||
|
|
transition: all 0.3s;
|
|||
|
|
|
|||
|
|
&:active {
|
|||
|
|
transform: translateX(-8upx);
|
|||
|
|
box-shadow: 0 12upx 40upx rgba(0, 0, 0, 0.12);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-icon {
|
|||
|
|
width: 80upx;
|
|||
|
|
height: 80upx;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: linear-gradient(135deg, rgba(139, 115, 85, 0.1) 0%, rgba(109, 139, 139, 0.1) 100%);
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
font-size: 40upx;
|
|||
|
|
margin-right: 20upx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-content {
|
|||
|
|
flex: 1;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
|
|||
|
|
.item-title {
|
|||
|
|
font-size: 30upx;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: #333;
|
|||
|
|
margin-bottom: 8upx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-desc {
|
|||
|
|
font-size: 26upx;
|
|||
|
|
color: #666;
|
|||
|
|
margin-bottom: 8upx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-footer {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-time {
|
|||
|
|
font-size: 22upx;
|
|||
|
|
color: #999;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-amount {
|
|||
|
|
font-size: 24upx;
|
|||
|
|
color: #ff6b6b;
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-action {
|
|||
|
|
padding: 10upx;
|
|||
|
|
margin-right: 20upx;
|
|||
|
|
|
|||
|
|
.action-icon {
|
|||
|
|
font-size: 36upx;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|