feat: P3 功能增强完成
- 下拉刷新:index / strategies / me / detail 四个页面 - 上拉加载:detail 交易记录分页加载 - 空状态优化:index / strategies / detail 空状态提示 - pages.json 开启 enablePullDownRefresh 功能细节: - onPullDownRefresh / onReachBottom 生命周期 - 交易记录分页逻辑(logPage / logHasMore / logLoading) - 加载状态提示
This commit is contained in:
parent
365c461ea4
commit
762665bfd5
20
pages.json
20
pages.json
@ -1,27 +1,35 @@
|
||||
{
|
||||
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "资产"
|
||||
"navigationBarTitleText": "资产",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundTextStyle": "dark"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/strategies/strategies",
|
||||
"style": {
|
||||
"navigationBarTitleText": "策略库"
|
||||
"navigationBarTitleText": "策略库",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundTextStyle": "dark"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/me/me",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的"
|
||||
"navigationBarTitleText": "我的",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundTextStyle": "dark"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/detail/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": ""
|
||||
"navigationBarTitleText": "",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundTextStyle": "dark"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -75,4 +83,4 @@
|
||||
"^u-(.*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -257,9 +257,16 @@
|
||||
<view class="section-container">
|
||||
<view class="section-header">
|
||||
<text class="section-title">最近交易记录</text>
|
||||
<text class="section-count" v-if="logTotal > 0">共 {{ logTotal }} 条</text>
|
||||
</view>
|
||||
|
||||
<view class="timeline-box">
|
||||
<!-- 空状态 -->
|
||||
<view v-if="logs.length === 0 && !loading" class="empty-state-sm">
|
||||
<uni-icons type="list" size="48" color="#D1D5DB"></uni-icons>
|
||||
<text class="empty-text-sm">暂无交易记录</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="timeline-box">
|
||||
<view class="timeline-item" v-for="(log, k) in logs" :key="k">
|
||||
<view class="tl-left">
|
||||
<text class="tl-date">{{ log.date }}</text>
|
||||
@ -281,6 +288,13 @@
|
||||
<text class="tl-desc mt-1">{{ log.type === 'buy' ? '买入' : '卖出' }} 操作</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
<view class="load-more" v-if="logs.length > 0">
|
||||
<text class="load-more-text" v-if="logLoading">加载中...</text>
|
||||
<text class="load-more-text" v-else-if="!logHasMore">没有更多了</text>
|
||||
<text class="load-more-text" v-else>上拉加载更多</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -551,6 +565,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, getCurrentInstance, nextTick } from 'vue';
|
||||
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app';
|
||||
import { api } from '@/utils/api';
|
||||
import type { PositionItem, TransactionItem, NavHistoryItem, NavStatistics } from '@/types';
|
||||
|
||||
@ -582,6 +597,13 @@ const portfolioData = ref<any>({
|
||||
const positions = ref<PositionItem[]>([]);
|
||||
const logs = ref<TransactionItem[]>([]);
|
||||
|
||||
// 交易记录分页
|
||||
const logPage = ref<number>(0);
|
||||
const logPageSize = ref<number>(10);
|
||||
const logTotal = ref<number>(0);
|
||||
const logHasMore = ref<boolean>(true);
|
||||
const logLoading = ref<boolean>(false);
|
||||
|
||||
const navLoading = ref<boolean>(false);
|
||||
const navPeriod = ref<string>('30d');
|
||||
const navHistory = ref<NavHistoryItem[]>([]);
|
||||
@ -915,19 +937,35 @@ const fetchPortfolioData = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTransactions = async () => {
|
||||
const fetchTransactions = async (loadMore: boolean = false) => {
|
||||
try {
|
||||
if (!portfolioId.value) return;
|
||||
if (loadMore && logLoading.value) return;
|
||||
if (loadMore && !logHasMore.value) return;
|
||||
|
||||
logLoading.value = true;
|
||||
|
||||
const offset = loadMore ? (logPage.value + 1) * logPageSize.value : 0;
|
||||
|
||||
const response = await api.assets.getTransactions({
|
||||
portfolioId: portfolioId.value,
|
||||
limit: 10,
|
||||
offset: 0
|
||||
limit: logPageSize.value,
|
||||
offset: offset
|
||||
});
|
||||
|
||||
if (response.code === 200) {
|
||||
logs.value = response.data.items || [];
|
||||
console.log('交易记录获取成功:', response.data);
|
||||
const items = response.data.items || [];
|
||||
|
||||
if (loadMore) {
|
||||
logs.value = [...logs.value, ...items];
|
||||
logPage.value++;
|
||||
} else {
|
||||
logs.value = items;
|
||||
logPage.value = 0;
|
||||
}
|
||||
|
||||
logTotal.value = response.data.total || 0;
|
||||
logHasMore.value = logs.value.length < logTotal.value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取交易记录失败:', error);
|
||||
@ -936,6 +974,8 @@ const fetchTransactions = async () => {
|
||||
message: '加载交易记录失败',
|
||||
icon: 'error'
|
||||
});
|
||||
} finally {
|
||||
logLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@ -994,6 +1034,23 @@ onMounted(async () => {
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
// 下拉刷新
|
||||
onPullDownRefresh(async () => {
|
||||
await Promise.all([
|
||||
fetchPortfolioData(),
|
||||
fetchTransactions(),
|
||||
fetchNavHistory()
|
||||
]);
|
||||
uni.stopPullDownRefresh();
|
||||
});
|
||||
|
||||
// 上拉加载更多交易记录
|
||||
onReachBottom(async () => {
|
||||
if (logHasMore.value && !logLoading.value) {
|
||||
await fetchTransactions(true);
|
||||
}
|
||||
});
|
||||
|
||||
const goStrategyConfig = () => {
|
||||
if (portfolioData.value.strategy?.id) {
|
||||
uni.navigateTo({ url: `/pages/strategies/edit/edit?id=${portfolioData.value.strategy.id}` });
|
||||
@ -1717,4 +1774,40 @@ const deletePortfolio = async () => {
|
||||
font-size: 28rpx;
|
||||
color: #1F2937;
|
||||
}
|
||||
|
||||
/* 空状态小 */
|
||||
.empty-state-sm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60rpx 40rpx;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.empty-text-sm {
|
||||
font-size: 26rpx;
|
||||
color: #9CA3AF;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
/* 加载更多 */
|
||||
.load-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 32rpx 0;
|
||||
}
|
||||
|
||||
.load-more-text {
|
||||
font-size: 24rpx;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
/* 标题栏附加信息 */
|
||||
.section-count {
|
||||
font-size: 24rpx;
|
||||
color: #9CA3AF;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
</style>
|
||||
@ -85,6 +85,19 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else-if="holdings.length === 0" class="empty-state">
|
||||
<view class="empty-icon">
|
||||
<uni-icons type="folder" size="64" color="#D1D5DB"></uni-icons>
|
||||
</view>
|
||||
<text class="empty-title">暂无组合记录</text>
|
||||
<text class="empty-desc">点击上方"新建组合"开始记账</text>
|
||||
<view class="empty-btn" @click="goConfig">
|
||||
<uni-icons type="plus" size="16" color="#064E3B"></uni-icons>
|
||||
<text class="empty-btn-text">新建第一个组合</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 真实内容:持仓卡片 -->
|
||||
<view
|
||||
v-else
|
||||
@ -137,12 +150,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { onShow, onPullDownRefresh } from '@dcloudio/uni-app';
|
||||
import { api } from '@/utils/api';
|
||||
import { getCurrencySymbol } from '@/utils/currency';
|
||||
import type { PortfolioListItem, TotalAssetsResponse } from '@/types';
|
||||
|
||||
const loading = ref<boolean>(true);
|
||||
const refreshing = ref<boolean>(false);
|
||||
|
||||
const assetData = ref<TotalAssetsResponse>({
|
||||
totalValue: 0,
|
||||
@ -236,6 +250,22 @@ onShow(async () => {
|
||||
loading.value = false;
|
||||
isFetching = false;
|
||||
});
|
||||
|
||||
// 下拉刷新
|
||||
onPullDownRefresh(async () => {
|
||||
refreshing.value = true;
|
||||
await Promise.all([fetchAssetData(), fetchHoldingsData()]);
|
||||
refreshing.value = false;
|
||||
uni.stopPullDownRefresh();
|
||||
});
|
||||
|
||||
// 刷新按钮点击
|
||||
const handleRefresh = async (): Promise<void> => {
|
||||
loading.value = true;
|
||||
await Promise.all([fetchAssetData(), fetchHoldingsData()]);
|
||||
loading.value = false;
|
||||
uni.showToast({ title: '刷新成功', icon: 'success', duration: 1500 });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -671,4 +701,53 @@ onShow(async () => {
|
||||
font-weight: 600;
|
||||
font-family: 'DIN Alternate', sans-serif;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 40rpx;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 32rpx;
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: #9CA3AF;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.empty-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
background-color: #ECFDF5;
|
||||
padding: 20rpx 40rpx;
|
||||
border-radius: 100rpx;
|
||||
border: 2rpx solid #10B981;
|
||||
}
|
||||
|
||||
.empty-btn:active {
|
||||
background-color: #D1FAE5;
|
||||
}
|
||||
|
||||
.empty-btn-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #064E3B;
|
||||
}
|
||||
</style>
|
||||
@ -80,6 +80,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, getCurrentInstance } from 'vue';
|
||||
import { onPullDownRefresh } from '@dcloudio/uni-app';
|
||||
import type { UserInfoResponse, UserStatsResponse, ApiResponse } from '@/types';
|
||||
|
||||
// 加载状态
|
||||
@ -151,6 +152,15 @@ onMounted(async () => {
|
||||
]);
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
// 下拉刷新
|
||||
onPullDownRefresh(async () => {
|
||||
await Promise.all([
|
||||
fetchUserInfo(),
|
||||
fetchUserStats()
|
||||
]);
|
||||
uni.stopPullDownRefresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -31,6 +31,19 @@
|
||||
<view class="skeleton-text skeleton-btn"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else-if="strategies.length === 0" class="empty-state">
|
||||
<view class="empty-icon">
|
||||
<uni-icons type="bars" size="64" color="#D1D5DB"></uni-icons>
|
||||
</view>
|
||||
<text class="empty-title">暂无策略</text>
|
||||
<text class="empty-desc">点击右上角"+"创建你的第一个策略</text>
|
||||
<view class="empty-btn" @click="goToAdd">
|
||||
<uni-icons type="plus" size="16" color="#064E3B"></uni-icons>
|
||||
<text class="empty-btn-text">创建策略</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 真实内容 -->
|
||||
<view
|
||||
@ -78,7 +91,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, getCurrentInstance } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { onShow, onPullDownRefresh } from '@dcloudio/uni-app';
|
||||
import type { StrategyListItemDto, ApiResponse } from '@/types';
|
||||
|
||||
interface StrategyCardItem {
|
||||
@ -135,6 +148,12 @@ onShow(async () => {
|
||||
isFetching = false;
|
||||
});
|
||||
|
||||
// 下拉刷新
|
||||
onPullDownRefresh(async () => {
|
||||
await fetchStrategies();
|
||||
uni.stopPullDownRefresh();
|
||||
});
|
||||
|
||||
const goToAdd = (): void => {
|
||||
uni.navigateTo({ url: '/pages/strategies/edit/edit' });
|
||||
};
|
||||
@ -327,4 +346,54 @@ const handleAction = (item: StrategyCardItem): void => {
|
||||
.gap-3 { gap: 24rpx; }
|
||||
.gap-2 { gap: 16rpx; }
|
||||
.mb-4 { margin-bottom: 32rpx; }
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 40rpx;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 32rpx;
|
||||
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.06);
|
||||
border: 1rpx solid #E5E7EB;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: #9CA3AF;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.empty-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
background-color: #ECFDF5;
|
||||
padding: 20rpx 40rpx;
|
||||
border-radius: 100rpx;
|
||||
border: 2rpx solid #10B981;
|
||||
}
|
||||
|
||||
.empty-btn:active {
|
||||
background-color: #D1FAE5;
|
||||
}
|
||||
|
||||
.empty-btn-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #064E3B;
|
||||
}
|
||||
</style>
|
||||
|
||||
8
todo.md
8
todo.md
@ -47,10 +47,10 @@
|
||||
- [x] config.vue: 替换 4 处 u-input 为原生 input
|
||||
- [x] detail.vue: 替换 1 处 u-input 为原生 input
|
||||
|
||||
### P3 - 功能增强
|
||||
- [ ] 下拉刷新
|
||||
- [ ] 上拉加载更多
|
||||
- [ ] 空状态优化
|
||||
### P3 - 功能增强 ✅ 完成
|
||||
- [x] 下拉刷新(index / strategies / me / detail)
|
||||
- [x] 上拉加载(detail 交易记录分页)
|
||||
- [x] 空状态优化(index / strategies / detail)
|
||||
|
||||
### P4 - TypeScript 迁移 🚀 进行中
|
||||
详细计划见 `TYPESCRIPT_MIGRATION.md`
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
"lib": ["ES2019", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"ignoreDeprecations": "6.0",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user