feat: 套餐加油包变子

This commit is contained in:
luo
2026-09-08 09:56:16 +08:00
parent 484a807dce
commit 8b6ce505b5
7 changed files with 572 additions and 206 deletions

View File

@@ -0,0 +1,286 @@
<template>
<view class="card package-node" :class="{ 'package-node-child': depth > 0, 'relationship-exception': isMasterMissing }">
<view class="package-header flex-row-sb">
<view class="package-title-wrap">
<view class="package-name">{{ node.package_name }}</view>
<text v-if="packageTypeName" class="package-type">{{ packageTypeName }}</text>
</view>
<view class="tag-apple" :class="getStatusClass(node.status)">{{ node.status_name }}</view>
</view>
<view v-if="isMasterMissing" class="relationship-status">
{{ node.relationship_status_name || node.relationship_status }}
</view>
<view v-if="hasMasterUsageId" class="relationship-meta">
关联主套餐使用记录{{ node.master_usage_id }}
</view>
<view class="package-info">
<view class="info-row flex-row-sb">
<view class="info-label">购买时间</view>
<view class="info-value">{{ formatDate(node.created_at) }}</view>
</view>
<view class="info-row flex-row-sb">
<view class="info-label">激活时间</view>
<view class="info-value">{{ formatDate(node.activated_at) }}</view>
</view>
<view class="info-row flex-row-sb">
<view class="info-label">到期时间</view>
<view class="info-value">{{ formatDate(node.expires_at) }}</view>
</view>
</view>
<view class="divider"></view>
<view class="flow-info">
<view class="flow-title">流量信息</view>
<view class="flow-stats">
<view class="flow-item">
<view class="flow-label">已使用</view>
<view class="flow-value">{{ getUsedFlow(node) }}</view>
</view>
<view class="flow-item">
<view class="flow-label">总流量</view>
<view class="flow-value">{{ getTotalFlow(node) }}</view>
</view>
<view class="flow-item">
<view class="flow-label">剩余</view>
<view class="flow-value">{{ getRemainFlow(node) }}</view>
</view>
</view>
<view class="progress-section">
<view class="progress-apple">
<view class="progress-fill" :style="{ width: getUsagePercent(node) }"></view>
</view>
</view>
</view>
<view
v-if="hasChildren"
class="expand-control"
role="button"
tabindex="0"
:aria-expanded="String(expanded)"
@tap.stop="toggleExpanded"
>
<text>{{ expanded ? '收起关联加油包' : '展开关联加油包' }}</text>
<text class="expand-count">{{ children.length }}</text>
</view>
<view v-if="expanded && hasChildren" class="child-list">
<PackageHistoryNode
v-for="(child, index) in children"
:key="getNodeKey(child, index)"
:node="child"
:depth="depth + 1"
/>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue';
const props = defineProps({
node: {
type: Object,
default: () => ({})
},
depth: {
type: Number,
default: 0
}
});
const children = computed(() => Array.isArray(props.node.children) ? props.node.children : []);
const hasChildren = computed(() => children.value.length > 0);
const isMasterMissing = computed(() => props.node.relationship_status === 'master_missing');
const hasMasterUsageId = computed(() => (
props.node.master_usage_id !== null && props.node.master_usage_id !== undefined
));
const packageTypeName = computed(() => ({
formal: '正式套餐',
addon: '加油包'
}[props.node.package_type] || props.node.package_type || ''));
const expanded = ref(props.node.expand_by_default === true);
const getStatusClass = (status) => ({
0: 'tag-warning',
1: 'tag-success',
2: 'tag-primary',
3: 'tag-secondary',
4: 'tag-danger'
}[Number(status)] || '');
const toNumber = (value) => {
const number = Number(value);
return Number.isFinite(number) ? number : 0;
};
const formatMB = (value) => {
const mb = toNumber(value);
if (mb >= 1024) {
return `${(mb / 1024).toFixed(2)} GB`;
}
return `${mb.toFixed(2)} MB`;
};
const getUsedAmount = (item) => item.enable_virtual_data
? toNumber(item.virtual_used_mb)
: toNumber(item.real_used_mb);
const getUsedFlow = (item) => formatMB(getUsedAmount(item));
const getTotalFlow = (item) => formatMB(item.real_total_mb);
const getRemainFlow = (item) => formatMB(Math.max(toNumber(item.real_total_mb) - getUsedAmount(item), 0));
const getUsagePercent = (item) => {
const total = toNumber(item.real_total_mb);
if (!total) return '0%';
return `${Math.min((getUsedAmount(item) / total) * 100, 100).toFixed(2)}%`;
};
const formatDate = (value) => value ? String(value).replace('T', ' ').slice(0, 19) : '-';
const getNodeKey = (item, index) => item.package_usage_id ?? `${props.depth}-${index}`;
const toggleExpanded = () => {
expanded.value = !expanded.value;
};
</script>
<style lang="scss" scoped>
.package-node {
margin-bottom: var(--space-md);
&.package-node-child {
margin: var(--space-sm) 0 0;
border-left: 6rpx solid var(--primary-light);
box-shadow: none;
}
&.relationship-exception {
border-left: 6rpx solid var(--danger);
}
}
.package-header {
align-items: flex-start;
margin-bottom: var(--space-md);
}
.package-title-wrap {
display: flex;
align-items: center;
min-width: 0;
gap: var(--space-xs);
}
.package-name {
font-size: 32rpx;
font-weight: 600;
color: var(--text-primary);
}
.package-type,
.relationship-meta {
font-size: 22rpx;
color: var(--text-tertiary);
}
.relationship-status {
margin-bottom: var(--space-sm);
padding: 12rpx 16rpx;
border-radius: var(--radius-small);
background: var(--danger-light, #fff2f0);
color: var(--danger);
font-size: 24rpx;
}
.relationship-meta {
margin-bottom: var(--space-sm);
}
.package-info .info-row {
padding: var(--space-xs) 0;
}
.info-label {
font-size: 24rpx;
color: var(--text-tertiary);
}
.info-value {
font-size: 24rpx;
color: var(--text-primary);
}
.divider {
height: 1rpx;
margin: var(--space-md) 0;
background: var(--gray-200);
}
.flow-title {
margin-bottom: var(--space-sm);
font-size: 26rpx;
font-weight: 600;
color: var(--text-primary);
}
.flow-stats {
display: flex;
justify-content: space-between;
margin-bottom: var(--space-sm);
}
.flow-item {
text-align: center;
}
.flow-label {
margin-bottom: 4rpx;
font-size: 20rpx;
color: var(--text-tertiary);
}
.flow-value {
font-size: 26rpx;
font-weight: 600;
color: var(--text-primary);
}
.progress-apple {
overflow: hidden;
width: 100%;
height: 8rpx;
border-radius: var(--radius-small);
background: var(--gray-200);
}
.progress-fill {
height: 100%;
border-radius: var(--radius-small);
background: linear-gradient(90deg, var(--primary), var(--primary-light));
}
.expand-control {
display: inline-flex;
margin-top: var(--space-md);
padding: 12rpx 16rpx;
border-radius: var(--radius-small);
background: var(--primary-light);
color: var(--primary);
font-size: 24rpx;
}
.expand-count {
margin-left: 4rpx;
}
.child-list {
margin-top: var(--space-sm);
}
.tag-secondary {
background: var(--gray-200) !important;
color: var(--gray-600) !important;
}
</style>