1476 lines
47 KiB
Vue
1476 lines
47 KiB
Vue
<template>
|
||
<ListPageLayout
|
||
title="订阅管理"
|
||
subtitle="管理用户订阅和价格调整"
|
||
>
|
||
<!-- 单用户模式头部 -->
|
||
<template #actions v-if="singleUserMode">
|
||
<div :class="['single-user-header', isMobile ? 'flex-col' : '']">
|
||
<div class="user-info">
|
||
<el-icon class="user-icon"><user /></el-icon>
|
||
<div class="user-details">
|
||
<div class="company-name">{{ currentUser?.company_name || '未知公司' }}</div>
|
||
<div class="user-phone">{{ currentUser?.phone || '-' }}</div>
|
||
</div>
|
||
</div>
|
||
<div :class="['user-actions', isMobile ? 'w-full flex-wrap' : '']">
|
||
<el-button :size="isMobile ? 'small' : 'small'" @click="showBatchPriceDialog" type="warning">
|
||
<el-icon><edit /></el-icon>
|
||
<span>一键改价</span>
|
||
</el-button>
|
||
<el-button :size="isMobile ? 'small' : 'small'" @click="exitSingleUserMode" type="info">
|
||
<el-icon><close /></el-icon>
|
||
<span>取消</span>
|
||
</el-button>
|
||
<el-button :size="isMobile ? 'small' : 'small'" @click="goBackToUsers" type="primary">
|
||
<el-icon><back /></el-icon>
|
||
<span>返回用户管理</span>
|
||
</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>
|
||
|
||
<FilterItem label="订阅时间" class="col-span-1">
|
||
<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"
|
||
:size="isMobile ? 'small' : 'default'"
|
||
/>
|
||
</FilterItem>
|
||
|
||
<template #stats>
|
||
共找到 {{ total }} 个订阅
|
||
<span v-if="singleUserMode" class="text-blue-600">
|
||
(仅显示当前用户)
|
||
</span>
|
||
</template>
|
||
|
||
<template #buttons>
|
||
<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>
|
||
</template>
|
||
</FilterSection>
|
||
</template>
|
||
|
||
<template #table>
|
||
<!-- 加载状态 -->
|
||
<div v-if="loading" class="flex justify-center items-center py-12">
|
||
<el-loading size="large" />
|
||
</div>
|
||
|
||
<!-- 移动端卡片布局 -->
|
||
<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 v-if="subscription.product?.is_package || subscription.product_admin?.is_package" class="card-row">
|
||
<span class="card-label">UI组件价格</span>
|
||
<span class="card-value text-purple-600 font-semibold">¥{{ formatPrice(subscription.ui_component_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 v-if="hasAnyPackageProducts" label="UI组件价格" width="130">
|
||
<template #default="{ row }">
|
||
<span v-if="row.product?.is_package || row.product_admin?.is_package" class="font-medium text-purple-600">¥{{ formatPrice(row.ui_component_price) }}</span>
|
||
<span v-else class="text-gray-400">-</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">
|
||
<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"
|
||
:layout="isMobile ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper'"
|
||
:small="isMobile"
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange"
|
||
/>
|
||
</template>
|
||
|
||
<template #extra>
|
||
<!-- 价格调整弹窗 -->
|
||
<el-dialog
|
||
v-model="priceDialogVisible"
|
||
title="调整订阅价格"
|
||
:width="isMobile ? '90%' : '600px'"
|
||
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 v-if="selectedSubscription?.product?.is_package || selectedSubscription?.product_admin?.is_package" label="UI组件价格">
|
||
<div class="flex flex-col gap-1">
|
||
<span class="text-lg font-semibold text-purple-600">
|
||
¥{{ formatPrice(selectedSubscription?.ui_component_price) }}
|
||
</span>
|
||
</div>
|
||
</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="(selectedSubscription?.product?.is_package || selectedSubscription?.product_admin?.is_package) && priceForm.adjustmentType === 'price'" label="UI组件价格" prop="ui_component_price">
|
||
<el-input-number
|
||
v-model="priceForm.ui_component_price"
|
||
:precision="2"
|
||
:min="0"
|
||
:step="0.01"
|
||
placeholder="请输入UI组件价格"
|
||
class="w-full"
|
||
/>
|
||
<div class="text-sm text-gray-500 mt-1">
|
||
<div class="text-purple-600">
|
||
组合包UI组件的购买报告价格
|
||
</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>
|
||
<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"
|
||
>
|
||
确认调整
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 一键改价弹窗 -->
|
||
<el-dialog
|
||
v-model="batchPriceDialogVisible"
|
||
title="一键改价"
|
||
:width="isMobile ? '90%' : '500px'"
|
||
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>
|
||
<div :class="['flex gap-3', isMobile ? 'flex-col' : 'justify-end']">
|
||
<el-button
|
||
:class="isMobile ? 'w-full' : ''"
|
||
@click="batchPriceDialogVisible = false"
|
||
>
|
||
取消
|
||
</el-button>
|
||
<el-button
|
||
type="warning"
|
||
:class="isMobile ? 'w-full' : ''"
|
||
@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'
|
||
import { useMobileTable } from '@/composables/useMobileTable'
|
||
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()
|
||
|
||
// 移动端检测
|
||
const { isMobile, isTablet } = useMobileTable()
|
||
|
||
// 响应式数据
|
||
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,
|
||
ui_component_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' }
|
||
],
|
||
ui_component_price: [
|
||
{ required: false, message: '请输入UI组件价格', trigger: 'blur' },
|
||
{ type: 'number', min: 0, message: 'UI组件价格不能小于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
|
||
})
|
||
|
||
// 计算属性:检查是否有任何组合包产品
|
||
const hasAnyPackageProducts = computed(() => {
|
||
return subscriptions.value.some(sub => sub.product?.is_package || sub.product_admin?.is_package)
|
||
})
|
||
|
||
// 初始化
|
||
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 = () => {
|
||
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
|
||
})
|
||
}
|
||
|
||
// 监听路由变化
|
||
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
|
||
|
||
// 获取UI组件价格,如果订阅中没有设置,则从产品中获取
|
||
let uiComponentPrice = subscription.ui_component_price || 0
|
||
if (uiComponentPrice === 0 && (subscription.product?.is_package || subscription.product_admin?.is_package)) {
|
||
uiComponentPrice = subscription.product?.ui_component_price || subscription.product_admin?.ui_component_price || 0
|
||
}
|
||
priceForm.ui_component_price = uiComponentPrice
|
||
|
||
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
|
||
}
|
||
|
||
// 构建请求数据
|
||
const requestData = {
|
||
price: finalPrice
|
||
}
|
||
|
||
// 如果是组合包,包含UI组件价格
|
||
if (selectedSubscription.value.product?.is_package || selectedSubscription.value.product_admin?.is_package) {
|
||
// 只有在直接输入价格模式下才使用表单中的UI组件价格
|
||
if (priceForm.adjustmentType === 'price') {
|
||
requestData.ui_component_price = priceForm.ui_component_price
|
||
}
|
||
}
|
||
|
||
await productAdminApi.updateSubscriptionPrice(selectedSubscription.value.id, requestData)
|
||
|
||
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;
|
||
}
|
||
|
||
/* 移动端卡片布局 */
|
||
.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;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.single-user-header {
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.user-actions {
|
||
width: 100%;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.price-dialog :deep(.el-dialog),
|
||
.batch-price-dialog :deep(.el-dialog) {
|
||
margin: 20px;
|
||
width: calc(100% - 40px) !important;
|
||
}
|
||
|
||
/* 表格在移动端优化 */
|
||
.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;
|
||
}
|
||
}
|
||
</style>
|