AI-accounting-soft-uniApp/pages/gold-price/gold-price.vue

727 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<scroll-view scroll-y="true" class="page-scroll">
<view class="container">
<view class="header">
<text class="title">黄金实时价格</text>
<text class="subtitle">黄金9999 (1053)</text>
</view>
<!-- 当前价格卡片 -->
<view class="price-card" v-if="currentPrice">
<view class="price-main">
<view class="price-main-left">
<text class="price-label">当前价格</text>
<text class="price-value">¥{{ formatPrice(currentPrice.price) }}</text>
</view>
<view class="price-main-right">
<view class="refresh-btn" @click="handleRefreshPrice" :class="{ loading: refreshing }">
<text>{{ refreshing ? '查询中...' : '立马查询' }}</text>
</view>
</view>
</view>
<view class="price-change" :class="priceChangeClass">
<text class="change-value">{{ priceChangeText }}</text>
<text class="change-percent">{{ priceChangePercentText }}</text>
</view>
<view class="price-details">
<view class="detail-item">
<text class="detail-label">最高</text>
<text class="detail-value">¥{{ formatPrice(currentPrice.highPrice) }}</text>
</view>
<view class="detail-item">
<text class="detail-label">最低</text>
<text class="detail-value">¥{{ formatPrice(currentPrice.lowPrice) }}</text>
</view>
<view class="detail-item">
<text class="detail-label">开盘</text>
<text class="detail-value">¥{{ formatPrice(currentPrice.openPrice) }}</text>
</view>
<view class="detail-item">
<text class="detail-label">昨收</text>
<text class="detail-value">¥{{ formatPrice(currentPrice.yesterdayClose) }}</text>
</view>
</view>
<view class="update-time">
<text>更新时间: {{ currentPrice.updateTime }}</text>
</view>
</view>
<!-- 加载状态 -->
<view class="loading-card" v-if="loading">
<text class="loading-text">加载中...</text>
</view>
<!-- 无数据状态 -->
<view class="error-card" v-if="!loading && !error && !currentPrice">
<text class="error-text">暂无黄金价格数据,请稍后再试</text>
</view>
<!-- 错误状态 -->
<view class="error-card" v-if="error && !loading">
<text class="error-text">{{ error }}</text>
<view class="retry-btn" @click="loadCurrentPrice">
<text>重试</text>
</view>
</view>
<!-- 日期切换区域 -->
<view class="date-switch-section" v-if="currentDate">
<view class="date-switch-container">
<view class="date-btn" @click="goToPreviousDay" :class="{ disabled: isFirstDay }">
<text>前一天</text>
</view>
<view class="date-display">
<text class="date-text">{{ currentDate }}</text>
</view>
<view class="date-btn" @click="goToNextDay" :class="{ disabled: isToday }">
<text>后一天</text>
</view>
</view>
</view>
<!-- 历史价格图表 -->
<view class="chart-section" v-if="dailyPrices.length > 0">
<text class="section-title">{{ currentDate }} 价格走势</text>
<view class="chart-container">
<canvas canvas-id="priceChart" id="priceChart" type="2d" class="price-chart"></canvas>
</view>
</view>
<!-- 无数据提示 -->
<view class="error-card" v-if="!loading && dailyPrices.length === 0 && currentDate">
<text class="error-text">该日期暂无数据</text>
</view>
<!-- 历史价格列表 -->
<view class="history-section" v-if="dailyPrices.length > 0">
<text class="section-title">{{ currentDate }} 价格记录</text>
<view class="history-list">
<view class="history-item" v-for="(item, index) in dailyPrices" :key="index">
<text class="history-date">{{ item.updateTime }}</text>
<text class="history-price">¥{{ formatPrice(item.price) }}</text>
<text class="history-change" :class="getChangeClass(item.priceChange)">
{{ formatChange(item.priceChange) }}
</text>
</view>
</view>
</view>
<!-- 底部预留空白区域避免历史记录被底部导航遮挡 -->
<view class="bottom-spacer"></view>
</view>
</scroll-view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { getCurrentGoldPrice, getGoldPricesByDate, getLatestGoldPriceDate, refreshGoldPrice } from '../../api/gold-price';
const currentPrice = ref(null);
const dailyPrices = ref([]);
const currentDate = ref('');
const loading = ref(false);
const error = ref('');
const refreshing = ref(false);
const priceChangeClass = computed(() => {
if (!currentPrice.value || !currentPrice.value.priceChange) return '';
return currentPrice.value.priceChange >= 0 ? 'up' : 'down';
});
const priceChangeText = computed(() => {
if (!currentPrice.value || currentPrice.value.priceChange === null) return '--';
const change = currentPrice.value.priceChange;
return change >= 0 ? `+${formatPrice(change)}` : formatPrice(change);
});
const priceChangePercentText = computed(() => {
if (!currentPrice.value || currentPrice.value.priceChangePercent === null) return '--';
const percent = currentPrice.value.priceChangePercent;
return percent >= 0 ? `+${percent.toFixed(2)}%` : `${percent.toFixed(2)}%`;
});
const isToday = computed(() => {
if (!currentDate.value) return false;
const today = formatDate(new Date());
return currentDate.value === today;
});
const isFirstDay = computed(() => {
// 简单判断:如果当前日期是今天,则不能往前
// 实际应该查询数据库是否有更早的数据,这里简化处理
return false;
});
const formatPrice = (price) => {
if (price === null || price === undefined) return '--';
return Number(price).toFixed(2);
};
const formatChange = (change) => {
if (change === null || change === undefined) return '--';
return change >= 0 ? `+${Number(change).toFixed(2)}` : Number(change).toFixed(2);
};
const getChangeClass = (change) => {
if (change === null || change === undefined) return '';
return change >= 0 ? 'up' : 'down';
};
const 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}`;
};
const formatTime = (dateTimeStr) => {
if (!dateTimeStr) return '';
// dateTimeStr 格式: "2024-01-15 10:15:00"
return dateTimeStr;
};
const loadCurrentPrice = async () => {
loading.value = true;
error.value = '';
try {
const res = await getCurrentGoldPrice();
if (res && res.code === 200) {
if (res.data) {
currentPrice.value = res.data;
} else {
currentPrice.value = null;
}
} else {
error.value = res?.message || '获取价格失败';
}
} catch (e) {
console.error('获取当前价格失败', e);
error.value = '网络请求失败,请稍后重试';
} finally {
loading.value = false;
}
};
const handleRefreshPrice = async () => {
if (refreshing.value) return;
refreshing.value = true;
try {
const res = await refreshGoldPrice('1053');
if (res && res.code === 200) {
if (res.data) {
currentPrice.value = res.data;
uni.showToast({
title: '查询成功',
icon: 'success'
});
// 如果当前查看的是今天,刷新当天的价格列表
const today = formatDate(new Date());
if (currentDate.value === today) {
await loadDailyPrices(today);
}
} else {
uni.showToast({
title: '查询失败',
icon: 'none'
});
}
} else {
uni.showToast({
title: res?.message || '查询失败',
icon: 'none'
});
}
} catch (e) {
console.error('立即查询失败', e);
uni.showToast({
title: '查询失败,请稍后重试',
icon: 'none'
});
} finally {
refreshing.value = false;
}
};
const loadDailyPrices = async (date) => {
try {
const res = await getGoldPricesByDate('1053', date);
if (res && res.code === 200) {
dailyPrices.value = res.data || [];
if (dailyPrices.value.length > 0) {
// 确保数据按时间排序
dailyPrices.value.sort((a, b) => {
return new Date(a.updateTime) - new Date(b.updateTime);
});
// 延迟绘制图表,确保 canvas 已渲染
setTimeout(() => {
drawChart();
}, 100);
}
}
} catch (e) {
console.error('获取当天价格失败', e);
}
};
const goToPreviousDay = () => {
if (isFirstDay.value) return;
const date = new Date(currentDate.value);
date.setDate(date.getDate() - 1);
currentDate.value = formatDate(date);
loadDailyPrices(currentDate.value);
};
const goToNextDay = () => {
if (isToday.value) return;
const date = new Date(currentDate.value);
date.setDate(date.getDate() + 1);
const newDate = formatDate(date);
const today = formatDate(new Date());
if (newDate > today) return;
currentDate.value = newDate;
loadDailyPrices(currentDate.value);
};
const drawChart = () => {
const ctx = uni.createCanvasContext('priceChart');
const data = [...dailyPrices.value];
if (data.length === 0) return;
const width = 320;
const height = 180;
const padding = 40;
const chartWidth = width - padding * 2;
const chartHeight = height - padding * 2;
const prices = data.map((item) => Number(item.price));
const minPrice = Math.min(...prices) * 0.998;
const maxPrice = Math.max(...prices) * 1.002;
const priceRange = maxPrice - minPrice;
// 绘制背景
ctx.setFillStyle('#FFF8E1');
ctx.fillRect(0, 0, width, height);
// 绘制网格线
ctx.setStrokeStyle('#E0E0E0');
ctx.setLineWidth(0.5);
for (let i = 0; i <= 4; i++) {
const y = padding + (chartHeight / 4) * i;
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(width - padding, y);
ctx.stroke();
}
// 绘制价格线
ctx.setStrokeStyle('#FFB300');
ctx.setLineWidth(2);
ctx.beginPath();
data.forEach((item, index) => {
const x = padding + (chartWidth / (data.length - 1 || 1)) * index;
const y = padding + chartHeight - ((Number(item.price) - minPrice) / priceRange) * chartHeight;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
// 绘制数据点
ctx.setFillStyle('#FF8F00');
data.forEach((item, index) => {
const x = padding + (chartWidth / (data.length - 1 || 1)) * index;
const y = padding + chartHeight - ((Number(item.price) - minPrice) / priceRange) * chartHeight;
ctx.beginPath();
ctx.arc(x, y, 3, 0, 2 * Math.PI);
ctx.fill();
});
// 绘制Y轴标签
ctx.setFillStyle('#5D4037');
ctx.setFontSize(10);
for (let i = 0; i <= 4; i++) {
const price = maxPrice - (priceRange / 4) * i;
const y = padding + (chartHeight / 4) * i;
ctx.fillText(price.toFixed(1), 5, y + 3);
}
// 绘制X轴标签时间点
ctx.setFillStyle('#5D4037');
ctx.setFontSize(9);
const labelInterval = Math.max(1, Math.floor(data.length / 6)); // 最多显示6个标签
data.forEach((item, index) => {
if (index % labelInterval === 0 || index === data.length - 1) {
const x = padding + (chartWidth / (data.length - 1 || 1)) * index;
// 从 updateTime 提取时间部分HH:mm
const timeStr = item.updateTime ? item.updateTime.split(' ')[1]?.substring(0, 5) : '';
if (timeStr) {
ctx.fillText(timeStr, x - 15, height - 10);
}
}
});
ctx.draw();
};
const initPage = async () => {
// 先加载当前价格
await loadCurrentPrice();
// 获取最近有数据的日期
try {
const res = await getLatestGoldPriceDate('1053');
if (res && res.code === 200 && res.data) {
const latestDate = res.data;
const today = formatDate(new Date());
// 如果最近有数据的日期是今天,则显示今天;否则显示最近有数据的日期
currentDate.value = latestDate === today ? today : latestDate;
} else {
// 如果没有数据,默认显示今天
currentDate.value = formatDate(new Date());
}
} catch (e) {
console.error('获取最近日期失败', e);
// 出错时默认显示今天
currentDate.value = formatDate(new Date());
}
// 加载当前日期的数据
if (currentDate.value) {
await loadDailyPrices(currentDate.value);
}
};
onMounted(() => {
initPage();
});
</script>
<style scoped>
.page-scroll {
height: 100vh;
}
.container {
min-height: 100vh;
background: linear-gradient(135deg, #ffe5cc 0%, #ffd4a8 50%, #ffc08a 100%);
padding: 20rpx;
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
}
.header {
text-align: center;
padding: 30rpx 0;
}
.title {
display: block;
font-size: 44rpx;
font-weight: bold;
color: #5d4037;
text-shadow: 2rpx 2rpx 0 #ffd700;
}
.subtitle {
display: block;
font-size: 26rpx;
color: #8d6e63;
margin-top: 10rpx;
}
.price-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
border: 3rpx solid #c8956e;
box-shadow: 0 8rpx 20rpx rgba(200, 149, 110, 0.15);
}
.price-main {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.price-main-left {
flex: 1;
text-align: center;
}
.price-main-right {
flex: 0 0 auto;
margin-left: 20rpx;
}
.price-label {
display: block;
font-size: 26rpx;
color: #8d6e63;
margin-bottom: 10rpx;
}
.price-value {
display: block;
font-size: 60rpx;
font-weight: bold;
color: #5d4037;
}
.refresh-btn {
padding: 12rpx 24rpx;
background: linear-gradient(135deg, #c8956e, #ffd700);
border-radius: 20rpx;
border: 2rpx solid #5d4037;
text-align: center;
min-width: 120rpx;
}
.refresh-btn text {
font-size: 24rpx;
color: #5d4037;
font-weight: 600;
}
.refresh-btn.loading {
opacity: 0.7;
background: linear-gradient(135deg, #bdbdbd, #e0e0e0);
}
.refresh-btn.loading text {
color: #757575;
}
.price-change {
display: flex;
justify-content: center;
gap: 20rpx;
margin-bottom: 30rpx;
}
.price-change.up .change-value,
.price-change.up .change-percent {
color: #fa3534;
}
.price-change.down .change-value,
.price-change.down .change-percent {
color: #19be6b;
}
.change-value,
.change-percent {
font-size: 32rpx;
font-weight: 600;
}
.price-details {
display: flex;
flex-wrap: wrap;
gap: 15rpx;
}
.detail-item {
flex: 1;
min-width: 45%;
background: linear-gradient(135deg, rgba(200, 149, 110, 0.1), rgba(255, 215, 0, 0.1));
padding: 15rpx;
border-radius: 10rpx;
text-align: center;
}
.detail-label {
display: block;
font-size: 22rpx;
color: #8d6e63;
margin-bottom: 5rpx;
}
.detail-value {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #5d4037;
}
.update-time {
text-align: center;
margin-top: 20rpx;
font-size: 22rpx;
color: #bcaaa4;
}
.loading-card,
.error-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 20rpx;
padding: 60rpx 30rpx;
margin-bottom: 20rpx;
border: 3rpx solid #c8956e;
text-align: center;
}
.loading-text {
font-size: 28rpx;
color: #8d6e63;
}
.error-text {
display: block;
font-size: 28rpx;
color: #fa3534;
margin-bottom: 20rpx;
}
.retry-btn {
display: inline-block;
padding: 15rpx 40rpx;
background: linear-gradient(135deg, #c8956e, #ffd700);
border-radius: 30rpx;
border: 2rpx solid #5d4037;
}
.retry-btn text {
font-size: 26rpx;
color: #5d4037;
font-weight: 600;
}
.date-switch-section {
background: rgba(255, 255, 255, 0.95);
border-radius: 20rpx;
padding: 20rpx 30rpx;
margin-bottom: 20rpx;
border: 3rpx solid #c8956e;
}
.date-switch-container {
display: flex;
justify-content: space-between;
align-items: center;
}
.date-btn {
flex: 1;
padding: 15rpx 20rpx;
background: linear-gradient(135deg, #c8956e, #ffd700);
border-radius: 15rpx;
border: 2rpx solid #5d4037;
text-align: center;
}
.date-btn text {
font-size: 26rpx;
color: #5d4037;
font-weight: 600;
}
.date-btn.disabled {
background: #e0e0e0;
border-color: #bdbdbd;
opacity: 0.5;
}
.date-btn.disabled text {
color: #9e9e9e;
}
.date-display {
flex: 2;
text-align: center;
padding: 0 20rpx;
}
.date-text {
font-size: 32rpx;
font-weight: 600;
color: #5d4037;
}
.chart-section,
.history-section {
background: rgba(255, 255, 255, 0.95);
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
border: 3rpx solid #c8956e;
}
.section-title {
display: block;
font-size: 30rpx;
font-weight: 600;
color: #5d4037;
margin-bottom: 20rpx;
}
.chart-container {
width: 100%;
height: 360rpx;
display: flex;
justify-content: center;
align-items: center;
}
.price-chart {
width: 640rpx;
height: 360rpx;
}
.history-list {
max-height: 500rpx;
overflow-y: auto;
}
.history-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid rgba(200, 149, 110, 0.2);
}
.history-item:last-child {
border-bottom: none;
}
.history-date {
font-size: 24rpx;
color: #8d6e63;
flex: 1.5;
}
.history-price {
font-size: 28rpx;
font-weight: 600;
color: #5d4037;
flex: 1;
text-align: center;
}
.history-change {
font-size: 26rpx;
font-weight: 600;
flex: 1;
text-align: right;
}
.history-change.up {
color: #fa3534;
}
.history-change.down {
color: #19be6b;
}
.bottom-spacer {
height: 200px;
}
</style>