Files
tyapi-frontend/src/pages/admin/subscriptions/index.vue

1478 lines
47 KiB
Vue
Raw Normal View History

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>
2025-12-23 16:17:03 +08:00
<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>
2025-12-10 14:17:31 +08:00
<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>
2025-12-23 16:17:03 +08:00
<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>
2025-12-10 14:17:31 +08:00
<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>
2025-12-23 16:17:03 +08:00
<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>
2025-11-24 16:06:44 +08:00
<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>
2025-12-23 16:17:03 +08:00
<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>
2025-11-24 16:06:44 +08:00
<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,
2025-12-23 16:17:03 +08:00
ui_component_price: 0,
2025-11-24 16:06:44 +08:00
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' }
],
2025-12-23 16:17:03 +08:00
ui_component_price: [
{ required: false, message: '请输入UI组件价格', trigger: 'blur' },
{ type: 'number', min: 0, message: 'UI组件价格不能小于0', trigger: 'blur' }
],
2025-11-24 16:06:44 +08:00
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
})
2025-12-23 16:17:03 +08:00
// 计算属性:检查是否有任何组合包产品
const hasAnyPackageProducts = computed(() => {
return subscriptions.value.some(sub => sub.product?.is_package || sub.product_admin?.is_package)
})
2025-11-24 16:06:44 +08:00
// 初始化
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
2025-12-23 16:17:03 +08:00
// 获取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
2025-11-24 16:06:44 +08:00
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
}
2025-12-23 16:17:03 +08:00
// 构建请求数据
const requestData = {
2025-11-24 16:06:44 +08:00
price: finalPrice
2025-12-23 16:17:03 +08:00
}
// 如果是组合包包含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)
2025-11-24 16:06:44 +08:00
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>