2025-11-24 16:06:44 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<ListPageLayout
|
|
|
|
|
|
title="订阅管理"
|
|
|
|
|
|
subtitle="管理用户订阅和价格调整"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 单用户模式头部 -->
|
|
|
|
|
|
<template #actions v-if="singleUserMode">
|
2025-12-10 14:17:31 +08:00
|
|
|
|
<div :class="['single-user-header', isMobile ? 'flex-col' : '']">
|
2025-11-24 16:06:44 +08:00
|
|
|
|
<div class="user-info">
|
|
|
|
|
|
<el-icon class="user-icon"><user /></el-icon>
|
|
|
|
|
|
<div class="user-details">
|
2025-12-19 11:46:37 +08:00
|
|
|
|
<div class="company-name">{{ currentUser?.company_name || '未知公司' }}</div>
|
2025-11-24 16:06:44 +08:00
|
|
|
|
<div class="user-phone">{{ currentUser?.phone || '-' }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-10 14:17:31 +08:00
|
|
|
|
<div :class="['user-actions', isMobile ? 'w-full flex-wrap' : '']">
|
|
|
|
|
|
<el-button :size="isMobile ? 'small' : 'small'" @click="showBatchPriceDialog" type="warning">
|
2025-11-24 16:06:44 +08:00
|
|
|
|
<el-icon><edit /></el-icon>
|
2025-12-10 14:17:31 +08:00
|
|
|
|
<span :class="isMobile ? 'hidden sm:inline' : ''">一键改价</span>
|
|
|
|
|
|
<span :class="isMobile ? 'sm:hidden' : 'hidden'">改价</span>
|
2025-11-24 16:06:44 +08:00
|
|
|
|
</el-button>
|
2025-12-10 14:17:31 +08:00
|
|
|
|
<el-button :size="isMobile ? 'small' : 'small'" @click="exitSingleUserMode" type="info">
|
2025-11-24 16:06:44 +08:00
|
|
|
|
<el-icon><close /></el-icon>
|
2025-12-10 14:17:31 +08:00
|
|
|
|
<span :class="isMobile ? 'hidden sm:inline' : ''">取消</span>
|
2025-11-24 16:06:44 +08:00
|
|
|
|
</el-button>
|
2025-12-10 14:17:31 +08:00
|
|
|
|
<el-button :size="isMobile ? 'small' : 'small'" @click="goBackToUsers" type="primary">
|
2025-11-24 16:06:44 +08:00
|
|
|
|
<el-icon><back /></el-icon>
|
2025-12-10 14:17:31 +08:00
|
|
|
|
<span :class="isMobile ? 'hidden sm:inline' : ''">返回用户管理</span>
|
|
|
|
|
|
<span :class="isMobile ? 'sm:hidden' : 'hidden'">返回</span>
|
2025-11-24 16:06:44 +08:00
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #filters>
|
|
|
|
|
|
<FilterSection>
|
|
|
|
|
|
<FilterItem label="搜索订阅">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="filters.keyword"
|
|
|
|
|
|
placeholder="输入产品名称或编号"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
@input="handleSearch"
|
|
|
|
|
|
class="w-full"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</FilterItem>
|
|
|
|
|
|
|
|
|
|
|
|
<FilterItem v-if="!singleUserMode" label="企业名称">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="filters.company_name"
|
|
|
|
|
|
placeholder="输入企业名称"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
@input="handleSearch"
|
|
|
|
|
|
class="w-full"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</FilterItem>
|
|
|
|
|
|
|
|
|
|
|
|
<FilterItem label="产品名称">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="filters.product_name"
|
|
|
|
|
|
placeholder="输入产品名称"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
@input="handleSearch"
|
|
|
|
|
|
class="w-full"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</FilterItem>
|
|
|
|
|
|
|
2025-12-15 11:58:05 +08:00
|
|
|
|
<FilterItem label="订阅时间" class="col-span-1">
|
2025-11-24 16:06:44 +08:00
|
|
|
|
<el-date-picker
|
|
|
|
|
|
v-model="filters.timeRange"
|
|
|
|
|
|
type="datetimerange"
|
|
|
|
|
|
range-separator="至"
|
|
|
|
|
|
start-placeholder="开始时间"
|
|
|
|
|
|
end-placeholder="结束时间"
|
|
|
|
|
|
format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
|
@change="handleTimeRangeChange"
|
|
|
|
|
|
class="w-full"
|
2025-12-15 11:58:05 +08:00
|
|
|
|
:size="isMobile ? 'small' : 'default'"
|
2025-11-24 16:06:44 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</FilterItem>
|
|
|
|
|
|
|
|
|
|
|
|
<template #stats>
|
|
|
|
|
|
共找到 {{ total }} 个订阅
|
|
|
|
|
|
<span v-if="singleUserMode" class="text-blue-600">
|
|
|
|
|
|
(仅显示当前用户)
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #buttons>
|
2025-12-10 14:17:31 +08:00
|
|
|
|
<div :class="['flex gap-2', isMobile ? 'flex-wrap w-full' : '']">
|
|
|
|
|
|
<el-button :size="isMobile ? 'small' : 'default'" @click="resetFilters" :class="isMobile ? 'flex-1' : ''">
|
|
|
|
|
|
重置筛选
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button :size="isMobile ? 'small' : 'default'" type="primary" @click="loadSubscriptions" :class="isMobile ? 'flex-1' : ''">
|
|
|
|
|
|
应用筛选
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
2025-11-24 16:06:44 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</FilterSection>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #table>
|
2025-12-10 14:17:31 +08:00
|
|
|
|
<!-- 加载状态 -->
|
2025-11-24 16:06:44 +08:00
|
|
|
|
<div v-if="loading" class="flex justify-center items-center py-12">
|
|
|
|
|
|
<el-loading size="large" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-10 14:17:31 +08:00
|
|
|
|
<!-- 移动端卡片布局 -->
|
|
|
|
|
|
<div v-else-if="isMobile && subscriptions.length > 0" class="subscription-cards">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="subscription in subscriptions"
|
|
|
|
|
|
:key="subscription.id"
|
|
|
|
|
|
class="subscription-card"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<div class="flex-1">
|
|
|
|
|
|
<div class="flex items-center gap-2 mb-1">
|
|
|
|
|
|
<span class="font-semibold text-base text-blue-600">{{ subscription.product?.name || subscription.product_admin?.name || '未知产品' }}</span>
|
|
|
|
|
|
<el-tag v-if="subscription.product?.is_package || subscription.product_admin?.is_package" type="success" size="small">组合包</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-xs text-gray-500">编号: {{ subscription.product?.code || subscription.product_admin?.code || '-' }}</div>
|
|
|
|
|
|
<div v-if="!singleUserMode" class="text-xs text-gray-500 mt-1">公司: {{ subscription.user?.company_name || '未知公司' }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
|
<div class="card-row">
|
|
|
|
|
|
<span class="card-label">订阅价格</span>
|
|
|
|
|
|
<span class="card-value text-red-600 font-semibold">¥{{ formatPrice(subscription.price) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="(subscription.product?.price || subscription.product_admin?.price) && (subscription.product?.price || subscription.product_admin?.price) !== subscription.price" class="card-row">
|
|
|
|
|
|
<span class="card-label">折扣</span>
|
|
|
|
|
|
<span class="card-value text-blue-600 text-sm">{{ calculateDiscount(subscription.product?.price || subscription.product_admin?.price, subscription.price) }}折</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="card-row">
|
|
|
|
|
|
<span class="card-label">产品原价</span>
|
|
|
|
|
|
<span class="card-value text-gray-700">¥{{ formatPrice(subscription.product?.price || subscription.product_admin?.price) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="card-row">
|
|
|
|
|
|
<span class="card-label">成本价</span>
|
|
|
|
|
|
<span class="card-value text-gray-600">¥{{ formatPrice(subscription.product_admin?.cost_price) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="card-row">
|
|
|
|
|
|
<span class="card-label">订阅时间</span>
|
|
|
|
|
|
<span class="card-value text-sm">{{ formatDate(subscription.created_at) }} {{ formatTime(subscription.created_at) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="card-footer">
|
|
|
|
|
|
<div class="action-buttons">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
@click="handleEditPrice(subscription)"
|
|
|
|
|
|
class="action-btn"
|
|
|
|
|
|
>
|
|
|
|
|
|
调整价格
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
@click="handleViewDetails(subscription)"
|
|
|
|
|
|
class="action-btn"
|
|
|
|
|
|
>
|
|
|
|
|
|
查看详情
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 桌面端表格布局 -->
|
|
|
|
|
|
<div v-else-if="!isMobile" class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
|
|
|
|
|
<div class="table-container">
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
:data="subscriptions"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
:header-cell-style="{
|
|
|
|
|
|
background: '#f8fafc',
|
|
|
|
|
|
color: '#475569',
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
fontSize: '14px'
|
|
|
|
|
|
}"
|
|
|
|
|
|
:cell-style="{
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
color: '#1e293b'
|
|
|
|
|
|
}"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column label="公司名称" min-width="200">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="font-medium text-gray-900">{{ row.user?.company_name || '未知公司' }}</div>
|
|
|
|
|
|
<div class="text-sm text-gray-500">{{ row.user?.phone || '-' }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column label="产品信息" min-width="200">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="font-medium text-gray-900">{{ row.product?.name || row.product_admin?.name || '未知产品' }}</div>
|
|
|
|
|
|
<div class="text-sm text-gray-500">{{ row.product?.code || row.product_admin?.code || '-' }}</div>
|
|
|
|
|
|
<el-tag v-if="row.product?.is_package || row.product_admin?.is_package" type="success" size="small" class="mt-1">组合包</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column prop="price" label="订阅价格" width="150">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="flex flex-col gap-1">
|
|
|
|
|
|
<span class="font-semibold text-red-600">¥{{ formatPrice(row.price) }}</span>
|
|
|
|
|
|
<div v-if="(row.product?.price || row.product_admin?.price) && (row.product?.price || row.product_admin?.price) !== row.price" class="text-xs text-blue-600">
|
|
|
|
|
|
({{ calculateDiscount(row.product?.price || row.product_admin?.price, row.price) }}折)
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column label="产品原价" width="120">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<span class="font-medium text-gray-700">¥{{ formatPrice(row.product?.price || row.product_admin?.price) }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column label="成本价" width="120">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<span class="font-medium text-gray-600">¥{{ formatPrice(row.product_admin?.cost_price) }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column prop="created_at" label="订阅时间" width="160">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="text-sm">
|
|
|
|
|
|
<div class="text-gray-900">{{ formatDate(row.created_at) }}</div>
|
|
|
|
|
|
<div class="text-gray-500">{{ formatTime(row.created_at) }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column label="操作" width="200" fixed="right">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="flex items-center space-x-2">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
@click="handleEditPrice(row)"
|
|
|
|
|
|
>
|
|
|
|
|
|
调整价格
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
@click="handleViewDetails(row)"
|
|
|
|
|
|
>
|
|
|
|
|
|
查看详情
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 空状态 -->
|
|
|
|
|
|
<div v-if="!loading && subscriptions.length === 0" class="text-center py-12">
|
2025-11-24 16:06:44 +08:00
|
|
|
|
<el-empty description="暂无订阅数据">
|
|
|
|
|
|
<el-button type="primary" @click="loadSubscriptions">
|
|
|
|
|
|
重新加载
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</el-empty>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #pagination>
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
v-if="total > 0"
|
|
|
|
|
|
v-model:current-page="currentPage"
|
|
|
|
|
|
v-model:page-size="pageSize"
|
|
|
|
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
|
|
|
|
:total="total"
|
2025-12-10 14:17:31 +08:00
|
|
|
|
:layout="isMobile ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper'"
|
|
|
|
|
|
:small="isMobile"
|
2025-11-24 16:06:44 +08:00
|
|
|
|
@size-change="handleSizeChange"
|
|
|
|
|
|
@current-change="handleCurrentChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #extra>
|
|
|
|
|
|
<!-- 价格调整弹窗 -->
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
|
v-model="priceDialogVisible"
|
|
|
|
|
|
title="调整订阅价格"
|
2025-12-10 14:17:31 +08:00
|
|
|
|
:width="isMobile ? '90%' : '600px'"
|
2025-11-24 16:06:44 +08:00
|
|
|
|
class="price-dialog"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-form
|
|
|
|
|
|
ref="priceFormRef"
|
|
|
|
|
|
:model="priceForm"
|
|
|
|
|
|
:rules="priceRules"
|
|
|
|
|
|
label-width="120px"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-form-item label="产品原价">
|
|
|
|
|
|
<span class="text-lg font-semibold text-gray-700">
|
|
|
|
|
|
¥{{ formatPrice(selectedSubscription?.product?.price || selectedSubscription?.product_admin?.price) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<el-form-item label="成本价">
|
|
|
|
|
|
<span class="text-lg font-semibold text-gray-600">
|
|
|
|
|
|
¥{{ formatPrice(selectedSubscription?.product_admin?.cost_price) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<el-form-item label="当前价格">
|
|
|
|
|
|
<div class="flex flex-col gap-1">
|
|
|
|
|
|
<span class="text-lg font-semibold text-red-600">
|
|
|
|
|
|
¥{{ formatPrice(selectedSubscription?.price) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div class="text-sm text-gray-500 space-y-1">
|
|
|
|
|
|
<div v-if="(selectedSubscription?.product?.price || selectedSubscription?.product_admin?.price) && (selectedSubscription.product?.price || selectedSubscription.product_admin?.price) !== selectedSubscription.price" class="text-blue-600">
|
|
|
|
|
|
当前折扣: {{ calculateDiscount(selectedSubscription.product?.price || selectedSubscription.product_admin?.price, selectedSubscription.price) }}折
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="selectedSubscription?.product_admin?.cost_price" class="text-green-600">
|
|
|
|
|
|
成本价倍数: {{ calculateCostMultiple(selectedSubscription.product_admin.cost_price, selectedSubscription.price) }}倍
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<el-form-item label="调整方式">
|
|
|
|
|
|
<el-radio-group v-model="priceForm.adjustmentType" @change="handleAdjustmentTypeChange">
|
|
|
|
|
|
<el-radio label="price">直接输入价格</el-radio>
|
|
|
|
|
|
<el-radio label="discount">按售价折扣调整</el-radio>
|
|
|
|
|
|
<el-radio
|
|
|
|
|
|
label="cost_multiple"
|
|
|
|
|
|
:disabled="!hasValidCostPrice"
|
|
|
|
|
|
>
|
|
|
|
|
|
按成本价倍数调整
|
|
|
|
|
|
<el-tooltip v-if="!hasValidCostPrice" content="该产品未设置成本价或成本价为0,无法使用此方式" placement="top">
|
|
|
|
|
|
<el-icon class="ml-1"><QuestionFilled /></el-icon>
|
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
|
</el-radio>
|
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
|
<div v-if="!hasValidCostPrice && priceForm.adjustmentType === 'cost_multiple'" class="text-sm text-red-500 mt-1">
|
|
|
|
|
|
该产品未设置成本价或成本价为0,无法使用按成本价倍数调整
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<el-form-item v-if="priceForm.adjustmentType === 'price'" label="新价格" prop="price">
|
|
|
|
|
|
<el-input-number
|
|
|
|
|
|
v-model="priceForm.price"
|
|
|
|
|
|
:precision="2"
|
|
|
|
|
|
:min="0"
|
|
|
|
|
|
:step="0.01"
|
|
|
|
|
|
placeholder="请输入新价格"
|
|
|
|
|
|
class="w-full"
|
|
|
|
|
|
@input="handlePriceInput"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div class="text-sm text-gray-500 mt-1 space-y-1">
|
|
|
|
|
|
<div v-if="selectedSubscription?.product?.price || selectedSubscription?.product_admin?.price" class="text-blue-600">
|
|
|
|
|
|
相当于原价 {{ calculateDiscount(selectedSubscription.product?.price || selectedSubscription.product_admin?.price, priceForm.price) }}折
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="selectedSubscription?.product_admin?.cost_price" class="text-green-600">
|
|
|
|
|
|
成本价倍数: {{ calculateCostMultiple(selectedSubscription.product_admin.cost_price, priceForm.price) }}倍
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<el-form-item v-if="priceForm.adjustmentType === 'discount'" label="折扣比例" prop="discount">
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<el-input-number
|
|
|
|
|
|
v-model="priceForm.discount"
|
|
|
|
|
|
:precision="1"
|
|
|
|
|
|
:min="0.1"
|
|
|
|
|
|
:max="10"
|
|
|
|
|
|
:step="0.1"
|
|
|
|
|
|
placeholder="请输入折扣比例"
|
|
|
|
|
|
class="w-32"
|
|
|
|
|
|
@input="handleDiscountInput"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-gray-500">折</span>
|
|
|
|
|
|
<span class="text-sm text-gray-500">(0.1-10折)</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-sm text-gray-500 mt-1 space-y-1">
|
|
|
|
|
|
<div class="text-blue-600">
|
|
|
|
|
|
计算得出价格: ¥{{ formatPrice(calculatePriceByDiscount(selectedSubscription?.product?.price || selectedSubscription?.product_admin?.price, priceForm.discount)) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="selectedSubscription?.product_admin?.cost_price" class="text-green-600">
|
|
|
|
|
|
成本价倍数: {{ calculateCostMultiple(selectedSubscription.product_admin.cost_price, calculatePriceByDiscount(selectedSubscription?.product?.price || selectedSubscription?.product_admin?.price, priceForm.discount)) }}倍
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<el-form-item v-if="priceForm.adjustmentType === 'cost_multiple'" label="成本价倍数" prop="cost_multiple">
|
|
|
|
|
|
<div v-if="!hasValidCostPrice" class="text-red-500 text-sm mb-2">
|
|
|
|
|
|
该产品未设置成本价或成本价为0,无法使用按成本价倍数调整
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="flex items-center gap-2">
|
|
|
|
|
|
<el-input-number
|
|
|
|
|
|
v-model="priceForm.cost_multiple"
|
|
|
|
|
|
:precision="2"
|
|
|
|
|
|
:min="0.1"
|
|
|
|
|
|
:step="0.1"
|
|
|
|
|
|
placeholder="请输入倍数"
|
|
|
|
|
|
class="w-32"
|
|
|
|
|
|
@input="handleCostMultipleInput"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-gray-500">倍</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="hasValidCostPrice" class="text-sm text-gray-500 mt-1 space-y-1">
|
|
|
|
|
|
<div class="text-green-600">
|
|
|
|
|
|
计算得出价格: ¥{{ formatPrice(calculatePriceByCostMultiple(selectedSubscription.product_admin.cost_price, priceForm.cost_multiple)) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="selectedSubscription?.product?.price || selectedSubscription?.product_admin?.price" class="text-blue-600">
|
|
|
|
|
|
相当于原价 {{ calculateDiscount(selectedSubscription.product?.price || selectedSubscription.product_admin?.price, calculatePriceByCostMultiple(selectedSubscription?.product_admin?.cost_price, priceForm.cost_multiple)) }}折
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
2025-12-10 14:17:31 +08:00
|
|
|
|
<div :class="['flex gap-3', isMobile ? 'flex-col' : 'justify-end']">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
:class="isMobile ? 'w-full' : ''"
|
|
|
|
|
|
@click="priceDialogVisible = false"
|
|
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
:class="isMobile ? 'w-full' : ''"
|
|
|
|
|
|
@click="handleUpdatePrice"
|
|
|
|
|
|
:loading="updatingPrice"
|
|
|
|
|
|
>
|
2025-11-24 16:06:44 +08:00
|
|
|
|
确认调整
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 一键改价弹窗 -->
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
|
v-model="batchPriceDialogVisible"
|
|
|
|
|
|
title="一键改价"
|
2025-12-10 14:17:31 +08:00
|
|
|
|
:width="isMobile ? '90%' : '500px'"
|
2025-11-24 16:06:44 +08:00
|
|
|
|
class="batch-price-dialog"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-form
|
|
|
|
|
|
ref="batchPriceFormRef"
|
|
|
|
|
|
:model="batchPriceForm"
|
|
|
|
|
|
:rules="batchPriceRules"
|
|
|
|
|
|
label-width="120px"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-form-item label="调整方式">
|
|
|
|
|
|
<el-radio-group v-model="batchPriceForm.adjustmentType">
|
|
|
|
|
|
<el-radio label="discount">按售价折扣调整</el-radio>
|
|
|
|
|
|
<el-radio label="cost_multiple">按成本价倍数调整</el-radio>
|
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<el-form-item v-if="batchPriceForm.adjustmentType === 'discount'" label="折扣比例" prop="discount">
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<el-input-number
|
|
|
|
|
|
v-model="batchPriceForm.discount"
|
|
|
|
|
|
:precision="1"
|
|
|
|
|
|
:min="0.1"
|
|
|
|
|
|
:max="10"
|
|
|
|
|
|
:step="0.1"
|
|
|
|
|
|
placeholder="请输入折扣比例"
|
|
|
|
|
|
class="w-32"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-gray-500">折</span>
|
|
|
|
|
|
<span class="text-sm text-gray-500">(0.1-10折)</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-sm text-gray-500 mt-1">
|
|
|
|
|
|
<span class="text-blue-600">
|
|
|
|
|
|
示例:8.5折 = 原价 × 0.85
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<el-form-item v-if="batchPriceForm.adjustmentType === 'cost_multiple'" label="成本价倍数" prop="cost_multiple">
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<el-input-number
|
|
|
|
|
|
v-model="batchPriceForm.cost_multiple"
|
|
|
|
|
|
:precision="2"
|
|
|
|
|
|
:min="0.1"
|
|
|
|
|
|
:step="0.1"
|
|
|
|
|
|
placeholder="请输入倍数"
|
|
|
|
|
|
class="w-32"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-gray-500">倍</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-sm text-gray-500 mt-1 space-y-1">
|
|
|
|
|
|
<div class="text-green-600">
|
|
|
|
|
|
示例:1.5倍 = 成本价 × 1.5
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-orange-600 flex items-start gap-1">
|
|
|
|
|
|
<el-icon class="mt-0.5"><Warning /></el-icon>
|
|
|
|
|
|
<span>注意:此方式只会修改已设置成本价且成本价大于0的产品,未设置成本价或成本价为0的产品将被跳过</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<el-form-item label="改价范围" prop="scope">
|
|
|
|
|
|
<el-radio-group v-model="batchPriceForm.scope">
|
|
|
|
|
|
<el-radio label="undiscounted">仅修改未打折的订阅</el-radio>
|
|
|
|
|
|
<el-radio label="all">修改所有订阅(无论之前是否打折)</el-radio>
|
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<el-form-item label="改价说明">
|
|
|
|
|
|
<div class="text-sm text-gray-500">
|
|
|
|
|
|
<span v-if="batchPriceForm.scope === 'undiscounted'">
|
|
|
|
|
|
将影响该用户所有未打折的订阅
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span v-else>
|
|
|
|
|
|
将影响该用户的所有订阅
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
2025-12-10 14:17:31 +08:00
|
|
|
|
<div :class="['flex gap-3', isMobile ? 'flex-col' : 'justify-end']">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
:class="isMobile ? 'w-full' : ''"
|
|
|
|
|
|
@click="batchPriceDialogVisible = false"
|
|
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</el-button>
|
2025-11-24 16:06:44 +08:00
|
|
|
|
<el-button
|
|
|
|
|
|
type="warning"
|
2025-12-10 14:17:31 +08:00
|
|
|
|
:class="isMobile ? 'w-full' : ''"
|
2025-11-24 16:06:44 +08:00
|
|
|
|
@click="handleBatchUpdatePrice"
|
|
|
|
|
|
:loading="updatingBatchPrice"
|
|
|
|
|
|
>
|
|
|
|
|
|
确认一键改价
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</ListPageLayout>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { productAdminApi, userApi } from '@/api'
|
|
|
|
|
|
import FilterItem from '@/components/common/FilterItem.vue'
|
|
|
|
|
|
import FilterSection from '@/components/common/FilterSection.vue'
|
|
|
|
|
|
import ListPageLayout from '@/components/common/ListPageLayout.vue'
|
2025-12-10 14:17:31 +08:00
|
|
|
|
import { useMobileTable } from '@/composables/useMobileTable'
|
2025-11-24 16:06:44 +08:00
|
|
|
|
import { Back, Close, Edit, QuestionFilled, User, Warning } from '@element-plus/icons-vue'
|
|
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
|
|
|
|
|
|
|
|
// 获取路由实例
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
|
2025-12-10 14:17:31 +08:00
|
|
|
|
// 移动端检测
|
|
|
|
|
|
const { isMobile, isTablet } = useMobileTable()
|
|
|
|
|
|
|
2025-11-24 16:06:44 +08:00
|
|
|
|
// 响应式数据
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const subscriptions = ref([])
|
|
|
|
|
|
const total = ref(0)
|
|
|
|
|
|
const currentPage = ref(1)
|
|
|
|
|
|
const pageSize = ref(10)
|
|
|
|
|
|
|
|
|
|
|
|
// 单用户模式相关
|
|
|
|
|
|
const singleUserMode = ref(false)
|
|
|
|
|
|
const currentUser = ref(null)
|
|
|
|
|
|
|
|
|
|
|
|
// 筛选条件
|
|
|
|
|
|
const filters = reactive({
|
|
|
|
|
|
keyword: '',
|
|
|
|
|
|
company_name: '',
|
|
|
|
|
|
product_name: '',
|
|
|
|
|
|
timeRange: []
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 价格调整相关
|
|
|
|
|
|
const priceDialogVisible = ref(false)
|
|
|
|
|
|
const selectedSubscription = ref(null)
|
|
|
|
|
|
const updatingPrice = ref(false)
|
|
|
|
|
|
const priceFormRef = ref(null)
|
|
|
|
|
|
const priceForm = reactive({
|
|
|
|
|
|
price: 0,
|
|
|
|
|
|
discount: 10,
|
|
|
|
|
|
cost_multiple: 1.0,
|
|
|
|
|
|
adjustmentType: 'price' // 'price'、'discount' 或 'cost_multiple'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 一键改价相关
|
|
|
|
|
|
const batchPriceDialogVisible = ref(false)
|
|
|
|
|
|
const updatingBatchPrice = ref(false)
|
|
|
|
|
|
const batchPriceFormRef = ref(null)
|
|
|
|
|
|
const batchPriceForm = reactive({
|
|
|
|
|
|
adjustmentType: 'discount', // 'discount' 或 'cost_multiple'
|
|
|
|
|
|
discount: 10,
|
|
|
|
|
|
cost_multiple: 1.0,
|
|
|
|
|
|
scope: 'undiscounted' // 'undiscounted' 或 'all'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const priceRules = {
|
|
|
|
|
|
price: [
|
|
|
|
|
|
{ required: true, message: '请输入新价格', trigger: 'blur' },
|
|
|
|
|
|
{ type: 'number', min: 0, message: '价格不能小于0', trigger: 'blur' }
|
|
|
|
|
|
],
|
|
|
|
|
|
discount: [
|
|
|
|
|
|
{ required: true, message: '请输入折扣比例', trigger: 'blur' },
|
|
|
|
|
|
{ type: 'number', min: 0.1, max: 10, message: '折扣比例必须在0.1-10之间', trigger: 'blur' }
|
|
|
|
|
|
],
|
|
|
|
|
|
cost_multiple: [
|
|
|
|
|
|
{
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
message: '请输入成本价倍数',
|
|
|
|
|
|
trigger: 'blur',
|
|
|
|
|
|
validator: (rule, value, callback) => {
|
|
|
|
|
|
if (!hasValidCostPrice.value) {
|
|
|
|
|
|
callback(new Error('该产品未设置成本价或成本价为0,无法使用按成本价倍数调整'))
|
|
|
|
|
|
} else if (!value || value <= 0) {
|
|
|
|
|
|
callback(new Error('倍数必须大于0'))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
callback()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const batchPriceRules = {
|
|
|
|
|
|
discount: [
|
|
|
|
|
|
{
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
message: '请输入折扣比例',
|
|
|
|
|
|
trigger: 'blur',
|
|
|
|
|
|
validator: (rule, value, callback) => {
|
|
|
|
|
|
if (batchPriceForm.adjustmentType === 'discount' && (!value || value < 0.1 || value > 10)) {
|
|
|
|
|
|
callback(new Error('折扣比例必须在0.1-10之间'))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
callback()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
cost_multiple: [
|
|
|
|
|
|
{
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
message: '请输入成本价倍数',
|
|
|
|
|
|
trigger: 'blur',
|
|
|
|
|
|
validator: (rule, value, callback) => {
|
|
|
|
|
|
if (batchPriceForm.adjustmentType === 'cost_multiple' && (!value || value <= 0)) {
|
|
|
|
|
|
callback(new Error('倍数必须大于0'))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
callback()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
scope: [
|
|
|
|
|
|
{ required: true, message: '请选择改价范围', trigger: 'change' }
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 搜索防抖
|
|
|
|
|
|
let searchTimer = null
|
|
|
|
|
|
|
|
|
|
|
|
// 计算属性:检查成本价是否有效
|
|
|
|
|
|
const hasValidCostPrice = computed(() => {
|
|
|
|
|
|
const costPrice = selectedSubscription.value?.product_admin?.cost_price
|
|
|
|
|
|
return costPrice !== undefined && costPrice !== null && costPrice > 0
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await checkSingleUserMode()
|
|
|
|
|
|
loadSubscriptions()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否为单用户模式
|
|
|
|
|
|
const checkSingleUserMode = async () => {
|
|
|
|
|
|
const userId = route.query.user_id
|
|
|
|
|
|
if (userId) {
|
|
|
|
|
|
singleUserMode.value = true
|
|
|
|
|
|
await loadUserInfo(userId)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
singleUserMode.value = false
|
|
|
|
|
|
currentUser.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加载用户信息
|
|
|
|
|
|
const loadUserInfo = async (userId) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await userApi.getUserDetail(userId)
|
|
|
|
|
|
const userData = response.data
|
|
|
|
|
|
|
|
|
|
|
|
currentUser.value = {
|
|
|
|
|
|
id: userData.id,
|
|
|
|
|
|
company_name: userData.enterprise_info?.company_name || '未知公司',
|
|
|
|
|
|
phone: userData.phone || '-'
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载用户信息失败:', error)
|
|
|
|
|
|
ElMessage.error('加载用户信息失败')
|
|
|
|
|
|
// 设置默认值
|
|
|
|
|
|
currentUser.value = {
|
|
|
|
|
|
id: userId,
|
|
|
|
|
|
company_name: '加载失败',
|
|
|
|
|
|
phone: '-'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 退出单用户模式
|
|
|
|
|
|
const exitSingleUserMode = () => {
|
|
|
|
|
|
singleUserMode.value = false
|
|
|
|
|
|
currentUser.value = null
|
|
|
|
|
|
// 清除URL参数
|
|
|
|
|
|
router.replace({
|
|
|
|
|
|
name: 'AdminSubscriptions',
|
|
|
|
|
|
query: {}
|
|
|
|
|
|
})
|
|
|
|
|
|
// 重新加载数据
|
|
|
|
|
|
loadSubscriptions()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 返回用户管理
|
|
|
|
|
|
const goBackToUsers = () => {
|
2025-12-19 11:28:59 +08:00
|
|
|
|
const query = { user_id: currentUser.value?.id }
|
|
|
|
|
|
|
|
|
|
|
|
// 如果当前用户有手机号,添加到查询参数
|
|
|
|
|
|
if (currentUser.value?.phone) {
|
|
|
|
|
|
query.phone = currentUser.value.phone
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果当前用户有企业名称,添加到查询参数
|
|
|
|
|
|
if (currentUser.value?.enterprise_info?.company_name) {
|
|
|
|
|
|
query.company_name = currentUser.value.enterprise_info.company_name
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
router.push({
|
|
|
|
|
|
name: 'AdminUsers',
|
|
|
|
|
|
query
|
|
|
|
|
|
})
|
2025-11-24 16:06:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 监听路由变化
|
|
|
|
|
|
watch(() => route.query.user_id, async (newUserId) => {
|
|
|
|
|
|
if (newUserId) {
|
|
|
|
|
|
singleUserMode.value = true
|
|
|
|
|
|
await loadUserInfo(newUserId)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
singleUserMode.value = false
|
|
|
|
|
|
currentUser.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
loadSubscriptions()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 加载订阅列表
|
|
|
|
|
|
const loadSubscriptions = async () => {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const params = {
|
|
|
|
|
|
page: currentPage.value,
|
|
|
|
|
|
page_size: pageSize.value,
|
|
|
|
|
|
keyword: filters.keyword,
|
|
|
|
|
|
company_name: filters.company_name,
|
|
|
|
|
|
product_name: filters.product_name
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 单用户模式下添加用户ID筛选
|
|
|
|
|
|
if (singleUserMode.value && currentUser.value?.id) {
|
|
|
|
|
|
params.user_id = currentUser.value.id
|
|
|
|
|
|
console.log('单用户模式,添加用户ID筛选:', params.user_id)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加时间范围参数
|
|
|
|
|
|
if (filters.timeRange && filters.timeRange.length === 2) {
|
|
|
|
|
|
params.start_time = filters.timeRange[0]
|
|
|
|
|
|
params.end_time = filters.timeRange[1]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('订阅列表请求参数:', params)
|
|
|
|
|
|
const response = await productAdminApi.getSubscriptions(params)
|
|
|
|
|
|
console.log('订阅列表响应:', response.data)
|
|
|
|
|
|
subscriptions.value = response.data?.items || []
|
|
|
|
|
|
total.value = response.data?.total || 0
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载订阅失败:', error)
|
|
|
|
|
|
ElMessage.error('加载订阅失败')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化价格
|
|
|
|
|
|
const formatPrice = (price) => {
|
|
|
|
|
|
if (!price && price !== 0) return '0.00'
|
|
|
|
|
|
const num = Number(price)
|
|
|
|
|
|
if (isNaN(num)) return '0.00'
|
|
|
|
|
|
return num.toFixed(2)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化日期
|
|
|
|
|
|
const formatDate = (date) => {
|
|
|
|
|
|
if (!date) return '-'
|
|
|
|
|
|
try {
|
|
|
|
|
|
return new Date(date).toLocaleDateString('zh-CN', {
|
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
|
day: '2-digit'
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return '-'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化时间
|
|
|
|
|
|
const formatTime = (date) => {
|
|
|
|
|
|
if (!date) return '-'
|
|
|
|
|
|
try {
|
|
|
|
|
|
return new Date(date).toLocaleTimeString('zh-CN', {
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
|
minute: '2-digit'
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return '-'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算折扣比例(四舍五入到1位小数)
|
|
|
|
|
|
const calculateDiscount = (originalPrice, currentPrice) => {
|
|
|
|
|
|
if (!originalPrice || originalPrice <= 0) return 10
|
|
|
|
|
|
const discount = (currentPrice / originalPrice) * 10
|
|
|
|
|
|
return Math.round(discount * 10) / 10
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据折扣计算价格(四舍五入到2位小数)
|
|
|
|
|
|
const calculatePriceByDiscount = (originalPrice, discount) => {
|
|
|
|
|
|
if (!originalPrice || !discount) return 0
|
|
|
|
|
|
const price = (originalPrice * discount) / 10
|
|
|
|
|
|
return Math.round(price * 100) / 100
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算成本价倍数(四舍五入到2位小数)
|
|
|
|
|
|
const calculateCostMultiple = (costPrice, currentPrice) => {
|
|
|
|
|
|
if (!costPrice || costPrice <= 0) return 0
|
|
|
|
|
|
const multiple = currentPrice / costPrice
|
|
|
|
|
|
return Math.round(multiple * 100) / 100
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据成本价倍数计算价格(四舍五入到2位小数)
|
|
|
|
|
|
const calculatePriceByCostMultiple = (costPrice, multiple) => {
|
|
|
|
|
|
if (!costPrice || !multiple) return 0
|
|
|
|
|
|
const price = costPrice * multiple
|
|
|
|
|
|
return Math.round(price * 100) / 100
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理筛选变化
|
|
|
|
|
|
const handleFilterChange = () => {
|
|
|
|
|
|
currentPage.value = 1
|
|
|
|
|
|
loadSubscriptions()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理搜索
|
|
|
|
|
|
const handleSearch = () => {
|
|
|
|
|
|
if (searchTimer) {
|
|
|
|
|
|
clearTimeout(searchTimer)
|
|
|
|
|
|
}
|
|
|
|
|
|
searchTimer = setTimeout(() => {
|
|
|
|
|
|
currentPage.value = 1
|
|
|
|
|
|
loadSubscriptions()
|
|
|
|
|
|
}, 500)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理时间范围变化
|
|
|
|
|
|
const handleTimeRangeChange = () => {
|
|
|
|
|
|
currentPage.value = 1
|
|
|
|
|
|
loadSubscriptions()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重置筛选
|
|
|
|
|
|
const resetFilters = () => {
|
|
|
|
|
|
filters.keyword = ''
|
|
|
|
|
|
filters.company_name = ''
|
|
|
|
|
|
filters.product_name = ''
|
|
|
|
|
|
filters.timeRange = []
|
|
|
|
|
|
currentPage.value = 1
|
|
|
|
|
|
loadSubscriptions()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理分页大小变化
|
|
|
|
|
|
const handleSizeChange = (size) => {
|
|
|
|
|
|
pageSize.value = size
|
|
|
|
|
|
currentPage.value = 1
|
|
|
|
|
|
loadSubscriptions()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理当前页变化
|
|
|
|
|
|
const handleCurrentChange = (page) => {
|
|
|
|
|
|
currentPage.value = page
|
|
|
|
|
|
loadSubscriptions()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 调整价格
|
|
|
|
|
|
const handleEditPrice = (subscription) => {
|
|
|
|
|
|
selectedSubscription.value = subscription
|
|
|
|
|
|
priceForm.price = subscription.price
|
|
|
|
|
|
const productPrice = subscription.product?.price || subscription.product_admin?.price
|
|
|
|
|
|
priceForm.discount = calculateDiscount(productPrice, subscription.price)
|
|
|
|
|
|
if (subscription.product_admin?.cost_price) {
|
|
|
|
|
|
priceForm.cost_multiple = calculateCostMultiple(subscription.product_admin.cost_price, subscription.price)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
priceForm.cost_multiple = 1.0
|
|
|
|
|
|
}
|
|
|
|
|
|
priceForm.adjustmentType = 'price'
|
|
|
|
|
|
priceDialogVisible.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理调整方式变化
|
|
|
|
|
|
const handleAdjustmentTypeChange = () => {
|
|
|
|
|
|
const productPrice = selectedSubscription.value?.product?.price || selectedSubscription.value?.product_admin?.price
|
|
|
|
|
|
const costPrice = selectedSubscription.value?.product_admin?.cost_price
|
|
|
|
|
|
|
|
|
|
|
|
if (priceForm.adjustmentType === 'discount') {
|
|
|
|
|
|
// 切换到折扣模式时,根据当前价格计算折扣
|
|
|
|
|
|
if (productPrice) {
|
|
|
|
|
|
priceForm.discount = calculateDiscount(productPrice, priceForm.price)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (priceForm.adjustmentType === 'cost_multiple') {
|
|
|
|
|
|
// 切换到成本价倍数模式时,检查成本价是否有效
|
|
|
|
|
|
if (!hasValidCostPrice.value) {
|
|
|
|
|
|
// 如果成本价无效,切换回直接输入价格模式并提示
|
|
|
|
|
|
ElMessage.warning('该产品未设置成本价或成本价为0,无法使用按成本价倍数调整')
|
|
|
|
|
|
priceForm.adjustmentType = 'price'
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// 根据当前价格计算倍数
|
|
|
|
|
|
if (costPrice && costPrice > 0) {
|
|
|
|
|
|
priceForm.cost_multiple = calculateCostMultiple(costPrice, priceForm.price)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理价格输入变化
|
|
|
|
|
|
const handlePriceInput = () => {
|
|
|
|
|
|
const productPrice = selectedSubscription.value?.product?.price || selectedSubscription.value?.product_admin?.price
|
|
|
|
|
|
const costPrice = selectedSubscription.value?.product_admin?.cost_price
|
|
|
|
|
|
|
|
|
|
|
|
if (priceForm.adjustmentType === 'discount' && productPrice) {
|
|
|
|
|
|
// 如果当前是折扣模式,同步更新折扣值
|
|
|
|
|
|
priceForm.discount = calculateDiscount(productPrice, priceForm.price)
|
|
|
|
|
|
} else if (priceForm.adjustmentType === 'cost_multiple' && costPrice) {
|
|
|
|
|
|
// 如果当前是成本价倍数模式,同步更新倍数
|
|
|
|
|
|
priceForm.cost_multiple = calculateCostMultiple(costPrice, priceForm.price)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理折扣输入变化
|
|
|
|
|
|
const handleDiscountInput = () => {
|
|
|
|
|
|
if (priceForm.adjustmentType === 'discount') {
|
|
|
|
|
|
// 如果当前是折扣模式,同步更新价格值
|
|
|
|
|
|
const productPrice = selectedSubscription.value?.product?.price || selectedSubscription.value?.product_admin?.price
|
|
|
|
|
|
if (productPrice) {
|
|
|
|
|
|
priceForm.price = calculatePriceByDiscount(productPrice, priceForm.discount)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理成本价倍数输入变化
|
|
|
|
|
|
const handleCostMultipleInput = () => {
|
|
|
|
|
|
if (priceForm.adjustmentType === 'cost_multiple') {
|
|
|
|
|
|
// 如果当前是成本价倍数模式,同步更新价格值
|
|
|
|
|
|
const costPrice = selectedSubscription.value?.product_admin?.cost_price
|
|
|
|
|
|
if (costPrice) {
|
|
|
|
|
|
priceForm.price = calculatePriceByCostMultiple(costPrice, priceForm.cost_multiple)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新价格
|
|
|
|
|
|
const handleUpdatePrice = async () => {
|
|
|
|
|
|
if (!priceFormRef.value) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 如果选择按成本价倍数调整,验证成本价是否有效
|
|
|
|
|
|
if (priceForm.adjustmentType === 'cost_multiple' && !hasValidCostPrice.value) {
|
|
|
|
|
|
ElMessage.error('该产品未设置成本价或成本价为0,无法使用按成本价倍数调整')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await priceFormRef.value.validate()
|
|
|
|
|
|
updatingPrice.value = true
|
|
|
|
|
|
|
|
|
|
|
|
// 根据调整方式计算最终价格
|
|
|
|
|
|
let finalPrice = priceForm.price
|
|
|
|
|
|
const productPrice = selectedSubscription.value?.product?.price || selectedSubscription.value?.product_admin?.price
|
|
|
|
|
|
const costPrice = selectedSubscription.value?.product_admin?.cost_price
|
|
|
|
|
|
|
|
|
|
|
|
if (priceForm.adjustmentType === 'discount' && productPrice) {
|
|
|
|
|
|
finalPrice = calculatePriceByDiscount(productPrice, priceForm.discount)
|
|
|
|
|
|
// 更新表单中的价格值,确保显示一致
|
|
|
|
|
|
priceForm.price = finalPrice
|
|
|
|
|
|
} else if (priceForm.adjustmentType === 'cost_multiple') {
|
|
|
|
|
|
// 再次验证成本价
|
|
|
|
|
|
if (!costPrice || costPrice <= 0) {
|
|
|
|
|
|
ElMessage.error('该产品未设置成本价或成本价为0,无法使用按成本价倍数调整')
|
|
|
|
|
|
updatingPrice.value = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
finalPrice = calculatePriceByCostMultiple(costPrice, priceForm.cost_multiple)
|
|
|
|
|
|
// 更新表单中的价格值,确保显示一致
|
|
|
|
|
|
priceForm.price = finalPrice
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await productAdminApi.updateSubscriptionPrice(selectedSubscription.value.id, {
|
|
|
|
|
|
price: finalPrice
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
ElMessage.success('价格调整成功')
|
|
|
|
|
|
priceDialogVisible.value = false
|
|
|
|
|
|
|
|
|
|
|
|
// 重新加载数据
|
|
|
|
|
|
await loadSubscriptions()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (error !== false) { // 不是表单验证错误
|
|
|
|
|
|
console.error('调整价格失败:', error)
|
|
|
|
|
|
ElMessage.error(error.response?.data?.message || '调整价格失败,请重试')
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
updatingPrice.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查看详情
|
|
|
|
|
|
const handleViewDetails = (subscription) => {
|
|
|
|
|
|
// TODO: 实现查看详情功能
|
|
|
|
|
|
ElMessage.info('查看详情功能开发中')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 显示一键改价弹窗
|
|
|
|
|
|
const showBatchPriceDialog = () => {
|
|
|
|
|
|
batchPriceForm.adjustmentType = 'discount'
|
|
|
|
|
|
batchPriceForm.discount = 10
|
|
|
|
|
|
batchPriceForm.cost_multiple = 1.0
|
|
|
|
|
|
batchPriceForm.scope = 'undiscounted'
|
|
|
|
|
|
batchPriceDialogVisible.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 处理一键改价
|
|
|
|
|
|
const handleBatchUpdatePrice = async () => {
|
|
|
|
|
|
if (!batchPriceFormRef.value) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await batchPriceFormRef.value.validate()
|
|
|
|
|
|
updatingBatchPrice.value = true
|
|
|
|
|
|
|
|
|
|
|
|
// 确认操作
|
|
|
|
|
|
let confirmMessage = ''
|
|
|
|
|
|
if (batchPriceForm.adjustmentType === 'discount') {
|
|
|
|
|
|
confirmMessage = `确定要将该用户${batchPriceForm.scope === 'undiscounted' ? '所有未打折的订阅' : '所有订阅'}的价格调整为 ${batchPriceForm.discount} 折吗?此操作不可撤销!`
|
|
|
|
|
|
} else {
|
|
|
|
|
|
confirmMessage = `确定要将该用户${batchPriceForm.scope === 'undiscounted' ? '所有未打折的订阅' : '所有订阅'}的价格调整为成本价的 ${batchPriceForm.cost_multiple} 倍吗?此操作不可撤销!`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const confirmed = await ElMessageBox.confirm(
|
|
|
|
|
|
confirmMessage,
|
|
|
|
|
|
'确认一键改价',
|
|
|
|
|
|
{
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning'
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (confirmed) {
|
|
|
|
|
|
const requestData = {
|
|
|
|
|
|
user_id: currentUser.value.id,
|
|
|
|
|
|
adjustment_type: batchPriceForm.adjustmentType,
|
|
|
|
|
|
scope: batchPriceForm.scope
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (batchPriceForm.adjustmentType === 'discount') {
|
|
|
|
|
|
requestData.discount = batchPriceForm.discount
|
|
|
|
|
|
} else {
|
|
|
|
|
|
requestData.cost_multiple = batchPriceForm.cost_multiple
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await productAdminApi.batchUpdateSubscriptionPrices(requestData)
|
|
|
|
|
|
|
|
|
|
|
|
ElMessage.success('一键改价成功')
|
|
|
|
|
|
batchPriceDialogVisible.value = false
|
|
|
|
|
|
|
|
|
|
|
|
// 重新加载数据
|
|
|
|
|
|
await loadSubscriptions()
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (error !== 'cancel') { // 不是用户取消
|
|
|
|
|
|
console.error('一键改价失败:', error)
|
|
|
|
|
|
ElMessage.error(error.response?.data?.message || '一键改价失败,请重试')
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
updatingBatchPrice.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
/* 单用户模式头部样式 */
|
|
|
|
|
|
.single-user-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
|
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
|
|
|
|
|
border: 1px solid rgba(226, 232, 240, 0.6);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-icon {
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
color: #3b82f6;
|
|
|
|
|
|
background: rgba(59, 130, 246, 0.1);
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-details {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.company-name {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-phone {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 价格调整弹窗样式 */
|
|
|
|
|
|
.price-dialog :deep(.el-dialog) {
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 一键改价弹窗样式 */
|
|
|
|
|
|
.batch-price-dialog :deep(.el-dialog) {
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.batch-price-dialog :deep(.el-dialog__header) {
|
|
|
|
|
|
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
|
|
|
|
|
border-bottom: 1px solid rgba(251, 191, 36, 0.6);
|
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.batch-price-dialog :deep(.el-dialog__title) {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #92400e;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.batch-price-dialog :deep(.el-dialog__body) {
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.batch-price-dialog :deep(.el-dialog__footer) {
|
|
|
|
|
|
background: rgba(254, 243, 199, 0.5);
|
|
|
|
|
|
border-top: 1px solid rgba(251, 191, 36, 0.4);
|
|
|
|
|
|
padding: 16px 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.price-dialog :deep(.el-dialog__header) {
|
|
|
|
|
|
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
|
|
|
|
|
border-bottom: 1px solid rgba(226, 232, 240, 0.6);
|
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.price-dialog :deep(.el-dialog__title) {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.price-dialog :deep(.el-dialog__body) {
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.price-dialog :deep(.el-dialog__footer) {
|
|
|
|
|
|
background: rgba(248, 250, 252, 0.5);
|
|
|
|
|
|
border-top: 1px solid rgba(226, 232, 240, 0.4);
|
|
|
|
|
|
padding: 16px 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 表格样式优化 */
|
|
|
|
|
|
:deep(.el-table) {
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-table th) {
|
|
|
|
|
|
background: #f8fafc !important;
|
|
|
|
|
|
border-bottom: 1px solid #e2e8f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-table td) {
|
|
|
|
|
|
border-bottom: 1px solid #f1f5f9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-table tr:hover > td) {
|
|
|
|
|
|
background: #f8fafc !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 14:17:31 +08:00
|
|
|
|
/* 移动端卡片布局 */
|
|
|
|
|
|
.subscription-cards {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.subscription-card {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
padding-bottom: 12px;
|
|
|
|
|
|
border-bottom: 1px solid #f3f4f6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-body {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-label {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
min-width: 80px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-value {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-footer {
|
|
|
|
|
|
padding-top: 12px;
|
|
|
|
|
|
border-top: 1px solid #f3f4f6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-buttons {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-btn {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 表格容器 */
|
|
|
|
|
|
.table-container {
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 16:06:44 +08:00
|
|
|
|
/* 响应式设计 */
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.single-user-header {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-actions {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 14:17:31 +08:00
|
|
|
|
.price-dialog :deep(.el-dialog),
|
|
|
|
|
|
.batch-price-dialog :deep(.el-dialog) {
|
2025-11-24 16:06:44 +08:00
|
|
|
|
margin: 20px;
|
|
|
|
|
|
width: calc(100% - 40px) !important;
|
|
|
|
|
|
}
|
2025-12-10 14:17:31 +08:00
|
|
|
|
|
|
|
|
|
|
/* 表格在移动端优化 */
|
|
|
|
|
|
.table-container {
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-table) {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
min-width: 1000px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-table th),
|
|
|
|
|
|
:deep(.el-table td) {
|
|
|
|
|
|
padding: 8px 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-table .cell) {
|
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 分页组件在移动端优化 */
|
|
|
|
|
|
:deep(.el-pagination) {
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-pagination .el-pagination__sizes) {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-pagination .el-pagination__total) {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-pagination .el-pagination__jump) {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 超小屏幕进一步优化 */
|
|
|
|
|
|
@media (max-width: 480px) {
|
|
|
|
|
|
.subscription-card {
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-header {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-body {
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-label {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
min-width: 70px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-value {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-row {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-value {
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-buttons {
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-btn {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
padding: 6px 8px;
|
|
|
|
|
|
}
|
2025-11-24 16:06:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|