fix: 充值记录支持分页查询

This commit is contained in:
Gsh
2026-01-11 22:00:19 +08:00
parent d9f5f1f050
commit 6b6ddcf550
3 changed files with 406 additions and 94 deletions

View File

@@ -60,9 +60,57 @@ export function getApiKey() {
return get<any>('/token').json(); return get<any>('/token').json();
} }
// 充值记录查询参数类型
export interface RechargeLogQueryParams {
skipCount?: number;
maxResultCount?: number;
isFree?: boolean;
minRechargeAmount?: number;
maxRechargeAmount?: number;
startTime?: string;
endTime?: string;
orderByColumn?: string;
isAsc?: string;
isAscending?: boolean;
}
// 查询充值记录 // 查询充值记录
export function getRechargeLog() { export function getRechargeLog(params?: RechargeLogQueryParams) {
return get<any>('/recharge/account').json(); const queryParams = new URLSearchParams();
if (params?.skipCount !== undefined) {
queryParams.append('SkipCount', params.skipCount.toString());
}
if (params?.maxResultCount !== undefined) {
queryParams.append('MaxResultCount', params.maxResultCount.toString());
}
if (params?.isFree !== undefined) {
queryParams.append('IsFree', params.isFree.toString());
}
if (params?.minRechargeAmount !== undefined) {
queryParams.append('MinRechargeAmount', params.minRechargeAmount.toString());
}
if (params?.maxRechargeAmount !== undefined) {
queryParams.append('MaxRechargeAmount', params.maxRechargeAmount.toString());
}
if (params?.startTime) {
queryParams.append('StartTime', params.startTime);
}
if (params?.endTime) {
queryParams.append('EndTime', params.endTime);
}
if (params?.orderByColumn) {
queryParams.append('OrderByColumn', params.orderByColumn);
}
if (params?.isAsc) {
queryParams.append('IsAsc', params.isAsc);
}
if (params?.isAscending !== undefined) {
queryParams.append('IsAscending', params.isAscending.toString());
}
const queryString = queryParams.toString();
const url = queryString ? `/recharge/account?${queryString}` : '/recharge/account';
return get<any>(url).json();
} }
// 查询用户近7天token消耗 // 查询用户近7天token消耗

View File

@@ -1,8 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ChatLineRound, List, Refresh, Search } from '@element-plus/icons-vue'; import { ChatLineRound, List, Refresh } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { computed, onMounted, onUnmounted, ref } from 'vue'; import { computed, onMounted, onUnmounted, ref } from 'vue';
import { getRechargeLog } from '@/api/model/index.ts'; import { getRechargeLog } from '@/api/model/index.ts';
import type { RechargeLogQueryParams } from '@/api/model/index.ts';
import { isUserVip } from '@/utils/user.ts'; import { isUserVip } from '@/utils/user.ts';
import { showContactUs } from '@/utils/contact-us.ts'; import { showContactUs } from '@/utils/contact-us.ts';
@@ -19,11 +20,53 @@ interface RechargeLog {
const loading = ref(false); const loading = ref(false);
const logData = ref<RechargeLog[]>([]); const logData = ref<RechargeLog[]>([]);
const searchText = ref(''); const totalCount = ref(0);
const currentSort = ref({ prop: '', order: '' });
const currentPage = ref(1); const currentPage = ref(1);
const pageSize = ref(10); const pageSize = ref(10);
// 查询参数
const queryParams = ref<RechargeLogQueryParams>({
skipCount: 0,
maxResultCount: 10,
});
// 筛选条件
const dateRange = ref<[Date, Date] | null>(null);
const freeFilter = ref<boolean | null>(null);
const minAmount = ref<number | undefined>(undefined);
const maxAmount = ref<number | undefined>(undefined);
// 快捷时间选择
const shortcuts = [
{
text: '最近一周',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
return [start, end];
},
},
{
text: '最近一个月',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
return [start, end];
},
},
{
text: '最近三个月',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
return [start, end];
},
},
];
// 移动端检测 // 移动端检测
const isMobile = ref(false); const isMobile = ref(false);
@@ -32,15 +75,36 @@ function checkMobile() {
} }
// 模拟数据获取 // 模拟数据获取
async function fetchRechargeLog() { async function fetchRechargeLog(resetPage = false) {
if (resetPage) {
currentPage.value = 1;
}
loading.value = true;
try { try {
loading.value = true; const params: RechargeLogQueryParams = {
const res = await getRechargeLog(); skipCount: currentPage.value,
logData.value = res.data || []; maxResultCount: queryParams.value.maxResultCount,
startTime: queryParams.value.startTime,
endTime: queryParams.value.endTime,
orderByColumn: queryParams.value.orderByColumn,
isAsc: queryParams.value.isAsc,
isFree: queryParams.value.isFree,
minRechargeAmount: queryParams.value.minRechargeAmount,
maxRechargeAmount: queryParams.value.maxRechargeAmount,
};
const res = await getRechargeLog(params);
if (res.data) {
logData.value = res.data.items || [];
totalCount.value = res.data.totalCount || 0;
}
} }
catch (error) { catch (error) {
console.error('获取充值记录失败:', error); console.error('获取充值记录失败:', error);
ElMessage.error('获取充值记录失败'); ElMessage.error('获取充值记录失败');
logData.value = [];
totalCount.value = 0;
} }
finally { finally {
loading.value = false; loading.value = false;
@@ -48,26 +112,89 @@ async function fetchRechargeLog() {
} }
// 排序处理 // 排序处理
function handleSortChange({ prop, order }: { prop: string; order: string }) { function handleSortChange({ prop, order }: { prop: string; order: string | null }) {
currentSort.value = { prop, order }; if (order) {
queryParams.value.orderByColumn = prop;
queryParams.value.isAsc = order === 'ascending' ? 'ascending' : 'descending';
}
else {
queryParams.value.orderByColumn = undefined;
queryParams.value.isAsc = undefined;
}
fetchRechargeLog(true);
} }
// 搜索处理 // 处理分页
function handleSearch() { function handlePageChange(page: number) {
currentPage.value = 1; currentPage.value = page;
fetchRechargeLog();
} }
function handleSearchClear() { // 处理每页大小变化
searchText.value = ''; function handleSizeChange(size: number) {
pageSize.value = size;
queryParams.value.maxResultCount = size;
currentPage.value = 1; currentPage.value = 1;
fetchRechargeLog();
} }
// 处理时间筛选
function handleDateChange(value: [Date, Date] | null) {
if (value && value.length === 2) {
const startDate = new Date(value[0]);
startDate.setHours(0, 0, 0, 0);
const endDate = new Date(value[1]);
endDate.setHours(23, 59, 59, 999);
queryParams.value.startTime = startDate.toISOString();
queryParams.value.endTime = endDate.toISOString();
}
else {
queryParams.value.startTime = undefined;
queryParams.value.endTime = undefined;
}
fetchRechargeLog(true);
}
// 处理是否免费筛选
function handleFreeFilterChange(value: boolean | null) {
queryParams.value.isFree = value === null ? undefined : value;
fetchRechargeLog(true);
}
// 处理金额筛选
function handleAmountChange() {
queryParams.value.minRechargeAmount = minAmount.value;
queryParams.value.maxRechargeAmount = maxAmount.value;
fetchRechargeLog(true);
}
// 重置筛选
function resetFilters() {
dateRange.value = null;
freeFilter.value = null;
minAmount.value = undefined;
maxAmount.value = undefined;
queryParams.value = {
maxResultCount: pageSize.value,
orderByColumn: undefined,
isAsc: undefined,
startTime: undefined,
endTime: undefined,
isFree: undefined,
minRechargeAmount: undefined,
maxRechargeAmount: undefined,
};
fetchRechargeLog(true);
}
// 判断是否有活动的筛选条件
const hasActiveFilters = computed(() => {
return !!dateRange.value || freeFilter.value !== null || minAmount.value !== undefined || maxAmount.value !== undefined;
});
// 刷新数据 // 刷新数据
function refreshLog() { function refreshLog() {
fetchRechargeLog(); fetchRechargeLog();
currentPage.value = 1;
searchText.value = '';
currentSort.value = { prop: '', order: '' };
} }
// 联系售后弹窗 // 联系售后弹窗
@@ -84,44 +211,7 @@ function contactCustomerService() {
// 暴露方法给父组件使用 // 暴露方法给父组件使用
defineExpose({ defineExpose({
contactCustomerService, contactCustomerService,
}); refresh: fetchRechargeLog,
// 过滤和排序后的数据
const filteredData = computed(() => {
let data = [...logData.value];
// 搜索过滤
if (searchText.value) {
const search = searchText.value.toLowerCase();
data = data.filter(item =>
(item.contactInfo && item.contactInfo.toLowerCase().includes(search))
|| (item.remark && item.remark.toLowerCase().includes(search)),
);
}
// 排序处理
if (currentSort.value.prop) {
const { prop, order } = currentSort.value;
data.sort((a, b) => {
// 处理可能为null的值
const valA = a[prop as keyof RechargeLog] || '';
const valB = b[prop as keyof RechargeLog] || '';
if (order === 'ascending') {
return valA > valB ? 1 : valA < valB ? -1 : 0;
}
else {
return valA < valB ? 1 : valA > valB ? -1 : 0;
}
});
}
return data;
});
// 是否显示分页 暂时不需要分页功能
const showPagination = computed(() => {
return false;
}); });
onMounted(() => { onMounted(() => {
@@ -143,6 +233,9 @@ onUnmounted(() => {
充值记录 充值记录
</h2> </h2>
<div class="header-actions"> <div class="header-actions">
<el-tag v-if="totalCount > 0" type="primary" size="default" class="count-tag" effect="plain">
{{ totalCount }} 条记录
</el-tag>
<el-tooltip v-if="isUserVip()" content="联系售后" placement="top"> <el-tooltip v-if="isUserVip()" content="联系售后" placement="top">
<el-button circle :loading="loading" @click="contactCustomerService"> <el-button circle :loading="loading" @click="contactCustomerService">
<el-icon color="#07c160"> <el-icon color="#07c160">
@@ -155,26 +248,80 @@ onUnmounted(() => {
<el-icon><Refresh /></el-icon> <el-icon><Refresh /></el-icon>
</el-button> </el-button>
</el-tooltip> </el-tooltip>
<el-input </div>
v-model="searchText" </div>
placeholder="搜索备注"
clearable <!-- 筛选工具栏 -->
style="width: 200px; margin-left: 10px;" <div class="filter-toolbar">
@clear="handleSearchClear" <div class="filter-row">
@keyup.enter="handleSearch" <div class="filter-item">
> <el-date-picker
<template #append> v-model="dateRange"
<el-button @click="handleSearch"> type="daterange"
<el-icon><Search /></el-icon> range-separator=""
</el-button> start-placeholder="开始日期"
</template> end-placeholder="结束日期"
</el-input> size="default"
:shortcuts="shortcuts"
class="date-picker"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:editable="false"
@change="handleDateChange"
/>
</div>
<div class="filter-item">
<el-select
v-model="freeFilter"
placeholder="全部类型"
clearable
size="default"
class="free-select"
@change="handleFreeFilterChange"
>
<el-option label="免费" :value="true" />
<el-option label="付费" :value="false" />
</el-select>
</div>
<div class="filter-item amount-range">
<el-input-number
v-model="minAmount"
:min="0"
:precision="2"
:controls="false"
placeholder="最小金额"
size="default"
class="amount-input"
@change="handleAmountChange"
/>
<span class="range-separator">-</span>
<el-input-number
v-model="maxAmount"
:min="0"
:precision="2"
:controls="false"
placeholder="最大金额"
size="default"
class="amount-input"
@change="handleAmountChange"
/>
</div>
<div class="filter-actions">
<el-button
v-if="hasActiveFilters"
size="default"
@click="resetFilters"
>
<el-icon><i-ep-refresh-left /></el-icon>
重置
</el-button>
</div>
</div> </div>
</div> </div>
<el-table <el-table
v-loading="loading" v-loading="loading"
:data="filteredData" :data="logData"
style="width: 100%" style="width: 100%"
border border
stripe stripe
@@ -185,7 +332,7 @@ onUnmounted(() => {
prop="content" prop="content"
label="套餐类型" label="套餐类型"
min-width="150" min-width="150"
sortable="custom" sortable
show-overflow-tooltip show-overflow-tooltip
/> />
<el-table-column <el-table-column
@@ -193,7 +340,7 @@ onUnmounted(() => {
prop="rechargeAmount" prop="rechargeAmount"
label="金额(元)" label="金额(元)"
min-width="110" min-width="110"
sortable="custom" sortable
> >
<template #default="{ row }"> <template #default="{ row }">
<span class="amount-cell">¥{{ row.rechargeAmount.toFixed(2) }}</span> <span class="amount-cell">¥{{ row.rechargeAmount.toFixed(2) }}</span>
@@ -203,14 +350,14 @@ onUnmounted(() => {
prop="creationTime" prop="creationTime"
label="充值时间" label="充值时间"
min-width="160" min-width="160"
sortable="custom" sortable
show-overflow-tooltip show-overflow-tooltip
/> />
<el-table-column <el-table-column
prop="expireDateTime" prop="expireDateTime"
label="到期时间" label="到期时间"
min-width="160" min-width="160"
sortable="custom" sortable
show-overflow-tooltip show-overflow-tooltip
/> />
<!-- <el-table-column show-overflow-tooltip prop="contactInfo" width="100" label="联系方式"> <!-- <el-table-column show-overflow-tooltip prop="contactInfo" width="100" label="联系方式">
@@ -232,18 +379,29 @@ onUnmounted(() => {
</el-table> </el-table>
<div class="log-footer"> <div class="log-footer">
<div class="summary"> <!-- 空状态 -->
<span class="highlight">{{ filteredData.length }}</span> 条记录 <div v-if="!logData.length && !loading" class="empty-container">
<el-empty :description="hasActiveFilters ? '当前筛选条件下无数据' : '暂无充值记录'">
<el-button v-if="hasActiveFilters" type="primary" @click="resetFilters">
清除筛选
</el-button>
</el-empty>
</div>
<!-- 分页 -->
<div v-if="totalCount > 0" class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="totalCount"
layout="total, sizes, prev, pager, next, jumper"
prev-text="上一页"
next-text="下一页"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div> </div>
<el-pagination
v-if="showPagination"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="filteredData.length"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
background
/>
</div> </div>
</div> </div>
</template> </template>
@@ -355,14 +513,86 @@ onUnmounted(() => {
} }
.log-footer { .log-footer {
display: flex; flex-direction: column;
justify-content: space-between;
align-items: center;
margin-top: 25px; margin-top: 25px;
padding-top: 20px; padding-top: 20px;
border-top: 2px solid #e9ecef; border-top: 2px solid #e9ecef;
} }
/* 筛选工具栏 */
.filter-toolbar {
margin-bottom: 20px;
padding: 16px;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border-radius: 12px;
border: 1px solid #e8eaed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.filter-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.filter-item {
flex: 0 0 auto;
}
.date-picker {
width: 320px;
}
.free-select {
width: 140px;
}
/* 金额范围筛选 */
.amount-range {
display: flex;
align-items: center;
gap: 8px;
}
.amount-input {
width: 120px;
}
.range-separator {
color: #909399;
font-size: 14px;
}
.filter-actions {
margin-left: auto;
display: flex;
gap: 8px;
}
/* 记录数标签 */
.count-tag {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
font-weight: 600;
border-radius: 16px;
}
/* 空状态 */
.empty-container {
padding: 40px 0;
width: 100%;
}
/* 分页容器 */
.pagination-container {
width: 100%;
display: flex;
justify-content: center;
}
.summary { .summary {
color: #666; color: #666;
font-size: 15px; font-size: 15px;
@@ -455,10 +685,43 @@ onUnmounted(() => {
width: 100%; width: 100%;
} }
.header-actions .el-input { /* 移动端筛选工具栏 */
width: 100% !important; .filter-toolbar {
margin-left: 0 !important; padding: 12px;
margin-top: 8px; }
.filter-row {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.filter-item {
width: 100%;
}
.date-picker,
.free-select {
width: 100%;
}
.amount-range {
width: 100%;
}
.amount-input {
flex: 1;
width: auto;
}
.filter-actions {
margin-left: 0;
display: flex;
gap: 8px;
}
.filter-actions .el-button {
flex: 1;
} }
/* 表格移动端优化 */ /* 表格移动端优化 */

View File

@@ -7,6 +7,7 @@ interface ImportMetaEnv {
readonly VITE_WEB_BASE_API: string; readonly VITE_WEB_BASE_API: string;
readonly VITE_API_URL: string; readonly VITE_API_URL: string;
readonly VITE_FILE_UPLOAD_API: string; readonly VITE_FILE_UPLOAD_API: string;
readonly VITE_BUILD_COMPRESS: string;
readonly VITE_SSO_SEVER_URL: string; readonly VITE_SSO_SEVER_URL: string;
readonly VITE_APP_VERSION: string; readonly VITE_APP_VERSION: string;
} }