基于移动优先原则优化布局、排版、颜色和间距,提升设计感和专业度。 - 重构全局设计系统:颜色、阴影、间距、圆角、排版 - 优化 UserInfoCard:更大的头像和状态徽章设计 - 优化 DeviceStatusCard:信息层级和指标网格 - 优化 TrafficCard:进度条和数字排版 - 优化 FunctionCard:4列布局和触摸反馈 - 优化 WifiCard:列表设计和信息层次 - 优化 FloatingButton:去除渐变,纯色设计 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
252 lines
5.7 KiB
Vue
252 lines
5.7 KiB
Vue
<template>
|
||
<view class="traffic-card">
|
||
<view class="card-header">
|
||
<text class="text-heading">本月流量</text>
|
||
<view class="usage-indicator">
|
||
<text class="usage-value">{{ usedFlowPercent }}</text>
|
||
<text class="usage-unit">%</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="progress-section">
|
||
<view class="progress-track">
|
||
<view class="progress-fill" :style="{ width: usedFlowPercent + '%' }"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="traffic-stats">
|
||
<view class="stat-item">
|
||
<text class="stat-label">已使用</text>
|
||
<view class="stat-value-wrap">
|
||
<text class="stat-value">{{ usedValue }}</text>
|
||
<text class="stat-unit">{{ usedUnit }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-divider"></view>
|
||
<view class="stat-item">
|
||
<text class="stat-label">总流量</text>
|
||
<view class="stat-value-wrap">
|
||
<text class="stat-value">{{ totalValue }}</text>
|
||
<text class="stat-unit">{{ totalUnit }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-divider"></view>
|
||
<view class="stat-item">
|
||
<text class="stat-label">剩余</text>
|
||
<view class="stat-value-wrap">
|
||
<text class="stat-value remain">{{ remainValue }}</text>
|
||
<text class="stat-unit">{{ remainUnit }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch } from 'vue';
|
||
import { assetApi } from '@/api/index.js';
|
||
import { useUserStore } from '@/store/index.js';
|
||
|
||
const props = defineProps({
|
||
identifier: { type: String, default: '' }
|
||
});
|
||
|
||
const emit = defineEmits(['packageLoaded']);
|
||
|
||
const userStore = useUserStore();
|
||
|
||
// 流量数据
|
||
const usedMB = ref(0);
|
||
const totalMB = ref(0);
|
||
const remainMB = ref(0);
|
||
|
||
// 格式化流量数据(MB 转 GB)
|
||
const formatTraffic = (mb) => {
|
||
if (!mb || mb === 0) {
|
||
return { value: '0.00', unit: 'MB' };
|
||
}
|
||
|
||
if (mb >= 1024) {
|
||
return {
|
||
value: (mb / 1024).toFixed(2),
|
||
unit: 'GB'
|
||
};
|
||
}
|
||
|
||
return {
|
||
value: mb.toFixed(2),
|
||
unit: 'MB'
|
||
};
|
||
};
|
||
|
||
// 已使用流量
|
||
const usedValue = computed(() => formatTraffic(usedMB.value).value);
|
||
const usedUnit = computed(() => formatTraffic(usedMB.value).unit);
|
||
|
||
// 总流量
|
||
const totalValue = computed(() => formatTraffic(totalMB.value).value);
|
||
const totalUnit = computed(() => formatTraffic(totalMB.value).unit);
|
||
|
||
// 剩余流量
|
||
const remainValue = computed(() => formatTraffic(remainMB.value).value);
|
||
const remainUnit = computed(() => formatTraffic(remainMB.value).unit);
|
||
|
||
// 使用百分比
|
||
const usedFlowPercent = computed(() => {
|
||
if (totalMB.value && usedMB.value) {
|
||
const percent = (usedMB.value / totalMB.value * 100);
|
||
return Math.min(percent, 100).toFixed(1);
|
||
}
|
||
return '0.0';
|
||
});
|
||
|
||
// 加载套餐流量数据
|
||
const loadPackageData = async () => {
|
||
const identifier = props.identifier || userStore.state.identifier;
|
||
if (!identifier) return;
|
||
|
||
try {
|
||
const data = await assetApi.getPackageHistory(identifier, 1, 10, { status: 1 });
|
||
|
||
if (data.items && data.items.length > 0) {
|
||
// 只有一个生效中的套餐
|
||
const activePackage = data.items[0];
|
||
|
||
if (activePackage.enable_virtual_data) {
|
||
// 启用虚流量
|
||
usedMB.value = activePackage.virtual_used_mb || 0;
|
||
totalMB.value = activePackage.virtual_limit_mb || 0;
|
||
remainMB.value = activePackage.virtual_remain_mb || 0;
|
||
} else {
|
||
// 未启用虚流量,使用真流量
|
||
usedMB.value = activePackage.data_usage_mb || 0;
|
||
totalMB.value = activePackage.data_limit_mb || 0;
|
||
remainMB.value = totalMB.value - usedMB.value;
|
||
}
|
||
|
||
// 将套餐信息(包括到期时间)传递给父组件
|
||
emit('packageLoaded', activePackage);
|
||
} else {
|
||
// 没有生效中的套餐
|
||
usedMB.value = 0;
|
||
totalMB.value = 0;
|
||
remainMB.value = 0;
|
||
emit('packageLoaded', null);
|
||
}
|
||
} catch (e) {
|
||
console.error('加载套餐流量数据失败', e);
|
||
}
|
||
};
|
||
|
||
// 监听 identifier 变化
|
||
watch(() => props.identifier || userStore.state.identifier, (newVal) => {
|
||
if (newVal) {
|
||
loadPackageData();
|
||
}
|
||
}, { immediate: true });
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.traffic-card {
|
||
background: var(--bg-surface);
|
||
border-radius: var(--radius-xl);
|
||
padding: var(--space-5);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: var(--space-5);
|
||
}
|
||
|
||
.usage-indicator {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 2rpx;
|
||
}
|
||
|
||
.usage-value {
|
||
font-size: 48rpx;
|
||
font-weight: 700;
|
||
color: var(--primary);
|
||
letter-spacing: -0.02em;
|
||
line-height: 1;
|
||
}
|
||
|
||
.usage-unit {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: var(--text-tertiary);
|
||
}
|
||
|
||
.progress-section {
|
||
margin-bottom: var(--space-5);
|
||
}
|
||
|
||
.progress-track {
|
||
height: 14rpx;
|
||
background: var(--gray-100);
|
||
border-radius: var(--radius-full);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: var(--primary);
|
||
border-radius: var(--radius-full);
|
||
transition: width 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
||
}
|
||
|
||
.traffic-stats {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: var(--space-4);
|
||
background: var(--gray-50);
|
||
border-radius: var(--radius-lg);
|
||
}
|
||
|
||
.stat-item {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: var(--space-1);
|
||
}
|
||
|
||
.stat-divider {
|
||
width: 1rpx;
|
||
height: 60rpx;
|
||
background: var(--gray-200);
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 24rpx;
|
||
color: var(--text-tertiary);
|
||
}
|
||
|
||
.stat-value-wrap {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 4rpx;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: var(--text-primary);
|
||
line-height: 1.1;
|
||
|
||
&.remain {
|
||
color: var(--success);
|
||
}
|
||
}
|
||
|
||
.stat-unit {
|
||
font-size: 22rpx;
|
||
font-weight: 500;
|
||
color: var(--text-tertiary);
|
||
}
|
||
</style> |