feat: 套餐加油包变子
This commit is contained in:
286
components/PackageHistoryNode.vue
Normal file
286
components/PackageHistoryNode.vue
Normal 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>
|
||||
Reference in New Issue
Block a user