Files
tyapi-frontend/src/pages/admin/subscriptions/index.vue
2026-01-03 12:54:19 +08:00

1476 lines
47 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<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>