Files
device-voice-h5/components/PackageHistoryNode.vue
2026-09-08 16:12:45 +08:00

287 lines
6.9 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>
<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>